@opticell/workflow-companion 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +87 -14
  2. package/index.js +166 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # WorkFlow Companion
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@opticell/workflow-companion?label=npm)](https://www.npmjs.com/package/@opticell/workflow-companion)
4
+ [![MCP Registry](https://img.shields.io/badge/MCP_Registry-io.github.TeamDzX%2Fworkflow--companion-4F46E5)](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.TeamDzX/workflow-companion)
5
+
6
+ > **This repository** is WorkFlow Companion's home: the source (`src/index.js`,
7
+ > MIT licensed), downloads (`WorkFlow.mcpb` for Claude Desktop) on the
8
+ > [Releases](https://github.com/TeamDzX/workflow-companion/releases) page,
9
+ > and bugs and requests in [Issues](https://github.com/TeamDzX/workflow-companion/issues).
10
+ > The package itself installs from npm. On a Mac, use the open-source
11
+ > [workflow-agent-bridge](https://github.com/TeamDzX/workflow-agent-bridge) instead.
12
+ > All of Opticell's free tools for Claude: [opticell-mcp](https://github.com/TeamDzX/opticell-mcp).
13
+
3
14
  Lets **Claude on Windows or Linux** work in your WorkFlow tasks, notes and
4
15
  projects: reading the queue, replying in threads, ticking checklists, filing
5
16
  notes, updating documents. It works in Claude Code, Claude Desktop, or any MCP
@@ -28,12 +39,23 @@ many devices are on.
28
39
 
29
40
  ## Install
30
41
 
31
- **Windows** (PowerShell): `irm https://raw.githubusercontent.com/TeamDzX/workflow-companion/main/install.ps1 | iex`
42
+ One line installs Node.js if needed, signs this computer in, adds the
43
+ companion to Claude Code and offers to let it start work on its own:
44
+
45
+ **Windows** (PowerShell):
32
46
 
33
- **Mac or Linux:** `curl -fsSL https://raw.githubusercontent.com/TeamDzX/workflow-companion/main/install.sh | sh`
47
+ ```powershell
48
+ irm https://raw.githubusercontent.com/TeamDzX/workflow-companion/main/install.ps1 | iex
49
+ ```
34
50
 
35
- It installs Node.js if needed, signs this computer in, adds the companion to
36
- Claude Code and offers to let it start work on its own. By hand:
51
+ **Mac or Linux** (Terminal):
52
+
53
+ ```sh
54
+ curl -fsSL https://raw.githubusercontent.com/TeamDzX/workflow-companion/main/install.sh | sh
55
+ ```
56
+
57
+ Switch the companion on in WorkFlow first (step 1 below). The steps that
58
+ follow are what the installer does, if you'd rather do them by hand.
37
59
 
38
60
  ## Set up
39
61
 
@@ -158,24 +180,60 @@ npx -y @opticell/workflow-companion wake-on
158
180
  | `workflow-companion` | Runs the MCP server. This is what Claude starts. |
159
181
  | `workflow-companion setup` | Signs this computer in. Takes `--web-token` to skip the prompt. |
160
182
  | `workflow-companion status` | Which device answers, how fresh the snapshot is, any receipts that arrived late. |
183
+ | `workflow-companion doctor` | Checks everything on this computer and says how to fix what's wrong. Start here when something isn't working. |
161
184
  | `workflow-companion keepalive` | Renews the sign-in. The scheduled job runs this. |
162
185
  | `workflow-companion update` | Updates the scheduled jobs' copy to the latest release now, and shows how to keep Claude Code on `@latest`. |
163
186
  | `workflow-companion auto-update on\|off` | Whether the scheduled jobs update themselves (on by default). |
164
187
  | `workflow-companion wake-on` | Starts a Claude Code session on its own when work for its name arrives. See above. |
165
188
  | `workflow-companion wake-off` | Stops that. Work waits until you ask Claude here. |
166
189
  | `workflow-companion wake` | One check of the queue. The scheduled job runs this. |
190
+ | `workflow-companion monitor add\|list\|check\|remove` | Checks this computer runs without using Claude. See below. |
191
+ | `workflow-companion approvals on\|off` | Lets an assistant run one exact command after you reply "approve CODE" (off by default). |
192
+ | `workflow-companion inbox-on\|inbox-off` | Files dropped in a folder become tasks. |
193
+ | `workflow-companion notifications on\|off` | Notices on this computer (on by default). `notify-test` shows one. |
194
+ | `workflow-companion reports on\|off` | A short report attached to the task after each session (on by default). |
167
195
  | `workflow-companion signout` | Deletes the sign-in, cached data and both scheduled jobs. |
168
196
 
169
197
  ## On this computer, by itself (1.8)
170
198
 
171
- In the same 2-minute scheduled job, without using Claude: **monitors**
172
- (`monitor add --http/--port/--disk/--process/--file/--command`, posting
173
- changes on a task and optionally waking a session), **approvals** (an
174
- assistant asks to run one exact command and it runs once you reply
175
- "approve CODE"; off until `approvals on`), an **inbox folder** (`inbox-on`:
176
- dropped files become tasks), **notifications** on the computer, and a
177
- **session report** attached to the task after each session. Details:
178
- <https://github.com/TeamDzX/workflow-companion#on-this-computer-by-itself-18>
199
+ These run in the same 2-minute scheduled job as waking, and cost no Claude
200
+ usage.
201
+
202
+ **Monitors.** Checks that wake Claude only when something changes:
203
+
204
+ ```sh
205
+ workflow-companion monitor add --name site --http https://example.com --task "Server status" --wake
206
+ workflow-companion monitor add --name disk --disk C:\ --min-gb 20 --task "Server status"
207
+ workflow-companion monitor add --name api --port 10.0.0.5:443 --every 2
208
+ workflow-companion monitor add --name backup --file D:\backups\latest.zip --task "Backups"
209
+ workflow-companion monitor add --name nightly --command "check-backups.cmd" --task "Backups" --wake
210
+ ```
211
+
212
+ A change (down, back up, the file changed) is posted on the task, shows a
213
+ notification here, and with `--wake` starts a session about it. An assistant
214
+ can set up website, port, disk, process and file monitors itself
215
+ (`workflow_monitor`), but **only you can add a command monitor**, because it
216
+ runs a command every few minutes.
217
+
218
+ **Approvals.** An assistant can ask to run one exact command on this computer
219
+ (restart a service, install an update). It posts the command on the task with
220
+ a code; reply `approve K7Q2` (or `decline K7Q2`) from your phone. Only a
221
+ person's reply counts, never another assistant's; tasks in shared projects
222
+ can't ask; the command runs once, exactly as shown, within 24 hours. **Off
223
+ until you run `workflow-companion approvals on` on this computer.**
224
+
225
+ **Inbox folder.** `workflow-companion inbox-on` makes `~/WorkFlow Inbox`
226
+ (or `--folder`): drop a file in and it becomes a task with the file attached.
227
+ A subfolder named after a project files it into that project. Filed files
228
+ move to `Filed/<date>`. `--assign` puts them in this assistant's queue.
229
+
230
+ **Notifications.** A notice on this computer when work arrives, a monitor
231
+ changes, an approval is answered, a session needs attention, or the iCloud
232
+ sign-in has ended (with the command that fixes it).
233
+
234
+ **Session reports.** After each session that changed something, a short
235
+ report is attached to its task: what it was asked, what it changed, any
236
+ approved commands it ran, and its last words.
179
237
 
180
238
  ## Worth knowing
181
239
 
@@ -204,11 +262,26 @@ dropped files become tasks), **notifications** on the computer, and a
204
262
 
205
263
  ## Tools
206
264
 
207
- The same 29 tools as the Mac's Agent Bridge MCP server, including:
265
+ 36 tools, including:
208
266
  `workflow_queue`, `workflow_get_task`, `workflow_comment`,
209
267
  `workflow_set_status_line`, `workflow_check_subtasks`, `workflow_update_task`,
210
268
  `workflow_create_task`, `workflow_list_notes`, `workflow_reply_note`,
211
269
  `workflow_list_projects`, `workflow_list_documents`, `workflow_update_document`,
212
- `workflow_draft_email` and `workflow_state`.
270
+ `workflow_draft_email`, `workflow_state`, and from 1.7 and 1.8
271
+ `workflow_hand_off`, `workflow_watch`, `workflow_house_rules`,
272
+ `workflow_monitor`, `workflow_request_approval`, `workflow_check_approval`
273
+ and `workflow_run_approved`.
213
274
 
214
275
  © Opticell Limited · <https://www.opticell-limited.com/companion>
276
+
277
+ ## Source and licence
278
+
279
+ `src/index.js` is the whole server: one file, Node 18 or later, no
280
+ dependencies. It is MIT licensed (see `LICENSE`). The published npm package
281
+ is the same file with WorkFlow's CloudKit API token filled in at build time
282
+ (the token is not a secret: it only lets a signed-in user reach their own
283
+ data). Run from source, `setup` asks for it with `--api-token`.
284
+
285
+ The `Dockerfile` runs the server over stdio. MCP directories use it to check
286
+ that the server starts and lists its tools, which it does without signing in.
287
+
package/index.js CHANGED
@@ -27,7 +27,7 @@ const path = require("path");
27
27
  const os = require("os");
28
28
 
29
29
  const PROTOCOL_VERSION = "2024-11-05";
30
- const SERVER_INFO = { name: "workflow", version: "1.8.0" };
30
+ const SERVER_INFO = { name: "workflow", version: "1.8.2" };
31
31
  // Who the agent is in WorkFlow: the assignee its queue follows, the @name
32
32
  // that calls it, the signature on what it writes. On the Mac it's Claude.
33
33
  // A companion goes by the name chosen at setup (default "Companion"), so a
@@ -629,7 +629,11 @@ function showTask(t) {
629
629
  function queueItems(snap) {
630
630
  const shared = new Set((snap.projects || []).filter(p => p.isShared).map(p => p.name));
631
631
  const mine = t => (t.assignee || "").toLowerCase() === AGENT.name.toLowerCase();
632
- const rows = (snap.tasks || []).filter(t => t.status !== "Completed" && !t.isHidden && (mine(t) || addressedToMe(t, shared) || watchOf(t)));
632
+ // Nothing in a shared project, however it got there: a collaborator can
633
+ // assign tasks and write comments there, so it isn't only the owner asking
634
+ // (the Mac's rule too; the shared cases in Tools/agent-rules check both).
635
+ const rows = (snap.tasks || []).filter(t => t.status !== "Completed" && !t.isHidden && !(t.project && shared.has(t.project))
636
+ && (mine(t) || addressedToMe(t, shared) || watchOf(t)));
633
637
  const notes = (snap.notes || []).filter(n => !n.isArchived && !n.convertedAt && mentionsMe(`${n.title || ""}\n${n.body || ""}`));
634
638
  return { rows, notes, mine };
635
639
  }
@@ -1168,6 +1172,12 @@ const TOOLS = [
1168
1172
  return `Ran ${x.code} on ${os.hostname()} (${result.seconds} s): exit ${result.exit}\n${result.output || "(no output)"}`;
1169
1173
  },
1170
1174
  },
1175
+ {
1176
+ name: "workflow_doctor",
1177
+ description: "Check the companion on this computer end to end (sign-in, WorkFlow publishing and answering, version, scheduled jobs, Claude Code, last session, monitors) and say how to fix anything wrong. Use it when something isn't working.",
1178
+ inputSchema: { type: "object", properties: {} },
1179
+ handler: async () => (await doctorReport({ fromTool: true })).text,
1180
+ },
1171
1181
  {
1172
1182
  name: "workflow_ping",
1173
1183
  description: "Confirm the app is watching the bridge (round-trips one command).",
@@ -1878,17 +1888,20 @@ function notify(title, body, onceKey) {
1878
1888
  if (process.env.WF_NOTIFY_LOG) { try { fs.appendFileSync(process.env.WF_NOTIFY_LOG, `${title} | ${body}\n`); } catch (_) {} return; }
1879
1889
  const { spawn } = require("child_process");
1880
1890
  const opts = { detached: true, stdio: "ignore", windowsHide: true, env: Object.assign({}, process.env, { WF_T: title, WF_B: body }) };
1891
+ // A missing notifier (no notify-send on a server) fails asynchronously:
1892
+ // without a listener that error would end the whole process.
1893
+ const quiet = child => { child.on("error", () => {}); child.unref(); };
1881
1894
  try {
1882
1895
  if (process.platform === "win32") {
1883
1896
  const ps = "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null;" +
1884
1897
  "$t = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02);" +
1885
1898
  "$x = $t.GetElementsByTagName('text'); $x.Item(0).AppendChild($t.CreateTextNode($env:WF_T)) > $null; $x.Item(1).AppendChild($t.CreateTextNode($env:WF_B)) > $null;" +
1886
1899
  "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe').Show([Windows.UI.Notifications.ToastNotification]::new($t))";
1887
- spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", ps], opts).unref();
1900
+ quiet(spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", ps], opts));
1888
1901
  } else if (process.platform === "darwin") {
1889
- spawn("osascript", ["-e", "display notification (system attribute \"WF_B\") with title (system attribute \"WF_T\")"], opts).unref();
1902
+ quiet(spawn("osascript", ["-e", "display notification (system attribute \"WF_B\") with title (system attribute \"WF_T\")"], opts));
1890
1903
  } else {
1891
- spawn("notify-send", [title, body], opts).unref();
1904
+ quiet(spawn("notify-send", [title, body], opts));
1892
1905
  }
1893
1906
  } catch (_) {}
1894
1907
  }
@@ -2253,6 +2266,122 @@ async function inboxCLI(on) {
2253
2266
  return 0;
2254
2267
  }
2255
2268
 
2269
+ // ------------------------------------------------------------------ doctor (1.8.1)
2270
+ //
2271
+ // Checks the whole chain on this computer in one go and says how to fix
2272
+ // anything that's wrong: Node, the iCloud sign-in, whether WorkFlow is
2273
+ // publishing and answering, the name, the version, the scheduled jobs,
2274
+ // Claude Code, and what the last session did. `fromTool` skips the checks
2275
+ // that would be odd from inside a Claude session (asking Claude Code about
2276
+ // itself).
2277
+ async function doctorReport({ fromTool = false } = {}) {
2278
+ const npx = "npx -y @opticell/workflow-companion@latest";
2279
+ const lines = [`WorkFlow companion ${SERVER_INFO.version} on ${os.hostname()}, as "${AGENT.name}"`, ""];
2280
+ let problems = 0, warnings = 0;
2281
+ const ok = l => lines.push(` ✓ ${l}`);
2282
+ const bad = (l, fix) => { problems++; lines.push(` ✗ ${l}`); if (fix) lines.push(` fix: ${fix}`); };
2283
+ const warn = (l, fix) => { warnings++; lines.push(` ! ${l}`); if (fix) lines.push(` ${fix}`); };
2284
+ const done = () => {
2285
+ lines.push("", problems ? `${problems} problem(s)${warnings ? `, ${warnings} warning(s)` : ""}. Fix the ✗ lines first.`
2286
+ : warnings ? `Working, with ${warnings} warning(s).` : "All good.");
2287
+ return { text: lines.join("\n"), problems, warnings };
2288
+ };
2289
+ const ago = t => { const m = Math.round((Date.now() - t) / 60000); return m < 1 ? "just now" : m < 90 ? `${m} min ago` : m < 2880 ? `${Math.round(m / 60)} h ago` : `${Math.round(m / 1440)} days ago`; };
2290
+ const { spawnSync } = require("child_process");
2291
+
2292
+ const major = parseInt(process.versions.node, 10);
2293
+ major >= 18 ? ok(`Node.js ${process.versions.node}`) : bad(`Node.js ${process.versions.node} is too old (18 or later)`, "install the LTS from https://nodejs.org");
2294
+ if (!CLOUD) { warn("this is the Mac's own bridge, not a companion", "on the Mac, check Settings → Agent Bridge, or run: wf state"); return done(); }
2295
+
2296
+ const cfg = readJSON(CP.config, null);
2297
+ if (!cfg || !fs.existsSync(CP.token)) { bad("not signed in on this computer", `${npx} setup`); return done(); }
2298
+ try {
2299
+ await cloud.call("public/users/caller");
2300
+ ok(`iCloud sign-in works (${cfg.env || COMPANION.env})`);
2301
+ } catch (e) {
2302
+ if (e instanceof SignInNeeded) bad("the iCloud sign-in has ended (Apple ends one left unused for 9 to 14 hours)", `${npx} setup`);
2303
+ else bad(`can't reach iCloud: ${String(e.message).split("\n")[0]}`, "check this computer's internet connection, then run doctor again");
2304
+ return done();
2305
+ }
2306
+
2307
+ try { await cloud.load(true); } catch (e) { bad(`couldn't read WorkFlow's snapshot: ${e.message}`); return done(); }
2308
+ if (!cloudSnap) {
2309
+ bad("WorkFlow hasn't published anything for the companion yet",
2310
+ "switch it on in WorkFlow: iPhone/iPad Settings → Windows Companion → Answer the Companion, or Mac Settings → Agent Bridge → Cloud Bridge");
2311
+ } else {
2312
+ const age = Date.now() - Date.parse(cloudSnap.generatedAt);
2313
+ const line = `${cloudSnap.tasks.length} tasks, published ${ago(Date.parse(cloudSnap.generatedAt))}; answering: ${cloud.macLine()}`;
2314
+ age < 6 * 3600 * 1000 ? ok(line) : warn(line, "nothing has changed in WorkFlow for a while, or the device that answers is asleep or closed: open WorkFlow on it");
2315
+ const clash = nameClash(AGENT.name);
2316
+ clash ? bad(`"${AGENT.name}" is also the name of Claude on ${clash.host}: both would act on @${AGENT.name}`, `${npx} setup --name <another name>`)
2317
+ : ok(`the name "${AGENT.name}" is this computer's alone`);
2318
+ }
2319
+
2320
+ if (!fromTool) {
2321
+ const started = Date.now();
2322
+ try {
2323
+ const r = await send("ping", {}, 30000);
2324
+ r.status === "applied" ? ok(`a test command was applied by WorkFlow in ${Math.max(1, Math.round((Date.now() - started) / 1000))} s`)
2325
+ : warn("a test command got no answer within 30 s", "WorkFlow on the answering device may be asleep or closed. Commands wait safely and run when it next looks; an iPhone can take minutes");
2326
+ } catch (e) { warn(`couldn't send a test command: ${e.message}`); }
2327
+ }
2328
+
2329
+ const rel = await latestRelease(true);
2330
+ if (!rel) warn("couldn't check npm for a newer release");
2331
+ else if (newerVersion(rel.version, SERVER_INFO.version)) warn(`version ${SERVER_INFO.version}; ${rel.version} is out`, `${npx} update (and restart Claude Code)`);
2332
+ else ok(`version ${SERVER_INFO.version} is the latest`);
2333
+ const saved = versionOfScript(SAVED_SCRIPT);
2334
+ if (saved && rel && newerVersion(rel.version, saved)) warn(`the scheduled jobs run ${saved}`, cfg.autoUpdate === false ? `auto-update is off: ${npx} update` : "they update themselves within 6 hours, or now with: " + `${npx} update`);
2335
+
2336
+ const scheduled = name => {
2337
+ if (process.platform === "win32") return spawnSync("schtasks", ["/Query", "/TN", name], { encoding: "utf8", windowsHide: true }).status === 0;
2338
+ const r = spawnSync("crontab", ["-l"], { encoding: "utf8" });
2339
+ return r.status === 0 && r.stdout.includes(name === TASK_NAME ? "workflow-companion keepalive" : "workflow-companion wake");
2340
+ };
2341
+ let lastKeepAlive = "";
2342
+ try { lastKeepAlive = fs.readFileSync(KEEPALIVE_LOG, "utf8").trim().split("\n").pop(); } catch (_) {}
2343
+ scheduled(TASK_NAME) ? ok(`keep-alive scheduled${lastKeepAlive ? `; last run ${lastKeepAlive.split(" ")[0].slice(0, 16).replace("T", " ")}: ${lastKeepAlive.split(" ").slice(1).join(" ")}` : ""}`)
2344
+ : warn("the keep-alive isn't scheduled, so the sign-in can end while Claude isn't running", `${npx} setup (it schedules it)`);
2345
+
2346
+ if (cfg.wakeEnabled) {
2347
+ scheduled(WAKE.taskName) ? ok("waking is on and scheduled every 2 minutes") : bad("waking is on but its scheduled job is missing", `${npx} wake-on`);
2348
+ const bin = cfg.claudeBin || "claude";
2349
+ const v = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 20000, windowsHide: true, shell: process.platform === "win32" && /\.(cmd|bat)$/i.test(bin) });
2350
+ v.status === 0 ? ok(`Claude Code runs: ${String(v.stdout).trim().split("\n")[0]}`)
2351
+ : bad(`Claude Code at ${bin} doesn't run`, `reinstall or sign in to Claude Code, then: ${npx} wake-on`);
2352
+ let log = [];
2353
+ try { log = fs.readFileSync(WAKE.log, "utf8").trim().split("\n"); } catch (_) {}
2354
+ const last = log.filter(l => / session exited /.test(l)).pop();
2355
+ if (!last) ok("no sessions yet");
2356
+ else {
2357
+ const when = Date.parse(last.split(" ")[0]);
2358
+ const code = (/session exited (-?\d+)/.exec(last) || [])[1];
2359
+ code === "0" && !/time limit/.test(last) ? ok(`last session ${ago(when)} finished normally`)
2360
+ : warn(`last session ${ago(when)} didn't finish normally: ${last.split(" ").slice(1).join(" ").slice(0, 160)}`, `see ${WAKE.log}`);
2361
+ }
2362
+ if (log.slice(-30).some(l => /SIGN-IN ENDED/.test(l))) warn("the sign-in ended recently (it works now)", "if this PC sleeps for long, leave it awake or check it each morning");
2363
+ } else {
2364
+ warn("waking is off: work waits until you ask Claude on this computer", `${npx} wake-on`);
2365
+ }
2366
+
2367
+ if (!fromTool) {
2368
+ const bin = cfg.claudeBin || "claude";
2369
+ const list = spawnSync(bin, ["mcp", "list"], { encoding: "utf8", timeout: 60000, windowsHide: true, shell: process.platform === "win32" && /\.(cmd|bat)$/i.test(bin) });
2370
+ const text = String(list.stdout || "");
2371
+ if (list.status !== 0 && !text) warn("couldn't ask Claude Code which tools it has", `if it isn't installed: ${claudeCodeCommand()}`);
2372
+ else if (!/workflow/i.test(text)) bad("Claude Code doesn't have the companion", claudeCodeCommand());
2373
+ else if (!/@latest/.test(text.split("\n").find(l => /^workflow/i.test(l)) || "")) warn("Claude Code runs a pinned version of the companion", `claude mcp remove workflow -s user, then: ${claudeCodeCommand()}`);
2374
+ else ok("Claude Code has the companion, on @latest");
2375
+ }
2376
+
2377
+ const failing = loadMonitors().filter(m => m.state && !m.state.ok);
2378
+ if (loadMonitors().length) failing.length ? warn(`monitors failing: ${failing.map(m => `${m.name} (${m.state.detail})`).join(", ")}`) : ok(`${loadMonitors().length} monitor(s), all OK`);
2379
+ const pending = loadApprovals().filter(a => a.status === "pending");
2380
+ if (pending.length) warn(`${pending.length} approval(s) waiting for you: ${pending.map(a => `${a.code} on “${a.taskTitle}”`).join(", ")}`);
2381
+ if (cfg.inboxFolder) fs.existsSync(cfg.inboxFolder) ? ok(`inbox folder: ${cfg.inboxFolder}`) : bad(`the inbox folder ${cfg.inboxFolder} is missing`, `${npx} inbox-on --folder "<path>"`);
2382
+ return done();
2383
+ }
2384
+
2256
2385
  // ------------------------------------------------------------------ versions (1.6.1)
2257
2386
 
2258
2387
  // Each companion says which version it runs, and keeps itself current.
@@ -2479,6 +2608,7 @@ const HELP = `WorkFlow companion ${SERVER_INFO.version}
2479
2608
  workflow-companion run the MCP server (what Claude starts)
2480
2609
  workflow-companion setup sign this computer in to iCloud for WorkFlow (--name, --takeover, --no-browser)
2481
2610
  workflow-companion status which device answers, how fresh is the snapshot
2611
+ workflow-companion doctor check everything on this computer and say how to fix what's wrong
2482
2612
  workflow-companion keepalive renew the sign-in (what the scheduled job runs)
2483
2613
  workflow-companion update update the scheduled jobs' copy now, and say how to keep Claude Code current
2484
2614
  workflow-companion auto-update on|off whether the scheduled jobs update themselves (on by default)
@@ -2502,28 +2632,34 @@ Needs WorkFlow 5.2+ on an iPhone, iPad or Mac, with the companion switched on in
2502
2632
  Install in one line: see https://github.com/TeamDzX/workflow-companion#install
2503
2633
  `;
2504
2634
 
2505
- if (argv.includes("--print-config")) printConfig();
2506
- else if (argv.includes("--help") || verb === "help") process.stdout.write(HELP);
2507
- else if (verb === "setup") setup().then(c => { process.exitCode = c; }, e => { console.error(e.message || e); process.exitCode = 1; });
2508
- else if (verb === "status") cloudStateText().then(t => console.log(t), e => { console.error(e.message || e); process.exitCode = 1; });
2509
- else if (verb === "keepalive") keepAlive().then(c => { process.exitCode = c; });
2510
- else if (verb === "update") updateNow().then(c => { process.exitCode = c; }, e => { console.error(e.message || e); process.exitCode = 1; });
2511
- else if (verb === "auto-update") process.exitCode = setAutoUpdate(argv[argv.indexOf("auto-update") + 1] !== "off");
2512
- else if (verb === "wake") wake().then(c => { process.exitCode = c; }, e => { logWake(`failed: ${e.message || e}`); process.exitCode = 1; });
2513
- else if (verb === "wake-on" || verb === "wake-off") wakeSetup(verb === "wake-on").then(c => { process.exitCode = c; }, e => { console.error(e.message || e); process.exitCode = 1; });
2514
- else if (verb === "monitor") monitorCLI().then(c => { process.exitCode = c; }, e => { console.error(e.message || e); process.exitCode = 1; });
2515
- else if (verb === "approvals") process.exitCode = setFlag("approvalsEnabled", argv[argv.indexOf("approvals") + 1] === "on",
2516
- "Approvals are on. An assistant can ask to run one exact command on this computer; it runs only after you reply \"approve CODE\" on the task in WorkFlow, once, within 24 hours. Turn off with: workflow-companion approvals off",
2517
- "Approvals are off. Requests are refused and approved ones won't run.");
2518
- else if (verb === "inbox-on" || verb === "inbox-off") inboxCLI(verb === "inbox-on").then(c => { process.exitCode = c; }, e => { console.error(e.message || e); process.exitCode = 1; });
2519
- else if (verb === "notifications") process.exitCode = setFlag("notifications", argv[argv.indexOf("notifications") + 1] !== "off",
2520
- "Notifications are on.", "Notifications are off.");
2521
- else if (verb === "reports") process.exitCode = setFlag("reports", argv[argv.indexOf("reports") + 1] !== "off",
2522
- "Session reports are on: each session attaches a short report to its task.", "Session reports are off.");
2523
- else if (verb === "notify-test") { notify("WorkFlow companion", `Notifications work on ${os.hostname()}.`); console.log("Sent a test notification."); }
2524
- else if (verb === "signout") {
2525
- removeSchedule();
2526
- removeWakeSchedule();
2527
- for (const f of Object.values(CP)) try { fs.unlinkSync(f); } catch (_) {}
2528
- console.log(`Signed out: removed the token, the settings and the keep-alive from ${COMPANION.home}.`);
2529
- } else serve();
2635
+ // Loaded by the shared rule tests (Tools/agent-rules) rather than run.
2636
+ if (require.main !== module) {
2637
+ module.exports = { wakeItems, queueItems, addressingComment, assignedHandoff, agent: () => AGENT };
2638
+ } else {
2639
+ if (argv.includes("--print-config")) printConfig();
2640
+ else if (argv.includes("--help") || verb === "help") process.stdout.write(HELP);
2641
+ else if (verb === "setup") setup().then(c => { process.exitCode = c; }, e => { console.error(e.message || e); process.exitCode = 1; });
2642
+ else if (verb === "doctor") doctorReport().then(r => { console.log(r.text); process.exitCode = r.problems ? 1 : 0; }, e => { console.error(e.message || e); process.exitCode = 1; });
2643
+ else if (verb === "status") cloudStateText().then(t => console.log(t), e => { console.error(e.message || e); process.exitCode = 1; });
2644
+ else if (verb === "keepalive") keepAlive().then(c => { process.exitCode = c; });
2645
+ else if (verb === "update") updateNow().then(c => { process.exitCode = c; }, e => { console.error(e.message || e); process.exitCode = 1; });
2646
+ else if (verb === "auto-update") process.exitCode = setAutoUpdate(argv[argv.indexOf("auto-update") + 1] !== "off");
2647
+ else if (verb === "wake") wake().then(c => { process.exitCode = c; }, e => { logWake(`failed: ${e.message || e}`); process.exitCode = 1; });
2648
+ else if (verb === "wake-on" || verb === "wake-off") wakeSetup(verb === "wake-on").then(c => { process.exitCode = c; }, e => { console.error(e.message || e); process.exitCode = 1; });
2649
+ else if (verb === "monitor") monitorCLI().then(c => { process.exitCode = c; }, e => { console.error(e.message || e); process.exitCode = 1; });
2650
+ else if (verb === "approvals") process.exitCode = setFlag("approvalsEnabled", argv[argv.indexOf("approvals") + 1] === "on",
2651
+ "Approvals are on. An assistant can ask to run one exact command on this computer; it runs only after you reply \"approve CODE\" on the task in WorkFlow, once, within 24 hours. Turn off with: workflow-companion approvals off",
2652
+ "Approvals are off. Requests are refused and approved ones won't run.");
2653
+ else if (verb === "inbox-on" || verb === "inbox-off") inboxCLI(verb === "inbox-on").then(c => { process.exitCode = c; }, e => { console.error(e.message || e); process.exitCode = 1; });
2654
+ else if (verb === "notifications") process.exitCode = setFlag("notifications", argv[argv.indexOf("notifications") + 1] !== "off",
2655
+ "Notifications are on.", "Notifications are off.");
2656
+ else if (verb === "reports") process.exitCode = setFlag("reports", argv[argv.indexOf("reports") + 1] !== "off",
2657
+ "Session reports are on: each session attaches a short report to its task.", "Session reports are off.");
2658
+ else if (verb === "notify-test") { notify("WorkFlow companion", `Notifications work on ${os.hostname()}.`); console.log("Sent a test notification."); }
2659
+ else if (verb === "signout") {
2660
+ removeSchedule();
2661
+ removeWakeSchedule();
2662
+ for (const f of Object.values(CP)) try { fs.unlinkSync(f); } catch (_) {}
2663
+ console.log(`Signed out: removed the token, the settings and the keep-alive from ${COMPANION.home}.`);
2664
+ } else serve();
2665
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opticell/workflow-companion",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "mcpName": "io.github.TeamDzX/workflow-companion",
5
5
  "description": "Lets Claude on Windows or Linux work in your WorkFlow tasks, notes and projects, through WorkFlow for Mac and your own iCloud. An MCP server.",
6
6
  "keywords": [