@opticell/workflow-companion 1.6.0 → 1.6.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.
- package/README.md +36 -5
- package/index.js +233 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,19 +31,32 @@ many devices are on.
|
|
|
31
31
|
1. In WorkFlow on an iPhone or iPad: **Settings → Windows Companion → Answer
|
|
32
32
|
the Companion**. On a Mac: **Settings → Agent Bridge**, then the Agent
|
|
33
33
|
Bridge and **Cloud Bridge** switches.
|
|
34
|
-
2.
|
|
34
|
+
2. Install **Node.js 18 or later** (LTS) if `node -v` doesn't show it:
|
|
35
|
+
Windows `winget install OpenJS.NodeJS.LTS -e`, Mac `brew install node`,
|
|
36
|
+
Linux your package manager, or the installer from <https://nodejs.org>.
|
|
37
|
+
Then close and reopen the terminal (and restart Claude Code) so `node` and
|
|
38
|
+
`npx` are found; otherwise you'll see "node is not recognized".
|
|
39
|
+
3. In a normal terminal window:
|
|
35
40
|
|
|
36
41
|
```sh
|
|
37
|
-
npx -y @opticell/workflow-companion setup
|
|
42
|
+
npx -y @opticell/workflow-companion@latest setup
|
|
38
43
|
```
|
|
39
44
|
|
|
40
45
|
It opens <https://companion.opticell-limited.com> to sign in with the
|
|
41
46
|
Apple Account WorkFlow uses on your iPhone or Mac, then asks you to paste the token that
|
|
42
|
-
page shows.
|
|
43
|
-
|
|
47
|
+
page shows. It asks questions, so it can't run from Claude Code's `!`
|
|
48
|
+
prompt or another non-interactive shell; there, pass `--web-token <token>`
|
|
49
|
+
and `--name <name>`.
|
|
50
|
+
4. Add it to Claude:
|
|
44
51
|
|
|
45
52
|
```sh
|
|
46
|
-
claude mcp add --scope user workflow -- npx -y @opticell/workflow-companion
|
|
53
|
+
claude mcp add --scope user workflow -- npx -y @opticell/workflow-companion@latest
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
On **Windows**, `npx` is a script, so Claude Code needs `cmd /c` in front:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
claude mcp add --scope user workflow -- cmd /c npx -y @opticell/workflow-companion@latest
|
|
47
60
|
```
|
|
48
61
|
|
|
49
62
|
`--scope user` makes it available in every project on this computer.
|
|
@@ -113,6 +126,22 @@ npx -y @opticell/workflow-companion wake-on
|
|
|
113
126
|
found. `--skip-existing` treats what's already queued as handled.
|
|
114
127
|
- The log is `~/.workflow-companion/wake.log`. `wake-off` stops it all.
|
|
115
128
|
|
|
129
|
+
## Staying up to date
|
|
130
|
+
|
|
131
|
+
- **Claude Code** runs whatever its registration names. Register it with
|
|
132
|
+
`@latest` (setup prints the exact line) and every new session uses the
|
|
133
|
+
newest release.
|
|
134
|
+
- **The scheduled jobs** (keep-alive, waking) run a saved copy that npm never
|
|
135
|
+
touches, so they update themselves. At most every 6 hours they read npm's
|
|
136
|
+
entry for the package (`registry.npmjs.org/@opticell/workflow-companion/latest`).
|
|
137
|
+
When it's newer, they download the release, check it against npm's sha512
|
|
138
|
+
checksum and that Node can parse it, then swap it in. Turn this off with
|
|
139
|
+
`auto-update off`, and update by hand with `update`.
|
|
140
|
+
- **Its version is reported.** Every ten minutes or so while it runs, each
|
|
141
|
+
companion writes its version, computer and check-in time to your iCloud.
|
|
142
|
+
WorkFlow 5.3.2 shows them in Settings → Agent Bridge → Who's Bridged, and
|
|
143
|
+
`status` lists every companion's version.
|
|
144
|
+
|
|
116
145
|
## Commands
|
|
117
146
|
|
|
118
147
|
| | |
|
|
@@ -121,6 +150,8 @@ npx -y @opticell/workflow-companion wake-on
|
|
|
121
150
|
| `workflow-companion setup` | Signs this computer in. Takes `--web-token` to skip the prompt. |
|
|
122
151
|
| `workflow-companion status` | Which device answers, how fresh the snapshot is, any receipts that arrived late. |
|
|
123
152
|
| `workflow-companion keepalive` | Renews the sign-in. The scheduled job runs this. |
|
|
153
|
+
| `workflow-companion update` | Updates the scheduled jobs' copy to the latest release now, and shows how to keep Claude Code on `@latest`. |
|
|
154
|
+
| `workflow-companion auto-update on\|off` | Whether the scheduled jobs update themselves (on by default). |
|
|
124
155
|
| `workflow-companion wake-on` | Starts a Claude Code session on its own when work for its name arrives. See above. |
|
|
125
156
|
| `workflow-companion wake-off` | Stops that. Work waits until you ask Claude here. |
|
|
126
157
|
| `workflow-companion wake` | One check of the queue. The scheduled job runs this. |
|
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.6.
|
|
30
|
+
const SERVER_INFO = { name: "workflow", version: "1.6.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
|
|
@@ -675,6 +675,25 @@ function addressingComment(t) {
|
|
|
675
675
|
return comments.slice(-3).reverse().find(c => !isAgentID(c.authorID) && mentionsMe(c.text)) || null;
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
+
// On a task assigned to this agent: the newest comment from another
|
|
679
|
+
// assistant that @mentions it, since the last person wrote, within the hop
|
|
680
|
+
// limit. Unlike addressingComment it needn't be the very last comment, so an
|
|
681
|
+
// assistant's follow-up without a mention doesn't change the wake signature.
|
|
682
|
+
function assignedHandoff(t) {
|
|
683
|
+
const comments = t.comments || [];
|
|
684
|
+
for (let i = comments.length - 1; i >= 0; i--) {
|
|
685
|
+
const c = comments[i];
|
|
686
|
+
if (!isAgentID(c.authorID)) return null;
|
|
687
|
+
if (c.authorID === AGENT.authorID || !mentionsMe(c.text)) continue;
|
|
688
|
+
let hops = 0;
|
|
689
|
+
for (let j = i; j >= 0 && isAgentID(comments[j].authorID); j--) {
|
|
690
|
+
if (/(^|[^A-Za-z0-9_])@[A-Za-z]/.test(comments[j].text || "")) hops++;
|
|
691
|
+
}
|
|
692
|
+
return hops <= MAX_AGENT_HOPS ? c : null;
|
|
693
|
+
}
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
|
|
678
697
|
function showNote(n) {
|
|
679
698
|
const lines = [n.title || "(untitled)", `id ${n.id}`];
|
|
680
699
|
if (n.isPinned) lines.push("pinned");
|
|
@@ -1018,7 +1037,10 @@ async function handle(request) {
|
|
|
1018
1037
|
try {
|
|
1019
1038
|
// In iCloud mode every tool reads the Mac's latest snapshot first
|
|
1020
1039
|
// (task lookups need it too); workflow_state reports on its own.
|
|
1021
|
-
if (CLOUD && tool.name !== "workflow_state" && tool.name !== "workflow_ping")
|
|
1040
|
+
if (CLOUD && tool.name !== "workflow_state" && tool.name !== "workflow_ping") {
|
|
1041
|
+
await cloud.load();
|
|
1042
|
+
reportPresence().catch(() => {});
|
|
1043
|
+
}
|
|
1022
1044
|
return ok({ content: [{ type: "text", text: String(await tool.handler(args)) }], isError: false });
|
|
1023
1045
|
} catch (e) {
|
|
1024
1046
|
return ok({ content: [{ type: "text", text: `${e.message || e}` }], isError: true });
|
|
@@ -1068,11 +1090,18 @@ async function cloudStateText() {
|
|
|
1068
1090
|
try { lastKeepAlive = fs.readFileSync(KEEPALIVE_LOG, "utf8").trim().split("\n").pop(); } catch (_) {}
|
|
1069
1091
|
if (lastKeepAlive) lines.push(`keep-alive last run ${lastKeepAlive}`);
|
|
1070
1092
|
lines.push(`waking ${cfg.wakeEnabled ? `on — Claude Code starts by itself when @${AGENT.name} work arrives (log ${WAKE.log})` : "off — work waits until Claude here is asked (turn on: workflow-companion wake-on)"}`);
|
|
1093
|
+
lines.push(`version ${await versionLine()}`);
|
|
1071
1094
|
lines.push(`name ${AGENT.name} — assign tasks to "${AGENT.name}", or write @${AGENT.name} in a comment or note`);
|
|
1072
1095
|
try {
|
|
1073
1096
|
await cloud.load(true);
|
|
1074
1097
|
lines.push(`answering ${cloud.macLine()}`);
|
|
1075
1098
|
lines.push(`snapshot ${cloudSnap ? `${cloudSnap.tasks.length} tasks, ${(cloudSnap.notes || []).length} notes, generated ${local(cloudSnap.generatedAt)}` : "none yet — is the companion switched on in WorkFlow's Settings (Windows Companion on iPhone and iPad, Cloud Bridge on a Mac)?"}`);
|
|
1099
|
+
try {
|
|
1100
|
+
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`; };
|
|
1101
|
+
for (const c of await companionPresence()) {
|
|
1102
|
+
lines.push(`companion ${c.name}: ${c.version ? `${c.version} on ${c.host || "?"}, checked in ${ago(c.aliveAt)}${c.waking ? ", waking on" : ""}` : "hasn't reported a version (older than 1.6.1)"}`);
|
|
1103
|
+
}
|
|
1104
|
+
} catch (_) { /* presence is a nicety */ }
|
|
1076
1105
|
const clash = cloudSnap ? nameClash(AGENT.name) : null;
|
|
1077
1106
|
if (clash) lines.push(`WARNING ${clash.host} also answers to "${AGENT.name}": both would act on @${AGENT.name}. Run setup with another --name.`);
|
|
1078
1107
|
} catch (e) {
|
|
@@ -1169,7 +1198,8 @@ async function setup() {
|
|
|
1169
1198
|
} else {
|
|
1170
1199
|
console.log(`\nKeep-alive: ${installSchedule()}. It keeps the sign-in from lapsing while Claude isn't running.`);
|
|
1171
1200
|
}
|
|
1172
|
-
|
|
1201
|
+
await reportPresence(true);
|
|
1202
|
+
console.log(`\nNow add it to Claude Code (@latest, so each new session picks up the newest release):\n ${claudeCodeCommand()}`);
|
|
1173
1203
|
if (argv.includes("--wake") && !argv.includes("--no-schedule") && !process.env.WF_NO_SCHEDULE) {
|
|
1174
1204
|
console.log("");
|
|
1175
1205
|
await wakeSetup(true);
|
|
@@ -1199,7 +1229,9 @@ async function keepAlive() {
|
|
|
1199
1229
|
const at = new Date().toISOString();
|
|
1200
1230
|
try {
|
|
1201
1231
|
await cloud.call("public/users/caller");
|
|
1202
|
-
|
|
1232
|
+
await reportPresence();
|
|
1233
|
+
const update = await selfUpdate();
|
|
1234
|
+
logKeepAlive(`${at} ok${/^updated|failed/.test(update) ? `; ${update}` : ""}`);
|
|
1203
1235
|
return 0;
|
|
1204
1236
|
} catch (e) {
|
|
1205
1237
|
logKeepAlive(`${at} ${e instanceof SignInNeeded ? "SIGN-IN ENDED" : "failed"}: ${String(e.message).split("\n")[0]}`);
|
|
@@ -1295,7 +1327,13 @@ function wakeItems(snap) {
|
|
|
1295
1327
|
const personal = c => !String(c.authorID || "").startsWith("agent.");
|
|
1296
1328
|
const items = rows.map(t => {
|
|
1297
1329
|
const byPeople = (t.comments || []).filter(personal);
|
|
1298
|
-
|
|
1330
|
+
// On an assigned task, another assistant's @mention counts too (1.6.2):
|
|
1331
|
+
// only a person's comment did before, so opticell → Jarvis hand-offs on
|
|
1332
|
+
// tasks assigned to Jarvis woke nothing. The newer of the two wins.
|
|
1333
|
+
const trigger = mine(t)
|
|
1334
|
+
? [byPeople.slice(-1)[0], assignedHandoff(t)].filter(Boolean)
|
|
1335
|
+
.sort((x, y) => String(y.timestamp || "").localeCompare(String(x.timestamp || "")))[0]
|
|
1336
|
+
: addressingComment(t);
|
|
1299
1337
|
const why = mine(t) ? `assigned to ${AGENT.name}` : `@${AGENT.name} in a comment`;
|
|
1300
1338
|
return { key: `task:${t.id}`, sig: trigger ? `${trigger.id || ""}@${trigger.timestamp || ""}` : "assigned",
|
|
1301
1339
|
kind: "task", id: t.id, title: t.title, project: t.project || "", why };
|
|
@@ -1396,6 +1434,7 @@ async function wake() {
|
|
|
1396
1434
|
try {
|
|
1397
1435
|
try { await cloud.load(true); }
|
|
1398
1436
|
catch (e) { logWake(`${e instanceof SignInNeeded ? "SIGN-IN ENDED" : "iCloud failed"}: ${String(e.message).split("\n")[0]}`); return 1; }
|
|
1437
|
+
await reportPresence();
|
|
1399
1438
|
if (!cloudSnap) return 0;
|
|
1400
1439
|
if (blockingClash()) { logWake(`not waking: "${AGENT.name}" is also Claude on another computer`); return 0; }
|
|
1401
1440
|
const st = readJSON(WAKE.state, null) || {};
|
|
@@ -1450,6 +1489,7 @@ async function wake() {
|
|
|
1450
1489
|
return 1;
|
|
1451
1490
|
} finally {
|
|
1452
1491
|
try { fs.unlinkSync(WAKE.lock); } catch (_) {}
|
|
1492
|
+
try { const update = await selfUpdate(); if (/^updated|failed/.test(update)) logWake(update); } catch (_) {}
|
|
1453
1493
|
}
|
|
1454
1494
|
}
|
|
1455
1495
|
|
|
@@ -1539,9 +1579,192 @@ async function wakeSetup(on) {
|
|
|
1539
1579
|
return 0;
|
|
1540
1580
|
}
|
|
1541
1581
|
|
|
1582
|
+
// ------------------------------------------------------------------ versions (1.6.1)
|
|
1583
|
+
|
|
1584
|
+
// Each companion says which version it runs, and keeps itself current.
|
|
1585
|
+
//
|
|
1586
|
+
// Presence: a "companion-<name>" record of the Mac's own status type
|
|
1587
|
+
// (BridgeStatus: aliveAt, appVersion, device, pollSeconds, all already in the
|
|
1588
|
+
// Production schema), so WorkFlow's Who's Bridged and other companions can
|
|
1589
|
+
// show "Jarvis · 1.6.1 · checked in 4 min ago".
|
|
1590
|
+
//
|
|
1591
|
+
// Latest release: npm's registry entry for the package, which is JSON and is
|
|
1592
|
+
// by definition what was last published. Checked at most every 6 hours.
|
|
1593
|
+
//
|
|
1594
|
+
// Self-update: the scheduled jobs run a saved copy that npm never touches.
|
|
1595
|
+
// When a newer release is out, that copy is replaced by the published file,
|
|
1596
|
+
// after checking it against npm's own sha512 and that Node can parse it.
|
|
1597
|
+
// Claude Code stays current by being registered with @latest.
|
|
1598
|
+
const RELEASE_URL = "https://registry.npmjs.org/@opticell/workflow-companion/latest";
|
|
1599
|
+
const RELEASE_CHECK_MS = 6 * 3600 * 1000;
|
|
1600
|
+
const PRESENCE_EVERY_MS = 10 * 60 * 1000;
|
|
1601
|
+
const SAVED_SCRIPT = path.join(COMPANION.home, "workflow-companion.js");
|
|
1602
|
+
|
|
1603
|
+
// true when version a is newer than b ("1.10.0" > "1.9.3").
|
|
1604
|
+
function newerVersion(a, b) {
|
|
1605
|
+
const pa = String(a || "").split(".").map(n => parseInt(n, 10) || 0);
|
|
1606
|
+
const pb = String(b || "").split(".").map(n => parseInt(n, 10) || 0);
|
|
1607
|
+
for (let i = 0; i < 3; i++) if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) > (pb[i] || 0);
|
|
1608
|
+
return false;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function updateConfig(change) {
|
|
1612
|
+
if (!fs.existsSync(CP.config)) return;
|
|
1613
|
+
const cfg = readJSON(CP.config, {}) || {};
|
|
1614
|
+
change(cfg);
|
|
1615
|
+
saveAtomic(CP.config, JSON.stringify(cfg, null, 2));
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// The newest published release, from npm (cached for 6 hours in config.json).
|
|
1619
|
+
async function latestRelease(force = false) {
|
|
1620
|
+
const cached = (readJSON(CP.config, {}) || {}).latestRelease || null;
|
|
1621
|
+
if (!force && cached && Date.now() - cached.checkedAt < RELEASE_CHECK_MS) return cached;
|
|
1622
|
+
try {
|
|
1623
|
+
const ctl = new AbortController();
|
|
1624
|
+
const timer = setTimeout(() => ctl.abort(), 8000);
|
|
1625
|
+
const res = await fetch(RELEASE_URL, { signal: ctl.signal });
|
|
1626
|
+
clearTimeout(timer);
|
|
1627
|
+
if (!res.ok) return cached;
|
|
1628
|
+
const j = await res.json();
|
|
1629
|
+
if (!j || !j.version) return cached;
|
|
1630
|
+
const info = { version: j.version, tarball: j.dist && j.dist.tarball, integrity: j.dist && j.dist.integrity, checkedAt: Date.now() };
|
|
1631
|
+
updateConfig(cfg => { cfg.latestRelease = info; });
|
|
1632
|
+
return info;
|
|
1633
|
+
} catch (_) {
|
|
1634
|
+
return cached;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// The version a saved copy of this file declares.
|
|
1639
|
+
function versionOfScript(file) {
|
|
1640
|
+
try {
|
|
1641
|
+
const m = fs.readFileSync(file, "utf8").match(/const SERVER_INFO = \{ name: "workflow", version: "([0-9.]+)" \};/);
|
|
1642
|
+
return m ? m[1] : null;
|
|
1643
|
+
} catch (_) { return null; }
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// package/index.js out of an npm tarball (gzip'd ustar). No dependencies.
|
|
1647
|
+
function indexFromTarball(gz) {
|
|
1648
|
+
const tar = require("zlib").gunzipSync(gz);
|
|
1649
|
+
const text = (from, len) => tar.toString("utf8", from, from + len).replace(/\0[\s\S]*$/, "");
|
|
1650
|
+
for (let off = 0; off + 512 <= tar.length;) {
|
|
1651
|
+
if (tar.subarray(off, off + 512).every(b => b === 0)) break;
|
|
1652
|
+
const name = text(off, 100), prefix = text(off + 345, 155);
|
|
1653
|
+
const size = parseInt(text(off + 124, 12).trim(), 8) || 0;
|
|
1654
|
+
const start = off + 512;
|
|
1655
|
+
if ((prefix ? `${prefix}/${name}` : name) === "package/index.js") return tar.subarray(start, start + size);
|
|
1656
|
+
off = start + Math.ceil(size / 512) * 512;
|
|
1657
|
+
}
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// Replaces the saved copy the scheduled jobs run, if a newer release is out.
|
|
1662
|
+
// Returns "updated from X to Y", "current", "off", or a reason it didn't.
|
|
1663
|
+
async function selfUpdate({ force = false } = {}) {
|
|
1664
|
+
const cfg = readJSON(CP.config, null);
|
|
1665
|
+
if (!cfg) return "not signed in";
|
|
1666
|
+
if (cfg.autoUpdate === false && !force) return "off";
|
|
1667
|
+
if (!fs.existsSync(SAVED_SCRIPT)) return "nothing scheduled";
|
|
1668
|
+
const saved = versionOfScript(SAVED_SCRIPT);
|
|
1669
|
+
const rel = await latestRelease(force);
|
|
1670
|
+
if (!rel || !rel.version) return "couldn't reach npm";
|
|
1671
|
+
if (!newerVersion(rel.version, saved)) return "current";
|
|
1672
|
+
try {
|
|
1673
|
+
if (!rel.tarball || !/^sha512-/.test(rel.integrity || "")) throw new Error("npm gave no tarball or checksum");
|
|
1674
|
+
const res = await fetch(rel.tarball);
|
|
1675
|
+
if (!res.ok) throw new Error(`download failed (HTTP ${res.status})`);
|
|
1676
|
+
const gz = Buffer.from(await res.arrayBuffer());
|
|
1677
|
+
const sum = "sha512-" + require("crypto").createHash("sha512").update(gz).digest("base64");
|
|
1678
|
+
if (sum !== rel.integrity) throw new Error("checksum didn't match npm's");
|
|
1679
|
+
const code = indexFromTarball(gz);
|
|
1680
|
+
if (!code) throw new Error("no index.js in the package");
|
|
1681
|
+
// A .js name: Node won't --check a file with any other extension.
|
|
1682
|
+
const tmp = path.join(COMPANION.home, `workflow-companion.${process.pid}.new.js`);
|
|
1683
|
+
fs.writeFileSync(tmp, code);
|
|
1684
|
+
const check = require("child_process").spawnSync(process.execPath, ["--check", tmp], { encoding: "utf8" });
|
|
1685
|
+
if (check.status !== 0) { try { fs.unlinkSync(tmp); } catch (_) {} throw new Error("the new file didn't parse"); }
|
|
1686
|
+
fs.renameSync(tmp, SAVED_SCRIPT);
|
|
1687
|
+
return `updated from ${saved || "?"} to ${rel.version}`;
|
|
1688
|
+
} catch (e) {
|
|
1689
|
+
return `update to ${rel.version} failed: ${e.message}`;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// "I'm here, this version": at most every 10 minutes unless forced.
|
|
1694
|
+
async function reportPresence(force = false) {
|
|
1695
|
+
if (!CLOUD) return;
|
|
1696
|
+
const cfg = readJSON(CP.config, null);
|
|
1697
|
+
if (!cfg || !fs.existsSync(CP.token)) return;
|
|
1698
|
+
if (!force && cfg.presenceAt && Date.now() - cfg.presenceAt < PRESENCE_EVERY_MS) return;
|
|
1699
|
+
const slug = AGENT.authorID.slice("agent.".length);
|
|
1700
|
+
try {
|
|
1701
|
+
await cloud.call("private/records/modify", { zoneID: { zoneName: COMPANION.zone }, operations: [{ operationType: "forceReplace", record: {
|
|
1702
|
+
recordType: "BridgeStatus", recordName: `companion-${slug}`,
|
|
1703
|
+
fields: {
|
|
1704
|
+
aliveAt: { value: Date.now(), type: "TIMESTAMP" },
|
|
1705
|
+
appVersion: { value: SERVER_INFO.version, type: "STRING" },
|
|
1706
|
+
device: { value: os.hostname(), type: "STRING" },
|
|
1707
|
+
// Seconds between wake checks; 0 when waking is off.
|
|
1708
|
+
pollSeconds: { value: cfg.wakeEnabled ? WAKE.everyMinutes * 60 : 0, type: "INT64" },
|
|
1709
|
+
},
|
|
1710
|
+
} }] });
|
|
1711
|
+
updateConfig(c => { c.presenceAt = Date.now(); });
|
|
1712
|
+
} catch (_) { /* reported next time */ }
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// Every companion's last check-in, from their presence records.
|
|
1716
|
+
async function companionPresence() {
|
|
1717
|
+
const names = companionRoster().map(e => e.name);
|
|
1718
|
+
if (!names.includes(AGENT.name)) names.push(AGENT.name);
|
|
1719
|
+
const slugs = names.map(n => agentFor(n).authorID.slice("agent.".length));
|
|
1720
|
+
const got = await cloud.lookup(slugs.map(s => `companion-${s}`));
|
|
1721
|
+
return names.map((name, i) => {
|
|
1722
|
+
const r = got[`companion-${slugs[i]}`];
|
|
1723
|
+
return r ? { name, version: cloud.field(r, "appVersion"), host: cloud.field(r, "device"),
|
|
1724
|
+
aliveAt: cloud.field(r, "aliveAt"), waking: (cloud.field(r, "pollSeconds") || 0) > 0 } : { name };
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// One line for status and workflow_state: this version, and whether it's the latest.
|
|
1729
|
+
async function versionLine() {
|
|
1730
|
+
const rel = await latestRelease();
|
|
1731
|
+
const auto = (readJSON(CP.config, {}) || {}).autoUpdate === false ? "auto-update off" : "auto-update on";
|
|
1732
|
+
if (!rel) return `${SERVER_INFO.version} (couldn't check npm for a newer one; ${auto})`;
|
|
1733
|
+
return newerVersion(rel.version, SERVER_INFO.version)
|
|
1734
|
+
? `${SERVER_INFO.version}, UPDATE AVAILABLE: ${rel.version}. Run "npx -y @opticell/workflow-companion@latest update" (${auto})`
|
|
1735
|
+
: `${SERVER_INFO.version}, the latest (${auto})`;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// How Claude Code should run the companion: @latest, so every new session
|
|
1739
|
+
// picks up the newest release. Windows needs cmd /c in front of npx.
|
|
1740
|
+
function claudeCodeCommand() {
|
|
1741
|
+
return `claude mcp add --scope user workflow -- ${process.platform === "win32" ? "cmd /c " : ""}npx -y @opticell/workflow-companion@latest`;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// `workflow-companion update`: refresh the saved copy now, say the version,
|
|
1745
|
+
// and how to keep Claude Code current.
|
|
1746
|
+
async function updateNow() {
|
|
1747
|
+
const result = await selfUpdate({ force: true });
|
|
1748
|
+
const rel = await latestRelease();
|
|
1749
|
+
console.log(`Latest release: ${rel ? rel.version : "unknown (couldn't reach npm)"}`);
|
|
1750
|
+
console.log(`Scheduled jobs' copy: ${result === "nothing scheduled" ? "none (waking and keep-alive aren't scheduled here)" : result}`);
|
|
1751
|
+
await reportPresence(true);
|
|
1752
|
+
console.log(`\nClaude Code runs whatever its registration says. To always get the latest:\n claude mcp remove workflow -s user\n ${claudeCodeCommand()}`);
|
|
1753
|
+
return 0;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// `workflow-companion auto-update on|off`.
|
|
1757
|
+
function setAutoUpdate(on) {
|
|
1758
|
+
if (!fs.existsSync(CP.config)) { console.error(signInHelp("Not signed in yet.")); return 2; }
|
|
1759
|
+
updateConfig(cfg => { cfg.autoUpdate = on; });
|
|
1760
|
+
console.log(on ? "Auto-update is on: the scheduled jobs update themselves when a new release is out."
|
|
1761
|
+
: "Auto-update is off. Update by hand with: npx -y @opticell/workflow-companion@latest update");
|
|
1762
|
+
return 0;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1542
1765
|
function printConfig() {
|
|
1543
1766
|
if (CLOUD) {
|
|
1544
|
-
process.stdout.write(`Claude Code:\n
|
|
1767
|
+
process.stdout.write(`Claude Code:\n ${claudeCodeCommand()}\n\nClaude Desktop → Settings → Developer → Edit Config, add:\n${JSON.stringify({ mcpServers: { workflow: { command: "npx", args: ["-y", "@opticell/workflow-companion@latest"] } } }, null, 2)}\n`);
|
|
1545
1768
|
return;
|
|
1546
1769
|
}
|
|
1547
1770
|
const cfg = { mcpServers: { workflow: { command: "node", args: [__filename], env: { WF_BRIDGE: ROOT } } } };
|
|
@@ -1555,6 +1778,8 @@ const HELP = `WorkFlow companion ${SERVER_INFO.version}
|
|
|
1555
1778
|
workflow-companion setup sign this computer in to iCloud for WorkFlow (--name, --takeover, --no-browser)
|
|
1556
1779
|
workflow-companion status which device answers, how fresh is the snapshot
|
|
1557
1780
|
workflow-companion keepalive renew the sign-in (what the scheduled job runs)
|
|
1781
|
+
workflow-companion update update the scheduled jobs' copy now, and say how to keep Claude Code current
|
|
1782
|
+
workflow-companion auto-update on|off whether the scheduled jobs update themselves (on by default)
|
|
1558
1783
|
workflow-companion wake-on start a Claude Code session on its own when @name work arrives
|
|
1559
1784
|
(--allow-tools, --model, --workdir, --max-minutes, --claude, --skip-existing)
|
|
1560
1785
|
workflow-companion wake-off stop doing that; work waits until you ask
|
|
@@ -1570,6 +1795,8 @@ else if (argv.includes("--help") || verb === "help") process.stdout.write(HELP);
|
|
|
1570
1795
|
else if (verb === "setup") setup().then(c => { process.exitCode = c; }, e => { console.error(e.message || e); process.exitCode = 1; });
|
|
1571
1796
|
else if (verb === "status") cloudStateText().then(t => console.log(t), e => { console.error(e.message || e); process.exitCode = 1; });
|
|
1572
1797
|
else if (verb === "keepalive") keepAlive().then(c => { process.exitCode = c; });
|
|
1798
|
+
else if (verb === "update") updateNow().then(c => { process.exitCode = c; }, e => { console.error(e.message || e); process.exitCode = 1; });
|
|
1799
|
+
else if (verb === "auto-update") process.exitCode = setAutoUpdate(argv[argv.indexOf("auto-update") + 1] !== "off");
|
|
1573
1800
|
else if (verb === "wake") wake().then(c => { process.exitCode = c; }, e => { logWake(`failed: ${e.message || e}`); process.exitCode = 1; });
|
|
1574
1801
|
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; });
|
|
1575
1802
|
else if (verb === "signout") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opticell/workflow-companion",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.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": [
|