Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36659bed85 | ||
|
|
644db170dd | ||
|
|
f97953f15c | ||
|
|
95b3cad101 |
No files matched your search
+1
-1
@@ -30,12 +30,12 @@
|
||||
- CLI: add remote gateway client config (gateway.remote.*) with Bonjour-assisted discovery.
|
||||
- Skills: allow `bun` as a node manager for skill installs.
|
||||
- Skills: add `things-mac` (Things 3 CLI) for read/search plus add/update via URL scheme.
|
||||
- Skills: add `bear-notes` (Bear) skill via grizzly CLI (#120) — thanks @tylerwince.
|
||||
- Tests: add a Docker-based onboarding E2E harness.
|
||||
- Tests: harden wizard E2E flows for reset, providers, skills, and remote non-interactive runs.
|
||||
- Browser tools: add remote CDP URL support, Linux launcher options (`executablePath`, `noSandbox`), and surface `cdpUrl` in status.
|
||||
|
||||
### Fixes
|
||||
- Gmail hooks: auto-restart watcher on gateway restart, prevent double-start, redact gog tokens (#87) — thanks @jverdi.
|
||||
- Chat UI: keep the chat scrolled to the latest message after switching sessions.
|
||||
- WebChat: stream live updates for sessions even when runs start outside the chat UI.
|
||||
- Gateway CLI: read `CLAWDIS_GATEWAY_PASSWORD` from environment in `callGateway()` — allows `doctor`/`health` commands to auth without explicit `--password` flag.
|
||||
|
||||
@@ -644,11 +644,6 @@ Gmail helper config (used by `clawdis hooks gmail setup` / `run`):
|
||||
}
|
||||
```
|
||||
|
||||
Gateway auto-start:
|
||||
- If `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts
|
||||
`gog gmail watch serve` on boot and auto-renews the watch.
|
||||
- Set `CLAWDIS_SKIP_GMAIL_WATCHER=1` to disable the auto-start (for manual runs).
|
||||
|
||||
Note: when `tailscale.mode` is on, Clawdis defaults `serve.path` to `/` so
|
||||
Tailscale can proxy `/gmail-pubsub` correctly (it strips the set-path prefix).
|
||||
|
||||
|
||||
@@ -56,12 +56,7 @@ Want a custom endpoint? Use `--push-endpoint <url>` or `--tailscale off`.
|
||||
Platform note: on macOS the wizard installs `gcloud`, `gogcli`, and `tailscale`
|
||||
via Homebrew; on Linux install them manually first.
|
||||
|
||||
Gateway auto-start (recommended):
|
||||
- When `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts
|
||||
`gog gmail watch serve` on boot and auto-renews the watch.
|
||||
- Set `CLAWDIS_SKIP_GMAIL_WATCHER=1` to opt out (useful if you run the daemon yourself).
|
||||
|
||||
Manual daemon (starts `gog gmail watch serve` + auto-renew):
|
||||
Run the daemon (starts `gog gmail watch serve` + auto-renew):
|
||||
|
||||
```bash
|
||||
clawdis hooks gmail run
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ read_when:
|
||||
<p align="center">
|
||||
<a href="https://github.com/steipete/clawdis">GitHub</a> ·
|
||||
<a href="https://github.com/steipete/clawdis/releases">Releases</a> ·
|
||||
<a href="./clawd.md">Clawd setup</a>
|
||||
<a href="./clawd">Clawd setup</a>
|
||||
</p>
|
||||
|
||||
CLAWDIS bridges WhatsApp (via WhatsApp Web / Baileys), Telegram (Bot API / grammY), Discord (Bot API / discord.js), and iMessage (imsg CLI) to coding agents like [Pi](https://github.com/badlogic/pi-mono).
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: bear-notes
|
||||
description: Create, search, and manage Bear notes via grizzly CLI.
|
||||
homepage: https://bear.app
|
||||
metadata: {"clawdis":{"emoji":"🐻","os":["darwin"],"requires":{"bins":["grizzly"]},"install":[{"id":"go","kind":"go","module":"github.com/tylerwince/grizzly/cmd/grizzly@latest","bins":["grizzly"],"label":"Install grizzly (go)"}]}}
|
||||
---
|
||||
|
||||
# Bear Notes
|
||||
|
||||
Use `grizzly` to create, read, and manage notes in Bear on macOS.
|
||||
|
||||
Requirements
|
||||
- Bear app installed and running
|
||||
- For some operations (add-text, tags, open-note --selected), a Bear app token (stored in `~/.config/grizzly/token`)
|
||||
|
||||
## Getting a Bear Token
|
||||
|
||||
For operations that require a token (add-text, tags, open-note --selected), you need an authentication token:
|
||||
1. Open Bear → Help → API Token → Copy Token
|
||||
2. Save it: `echo "YOUR_TOKEN" > ~/.config/grizzly/token`
|
||||
|
||||
## Common Commands
|
||||
|
||||
Create a note
|
||||
```bash
|
||||
echo "Note content here" | grizzly create --title "My Note" --tag work
|
||||
grizzly create --title "Quick Note" --tag inbox < /dev/null
|
||||
```
|
||||
|
||||
Open/read a note by ID
|
||||
```bash
|
||||
grizzly open-note --id "NOTE_ID" --enable-callback --json
|
||||
```
|
||||
|
||||
Append text to a note
|
||||
```bash
|
||||
echo "Additional content" | grizzly add-text --id "NOTE_ID" --mode append --token-file ~/.config/grizzly/token
|
||||
```
|
||||
|
||||
List all tags
|
||||
```bash
|
||||
grizzly tags --enable-callback --json --token-file ~/.config/grizzly/token
|
||||
```
|
||||
|
||||
Search notes (via open-tag)
|
||||
```bash
|
||||
grizzly open-tag --name "work" --enable-callback --json
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
Common flags:
|
||||
- `--dry-run` — Preview the URL without executing
|
||||
- `--print-url` — Show the x-callback-url
|
||||
- `--enable-callback` — Wait for Bear's response (needed for reading data)
|
||||
- `--json` — Output as JSON (when using callbacks)
|
||||
- `--token-file PATH` — Path to Bear API token file
|
||||
|
||||
## Configuration
|
||||
|
||||
Grizzly reads config from (in priority order):
|
||||
1. CLI flags
|
||||
2. Environment variables (`GRIZZLY_TOKEN_FILE`, `GRIZZLY_CALLBACK_URL`, `GRIZZLY_TIMEOUT`)
|
||||
3. `.grizzly.toml` in current directory
|
||||
4. `~/.config/grizzly/config.toml`
|
||||
|
||||
Example `~/.config/grizzly/config.toml`:
|
||||
```toml
|
||||
token_file = "~/.config/grizzly/token"
|
||||
callback_url = "http://127.0.0.1:42123/success"
|
||||
timeout = "5s"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Bear must be running for commands to work
|
||||
- Note IDs are Bear's internal identifiers (visible in note info or via callbacks)
|
||||
- Use `--enable-callback` when you need to read data back from Bear
|
||||
- Some operations require a valid token (add-text, tags, open-note --selected)
|
||||
@@ -75,7 +75,6 @@ import {
|
||||
} from "../discord/index.js";
|
||||
import { type DiscordProbe, probeDiscord } from "../discord/probe.js";
|
||||
import { isVerbose } from "../globals.js";
|
||||
import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js";
|
||||
import {
|
||||
monitorIMessageProvider,
|
||||
sendMessageIMessage,
|
||||
@@ -6692,24 +6691,6 @@ export async function startGatewayServer(
|
||||
logBrowser.error(`server failed to start: ${String(err)}`);
|
||||
}
|
||||
|
||||
// Start Gmail watcher if configured (hooks.gmail.account).
|
||||
if (process.env.CLAWDIS_SKIP_GMAIL_WATCHER !== "1") {
|
||||
try {
|
||||
const gmailResult = await startGmailWatcher(cfgAtStart);
|
||||
if (gmailResult.started) {
|
||||
logHooks.info("gmail watcher started");
|
||||
} else if (
|
||||
gmailResult.reason &&
|
||||
gmailResult.reason !== "hooks not enabled" &&
|
||||
gmailResult.reason !== "no gmail account configured"
|
||||
) {
|
||||
logHooks.warn(`gmail watcher not started: ${gmailResult.reason}`);
|
||||
}
|
||||
} catch (err) {
|
||||
logHooks.error(`gmail watcher failed to start: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Launch configured providers (WhatsApp Web, Discord, Telegram) so gateway replies via the
|
||||
// surface the message came from. Tests can opt out via CLAWDIS_SKIP_PROVIDERS.
|
||||
if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") {
|
||||
@@ -6768,7 +6749,6 @@ export async function startGatewayServer(
|
||||
await stopDiscordProvider();
|
||||
await stopSignalProvider();
|
||||
await stopIMessageProvider();
|
||||
await stopGmailWatcher();
|
||||
cron.stop();
|
||||
heartbeatRunner.stop();
|
||||
broadcast("shutdown", {
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runtimeConfig = {
|
||||
account: "clawdbot@gmail.com",
|
||||
label: "INBOX",
|
||||
topic: "projects/test/topics/gog-gmail-watch",
|
||||
subscription: "gog-gmail-watch-push",
|
||||
pushToken: "push-token",
|
||||
hookToken: "hook-token",
|
||||
hookUrl: "http://127.0.0.1:18789/hooks/gmail",
|
||||
includeBody: false,
|
||||
maxBytes: 0,
|
||||
renewEveryMinutes: 720,
|
||||
serve: { bind: "127.0.0.1", port: 8788, path: "/" },
|
||||
tailscale: { mode: "off", path: "/gmail-pubsub" },
|
||||
};
|
||||
|
||||
class MockChild extends EventEmitter {
|
||||
stdout = new EventEmitter();
|
||||
stderr = new EventEmitter();
|
||||
exitCode: number | null = null;
|
||||
kill = vi.fn();
|
||||
}
|
||||
|
||||
const spawnMock = vi.fn(() => new MockChild() as unknown as ChildProcess);
|
||||
|
||||
vi.mock("node:child_process", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("node:child_process")>(
|
||||
"node:child_process",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
spawn: spawnMock,
|
||||
};
|
||||
});
|
||||
|
||||
const runCommandWithTimeoutMock = vi.fn(async () => ({
|
||||
code: 0,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
}));
|
||||
|
||||
vi.mock("../agents/skills.js", () => ({
|
||||
hasBinary: () => true,
|
||||
}));
|
||||
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: runCommandWithTimeoutMock,
|
||||
}));
|
||||
|
||||
vi.mock("./gmail-setup-utils.js", () => ({
|
||||
ensureTailscaleEndpoint: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.mock("./gmail.js", () => ({
|
||||
buildGogWatchServeArgs: () => [
|
||||
"gmail",
|
||||
"watch",
|
||||
"serve",
|
||||
"--token",
|
||||
runtimeConfig.pushToken,
|
||||
"--hook-url",
|
||||
runtimeConfig.hookUrl,
|
||||
"--hook-token",
|
||||
runtimeConfig.hookToken,
|
||||
],
|
||||
buildGogWatchStartArgs: () => ["gmail", "watch", "start"],
|
||||
resolveGmailHookRuntimeConfig: () => ({ ok: true, value: runtimeConfig }),
|
||||
}));
|
||||
|
||||
const { startGmailWatcher, stopGmailWatcher } = await import(
|
||||
"./gmail-watcher.js"
|
||||
);
|
||||
|
||||
const cfg = {
|
||||
hooks: {
|
||||
enabled: true,
|
||||
gmail: { account: runtimeConfig.account },
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
spawnMock.mockClear();
|
||||
runCommandWithTimeoutMock.mockClear();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await stopGmailWatcher();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("gmail watcher", () => {
|
||||
it("does not start twice when already running", async () => {
|
||||
const first = await startGmailWatcher(cfg);
|
||||
const second = await startGmailWatcher(cfg);
|
||||
|
||||
expect(first.started).toBe(true);
|
||||
expect(second.started).toBe(true);
|
||||
expect(spawnMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("restarts the gog watcher after a process error", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
await startGmailWatcher(cfg);
|
||||
const child = spawnMock.mock.results[0]?.value as MockChild | undefined;
|
||||
expect(child).toBeDefined();
|
||||
child?.emit("error", new Error("boom"));
|
||||
|
||||
expect(spawnMock).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
expect(spawnMock).toHaveBeenCalledTimes(2);
|
||||
|
||||
const restartChild = spawnMock.mock.results[1]?.value as
|
||||
| MockChild
|
||||
| undefined;
|
||||
const stopPromise = stopGmailWatcher();
|
||||
restartChild?.emit("exit", 0, null);
|
||||
await stopPromise;
|
||||
});
|
||||
});
|
||||
@@ -1,273 +0,0 @@
|
||||
/**
|
||||
* Gmail Watcher Service
|
||||
*
|
||||
* Automatically starts `gog gmail watch serve` when the gateway starts,
|
||||
* if hooks.gmail is configured with an account.
|
||||
*/
|
||||
|
||||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
import { hasBinary } from "../agents/skills.js";
|
||||
import type { ClawdisConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import {
|
||||
buildGogWatchServeArgs,
|
||||
buildGogWatchStartArgs,
|
||||
type GmailHookRuntimeConfig,
|
||||
resolveGmailHookRuntimeConfig,
|
||||
} from "./gmail.js";
|
||||
import { ensureTailscaleEndpoint } from "./gmail-setup-utils.js";
|
||||
|
||||
const log = createSubsystemLogger("gmail-watcher");
|
||||
|
||||
let watcherProcess: ChildProcess | null = null;
|
||||
let renewInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let restartTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let shuttingDown = false;
|
||||
let currentConfig: GmailHookRuntimeConfig | null = null;
|
||||
const redactedValue = "<redacted>";
|
||||
const redactedFlags = new Set(["--token", "--hook-token", "--hook-url"]);
|
||||
|
||||
function redactArgs(args: string[]): string[] {
|
||||
const redacted = [...args];
|
||||
for (let i = 0; i < redacted.length; i += 1) {
|
||||
if (redactedFlags.has(redacted[i] ?? "") && redacted[i + 1]) {
|
||||
redacted[i + 1] = redactedValue;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return redacted;
|
||||
}
|
||||
|
||||
function scheduleRestart(reason: string) {
|
||||
if (shuttingDown || !currentConfig || restartTimer) return;
|
||||
log.warn(`${reason}; restarting in 5s`);
|
||||
restartTimer = setTimeout(() => {
|
||||
restartTimer = null;
|
||||
if (shuttingDown || !currentConfig) return;
|
||||
watcherProcess = spawnGogServe(currentConfig);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if gog binary is available
|
||||
*/
|
||||
function isGogAvailable(): boolean {
|
||||
return hasBinary("gog");
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Gmail watch (registers with Gmail API)
|
||||
*/
|
||||
async function startGmailWatch(
|
||||
cfg: Pick<GmailHookRuntimeConfig, "account" | "label" | "topic">,
|
||||
): Promise<boolean> {
|
||||
const args = ["gog", ...buildGogWatchStartArgs(cfg)];
|
||||
try {
|
||||
const result = await runCommandWithTimeout(args, { timeoutMs: 120_000 });
|
||||
if (result.code !== 0) {
|
||||
const message =
|
||||
result.stderr || result.stdout || "gog watch start failed";
|
||||
log.error(`watch start failed: ${message}`);
|
||||
return false;
|
||||
}
|
||||
log.info(`watch started for ${cfg.account}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
log.error(`watch start error: ${String(err)}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn the gog gmail watch serve process
|
||||
*/
|
||||
function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess {
|
||||
const args = buildGogWatchServeArgs(cfg);
|
||||
if (restartTimer) {
|
||||
clearTimeout(restartTimer);
|
||||
restartTimer = null;
|
||||
}
|
||||
log.info(`starting gog ${redactArgs(args).join(" ")}`);
|
||||
|
||||
const child = spawn("gog", args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: false,
|
||||
});
|
||||
|
||||
child.stdout?.on("data", (data: Buffer) => {
|
||||
const line = data.toString().trim();
|
||||
if (line) log.info(`[gog] ${line}`);
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (data: Buffer) => {
|
||||
const line = data.toString().trim();
|
||||
if (line) log.warn(`[gog] ${line}`);
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
log.error(`gog process error: ${String(err)}`);
|
||||
if (watcherProcess === child) {
|
||||
watcherProcess = null;
|
||||
}
|
||||
scheduleRestart("gog process error");
|
||||
});
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
if (shuttingDown) return;
|
||||
if (watcherProcess === child) {
|
||||
watcherProcess = null;
|
||||
}
|
||||
scheduleRestart(`gog exited (code=${code}, signal=${signal})`);
|
||||
});
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
export type GmailWatcherStartResult = {
|
||||
started: boolean;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the Gmail watcher service.
|
||||
* Called automatically by the gateway if hooks.gmail is configured.
|
||||
*/
|
||||
export async function startGmailWatcher(
|
||||
cfg: ClawdisConfig,
|
||||
): Promise<GmailWatcherStartResult> {
|
||||
// Check if gmail hooks are configured
|
||||
if (!cfg.hooks?.enabled) {
|
||||
return { started: false, reason: "hooks not enabled" };
|
||||
}
|
||||
|
||||
if (!cfg.hooks?.gmail?.account) {
|
||||
return { started: false, reason: "no gmail account configured" };
|
||||
}
|
||||
|
||||
// Check if gog is available
|
||||
const gogAvailable = isGogAvailable();
|
||||
if (!gogAvailable) {
|
||||
return { started: false, reason: "gog binary not found" };
|
||||
}
|
||||
|
||||
// Resolve the full runtime config
|
||||
const resolved = resolveGmailHookRuntimeConfig(cfg, {});
|
||||
if (!resolved.ok) {
|
||||
return { started: false, reason: resolved.error };
|
||||
}
|
||||
|
||||
const runtimeConfig = resolved.value;
|
||||
if (isGmailWatcherRunning()) {
|
||||
log.info("gmail watcher already running; skipping start");
|
||||
return { started: true };
|
||||
}
|
||||
|
||||
if (renewInterval) {
|
||||
clearInterval(renewInterval);
|
||||
renewInterval = null;
|
||||
}
|
||||
if (restartTimer) {
|
||||
clearTimeout(restartTimer);
|
||||
restartTimer = null;
|
||||
}
|
||||
if (watcherProcess) {
|
||||
watcherProcess = null;
|
||||
}
|
||||
|
||||
currentConfig = runtimeConfig;
|
||||
|
||||
// Set up Tailscale endpoint if needed
|
||||
if (runtimeConfig.tailscale.mode !== "off") {
|
||||
try {
|
||||
await ensureTailscaleEndpoint({
|
||||
mode: runtimeConfig.tailscale.mode,
|
||||
path: runtimeConfig.tailscale.path,
|
||||
port: runtimeConfig.serve.port,
|
||||
});
|
||||
log.info(
|
||||
`tailscale ${runtimeConfig.tailscale.mode} configured for port ${runtimeConfig.serve.port}`,
|
||||
);
|
||||
} catch (err) {
|
||||
log.error(`tailscale setup failed: ${String(err)}`);
|
||||
return {
|
||||
started: false,
|
||||
reason: `tailscale setup failed: ${String(err)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Start the Gmail watch (register with Gmail API)
|
||||
const watchStarted = await startGmailWatch(runtimeConfig);
|
||||
if (!watchStarted) {
|
||||
log.warn("gmail watch start failed, but continuing with serve");
|
||||
}
|
||||
|
||||
// Spawn the gog serve process
|
||||
shuttingDown = false;
|
||||
watcherProcess = spawnGogServe(runtimeConfig);
|
||||
|
||||
// Set up renewal interval
|
||||
const renewMs = runtimeConfig.renewEveryMinutes * 60_000;
|
||||
renewInterval = setInterval(() => {
|
||||
if (shuttingDown) return;
|
||||
void startGmailWatch(runtimeConfig);
|
||||
}, renewMs);
|
||||
|
||||
log.info(
|
||||
`gmail watcher started for ${runtimeConfig.account} (renew every ${runtimeConfig.renewEveryMinutes}m)`,
|
||||
);
|
||||
|
||||
return { started: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the Gmail watcher service.
|
||||
*/
|
||||
export async function stopGmailWatcher(): Promise<void> {
|
||||
shuttingDown = true;
|
||||
|
||||
if (restartTimer) {
|
||||
clearTimeout(restartTimer);
|
||||
restartTimer = null;
|
||||
}
|
||||
|
||||
if (renewInterval) {
|
||||
clearInterval(renewInterval);
|
||||
renewInterval = null;
|
||||
}
|
||||
|
||||
if (watcherProcess) {
|
||||
log.info("stopping gmail watcher");
|
||||
watcherProcess.kill("SIGTERM");
|
||||
|
||||
// Wait a bit for graceful shutdown
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (watcherProcess) {
|
||||
watcherProcess.kill("SIGKILL");
|
||||
}
|
||||
resolve();
|
||||
}, 3000);
|
||||
|
||||
watcherProcess?.on("exit", () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
watcherProcess = null;
|
||||
}
|
||||
|
||||
currentConfig = null;
|
||||
log.info("gmail watcher stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Gmail watcher is running.
|
||||
*/
|
||||
export function isGmailWatcherRunning(): boolean {
|
||||
return (
|
||||
watcherProcess !== null && !shuttingDown && watcherProcess.exitCode === null
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user