tokmon 0.28.6 → 0.28.7

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.
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ classifyDaemonCompatibility,
4
+ daemonConflictMessage,
3
5
  isAlive,
4
6
  probeHealth,
5
7
  readForeignLock,
6
8
  readLock,
7
9
  retireIncompatibleCliOwner,
8
10
  verifyLock
9
- } from "./chunk-MMAKKTCU.js";
11
+ } from "./chunk-WHOIUSJM.js";
10
12
  import {
11
13
  daemonChannelFromWire,
12
14
  resolveDaemonChannel
@@ -54,26 +56,43 @@ async function attach(opts, protocolVersion) {
54
56
  const lock = await verifyLock(readLock(opts), protocolVersion);
55
57
  return lock ? connected(lock.url) : null;
56
58
  }
57
- async function incompatibleDesktopIssue(opts, protocolVersion, timeoutMs) {
59
+ function incompatibleOwnerIssue(owner, protocolVersion, options = {}) {
60
+ return {
61
+ kind: owner.ownerKind === "desktop" ? "incompatible-desktop" : owner.ownerKind === "cli" ? "incompatible-cli" : "incompatible-owner",
62
+ message: daemonConflictMessage(owner, {
63
+ clientKind: "cli",
64
+ clientProtocolVersion: protocolVersion,
65
+ ...options
66
+ }),
67
+ ownerKind: owner.ownerKind ?? null,
68
+ ownerVersion: owner.version ?? null,
69
+ ownerProtocolVersion: owner.protocolVersion ?? null,
70
+ clientProtocolVersion: protocolVersion
71
+ };
72
+ }
73
+ async function arbitrateIncompatibleOwner(opts, protocolVersion, timeoutMs) {
58
74
  const owner = readForeignLock(opts);
59
- if (!owner || owner.ownerKind !== "desktop" || owner.protocolVersion === void 0 || owner.protocolVersion === protocolVersion || !isAlive(owner.pid)) return null;
75
+ if (!owner || !isAlive(owner.pid)) return { retired: false, issue: null };
76
+ const decision = classifyDaemonCompatibility(owner, protocolVersion);
60
77
  const verified = await probeHealth(owner.url, owner.wsToken, {
61
- ownerKind: "desktop",
62
78
  channel: owner.channel,
63
- protocolVersion: owner.protocolVersion,
79
+ ...owner.ownerKind ? { ownerKind: owner.ownerKind } : {},
80
+ ...owner.protocolVersion === void 0 ? {} : { protocolVersion: owner.protocolVersion },
64
81
  ...owner.version ? { version: owner.version } : {}
65
82
  }, Math.min(500, timeoutMs));
66
- if (!verified) return null;
67
- const ownerIsOlder = owner.protocolVersion < protocolVersion;
68
- const direction = ownerIsOlder ? "an older" : "a newer";
69
- const ownerName = owner.version ? `Tokmon Desktop ${owner.version}` : "Tokmon Desktop";
70
- const recovery = ownerIsOlder ? "Update and restart the desktop app, or quit it before running the CLI." : "Update the CLI, or quit the desktop app before running this version. With pnpm release-age filtering, use `pnpm --config.minimum-release-age=0 dlx tokmon@latest` once.";
71
- return {
72
- kind: "incompatible-desktop",
73
- message: `${ownerName} is using ${direction} background service (protocol ${owner.protocolVersion}; this CLI needs ${protocolVersion}). ${recovery}`,
74
- ownerVersion: owner.version ?? null,
75
- ownerProtocolVersion: owner.protocolVersion,
76
- clientProtocolVersion: protocolVersion
83
+ if (!verified) {
84
+ return {
85
+ retired: false,
86
+ issue: incompatibleOwnerIssue(owner, protocolVersion, { verificationFailed: true })
87
+ };
88
+ }
89
+ if (decision.action !== "retire") {
90
+ return { retired: false, issue: incompatibleOwnerIssue(owner, protocolVersion) };
91
+ }
92
+ const retired = await retireIncompatibleCliOwner(opts, protocolVersion, timeoutMs);
93
+ return retired ? { retired: true, issue: null } : {
94
+ retired: false,
95
+ issue: incompatibleOwnerIssue(owner, protocolVersion, { retirementFailed: true })
77
96
  };
78
97
  }
79
98
  async function attachOrSpawn(opts = {}) {
@@ -84,11 +103,21 @@ async function attachOrSpawn(opts = {}) {
84
103
  const lockOpts = { cachePath: opts.cachePath, channel };
85
104
  const existing = await attach(lockOpts, protocolVersion);
86
105
  if (existing) return existing;
87
- const desktopIssue = await incompatibleDesktopIssue(lockOpts, protocolVersion, timeoutMs);
88
- if (desktopIssue) return degraded(desktopIssue);
89
- await retireIncompatibleCliOwner(lockOpts, protocolVersion, timeoutMs);
90
- const upgraded = await attach(lockOpts, protocolVersion);
91
- if (upgraded) return upgraded;
106
+ for (let attempt = 0; attempt < 2; attempt++) {
107
+ const arbitration = await arbitrateIncompatibleOwner(lockOpts, protocolVersion, timeoutMs);
108
+ if (arbitration.issue) return degraded(arbitration.issue);
109
+ if (!arbitration.retired) break;
110
+ const upgraded = await attach(lockOpts, protocolVersion);
111
+ if (upgraded) return upgraded;
112
+ }
113
+ const raced = await attach(lockOpts, protocolVersion);
114
+ if (raced) return raced;
115
+ const raceArbitration = await arbitrateIncompatibleOwner(lockOpts, protocolVersion, timeoutMs);
116
+ if (raceArbitration.issue) return degraded(raceArbitration.issue);
117
+ if (raceArbitration.retired) {
118
+ const replacement = await attach(lockOpts, protocolVersion);
119
+ if (replacement) return replacement;
120
+ }
92
121
  const entry = opts.entry ?? process.argv[1];
93
122
  if (!entry) return degraded();
94
123
  const execPath = opts.execPath ?? process.execPath;
@@ -7,6 +7,52 @@ import {
7
7
  cacheDir
8
8
  } from "./chunk-O7ORALKA.js";
9
9
 
10
+ // src/web/daemon-compatibility.ts
11
+ function classifyDaemonCompatibility(owner, clientProtocolVersion) {
12
+ if (owner.protocolVersion === clientProtocolVersion) {
13
+ return { action: "attach", reason: "same-protocol" };
14
+ }
15
+ if (owner.ownerKind === "desktop") {
16
+ return { action: "refuse", reason: "desktop-owner" };
17
+ }
18
+ if (owner.ownerKind === "cli" && owner.protocolVersion !== void 0) {
19
+ return owner.protocolVersion < clientProtocolVersion ? { action: "retire", reason: "older-cli" } : { action: "refuse", reason: "newer-cli" };
20
+ }
21
+ if (owner.ownerKind === void 0 && owner.protocolVersion === void 0) {
22
+ return { action: "retire", reason: "legacy-cli" };
23
+ }
24
+ return { action: "refuse", reason: "ambiguous-owner" };
25
+ }
26
+ function daemonConflictMessage(owner, context) {
27
+ const kind = owner.ownerKind === "desktop" ? "Tokmon Desktop" : owner.ownerKind === "cli" ? "Tokmon CLI background service" : "Legacy Tokmon background service";
28
+ const name = owner.version ? `${kind} ${owner.version}` : kind;
29
+ const ownerProtocol = owner.protocolVersion === void 0 ? "an implicit legacy protocol" : `protocol ${owner.protocolVersion}`;
30
+ const client = context.clientKind === "desktop" ? "this desktop app" : "this CLI";
31
+ if (context.verificationFailed) {
32
+ return `A live lock claims ${name} using ${ownerProtocol}, but its owner proof could not be verified. It was not signalled. Quit the existing Tokmon process, remove a stale lock only after confirming it has stopped, then retry.`;
33
+ }
34
+ if (owner.protocolVersion === context.clientProtocolVersion) {
35
+ return `${name} claims compatible protocol ${context.clientProtocolVersion}, but its owner proof could not be verified. It was not signalled. Quit the existing Tokmon process, then retry.`;
36
+ }
37
+ const summary = `${name} owns the background service using ${ownerProtocol}; ${client} needs protocol ${context.clientProtocolVersion}.`;
38
+ if (context.retirementFailed) {
39
+ return `${summary} Tokmon could not stop that older CLI service safely, so it was left running. Quit the existing Tokmon CLI/background service, then retry.`;
40
+ }
41
+ if (owner.ownerKind === "desktop") {
42
+ if (context.clientKind === "desktop") {
43
+ return owner.protocolVersion !== void 0 && owner.protocolVersion < context.clientProtocolVersion ? `${summary} Fully quit the older Tokmon Desktop instance, install the current release, then reopen Tokmon.` : `${summary} This app is older than the running Tokmon Desktop instance. Reopen the newer app, or replace this installation with the current release.`;
44
+ }
45
+ if (owner.protocolVersion !== void 0 && owner.protocolVersion < context.clientProtocolVersion) {
46
+ return `${summary} In Tokmon Desktop, open Settings \u2192 Desktop App \u2192 Check for Updates, install the update, then restart Tokmon. Alternatively, quit Tokmon Desktop before retrying the CLI.`;
47
+ }
48
+ return `${summary} Update the CLI with \`pnpm --config.minimum-release-age=0 dlx tokmon@latest\`, or quit Tokmon Desktop before retrying this CLI.`;
49
+ }
50
+ if (owner.ownerKind === "cli" && owner.protocolVersion !== void 0 && owner.protocolVersion > context.clientProtocolVersion) {
51
+ return context.clientKind === "desktop" ? `${summary} Update Tokmon Desktop and restart it, or stop the newer CLI background service before reopening the app.` : `${summary} Update the CLI with \`pnpm --config.minimum-release-age=0 dlx tokmon@latest\`, or stop the newer background service before retrying.`;
52
+ }
53
+ return `${summary} The owner could not be identified safely and was not signalled. Quit the existing Tokmon process, then retry.`;
54
+ }
55
+
10
56
  // src/web/lockfile.ts
11
57
  import { closeSync, fchmodSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
12
58
  import { isAbsolute, join } from "path";
@@ -247,11 +293,11 @@ async function retireIncompatibleCliOwner(opts, protocolVersion, timeoutMs = TAK
247
293
  const compatible = readLock(opts);
248
294
  if (compatible?.protocolVersion === protocolVersion) return false;
249
295
  const foreign = readForeignLock(opts);
250
- const legacyCli = foreign?.ownerKind === void 0 && foreign?.protocolVersion === void 0;
251
- if (!foreign || foreign.state !== "ready" || foreign.ownerKind === "desktop" || foreign.ownerKind !== "cli" && !legacyCli || foreign.pid === process.pid || !isAlive(foreign.pid)) return false;
296
+ const decision = foreign && classifyDaemonCompatibility(foreign, protocolVersion);
297
+ if (!foreign || foreign.state !== "ready" || decision?.action !== "retire" || foreign.pid === process.pid || !isAlive(foreign.pid)) return false;
252
298
  const health = await ownerHealth(foreign.url, foreign.wsToken, Math.min(1e3, timeoutMs));
253
299
  const healthChannel = daemonChannelFromWire(health?.channel);
254
- if (health?.ok !== true || health.owner !== true || health.ownerKind !== void 0 && health.ownerKind !== "cli" || healthChannel !== foreign.channel) return false;
300
+ if (health?.ok !== true || health.owner !== true || (decision.reason === "legacy-cli" ? health.ownerKind !== void 0 || health.protocolVersion !== void 0 : health.ownerKind !== "cli" || health.protocolVersion !== foreign.protocolVersion) || healthChannel !== foreign.channel) return false;
255
301
  try {
256
302
  process.kill(foreign.pid, "SIGTERM");
257
303
  } catch {
@@ -279,6 +325,8 @@ async function retireIncompatibleCliOwner(opts, protocolVersion, timeoutMs = TAK
279
325
  }
280
326
 
281
327
  export {
328
+ classifyDaemonCompatibility,
329
+ daemonConflictMessage,
282
330
  lockfilePath,
283
331
  readLock,
284
332
  readForeignLock,
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  attachOrSpawn
4
- } from "./chunk-23NB5Z6V.js";
5
- import "./chunk-MMAKKTCU.js";
4
+ } from "./chunk-MIXBF6QU.js";
5
+ import "./chunk-WHOIUSJM.js";
6
6
  import "./chunk-UYPDMVW5.js";
7
7
  import {
8
8
  createDaemonRpcClient
package/dist/cli.js CHANGED
@@ -41,7 +41,7 @@ function validateServeArgs(serveArgs) {
41
41
  }
42
42
  async function main() {
43
43
  if (subcommand && ["usage", "models", "query", "providers", "snapshot", "config"].includes(subcommand)) {
44
- const { runQueryCommand } = await import("./cli-command-MW6EBLNA.js");
44
+ const { runQueryCommand } = await import("./cli-command-XPFM6LUP.js");
45
45
  try {
46
46
  const output = await runQueryCommand(
47
47
  subcommand,
@@ -60,14 +60,14 @@ Run tokmon ${subcommand} --help for usage.
60
60
  return;
61
61
  }
62
62
  if (subcommand === "__daemon") {
63
- const { runDaemon } = await import("./daemon-XB6ZORTE.js");
63
+ const { runDaemon } = await import("./daemon-KFMXYG7V.js");
64
64
  await runDaemon(args.slice(1), { foreground: false });
65
65
  process.exitCode ??= 0;
66
66
  return;
67
67
  }
68
68
  if (subcommand === "serve" || subcommand === "web") {
69
69
  validateServeArgs(args.slice(1));
70
- const { runDaemon } = await import("./daemon-XB6ZORTE.js");
70
+ const { runDaemon } = await import("./daemon-KFMXYG7V.js");
71
71
  await runDaemon(args.slice(1), { foreground: true });
72
72
  process.exitCode ??= 0;
73
73
  return;
@@ -117,7 +117,7 @@ Run tokmon ${subcommand} --help for usage.
117
117
  }
118
118
  const { loadConfig } = await import("./config-JEDXAKPZ.js");
119
119
  const { resolveGlyphs, setGlyphs } = await import("./glyphs-NKCSZLGO.js");
120
- const { attachOrSpawn } = await import("./daemon-handle-4WDYZBVL.js");
120
+ const { attachOrSpawn } = await import("./daemon-handle-6RYUJPC2.js");
121
121
  const config = await loadConfig();
122
122
  setGlyphs(resolveGlyphs({
123
123
  flag: asciiFlag,
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  acquireLock,
4
+ classifyDaemonCompatibility,
5
+ daemonConflictMessage,
4
6
  isAlive,
5
7
  lockfilePath,
6
8
  readForeignLock,
@@ -12,7 +14,7 @@ import {
12
14
  unlinkLock,
13
15
  verifyLock,
14
16
  writeLock
15
- } from "./chunk-MMAKKTCU.js";
17
+ } from "./chunk-WHOIUSJM.js";
16
18
  import {
17
19
  appVersion,
18
20
  startWebServer
@@ -40,9 +42,13 @@ var OWNER_VERIFY_TIMEOUT_MS = 250;
40
42
  var OWNER_TAKEOVER_TIMEOUT_MS = 5e3;
41
43
  var localOwners = /* @__PURE__ */ new Map();
42
44
  var DaemonOwnerUnavailableError = class extends Error {
43
- constructor(message) {
45
+ owner;
46
+ clientProtocolVersion;
47
+ constructor(message, owner = null, clientProtocolVersion = null) {
44
48
  super(message);
45
49
  this.name = "DaemonOwnerUnavailableError";
50
+ this.owner = owner;
51
+ this.clientProtocolVersion = clientProtocolVersion;
46
52
  }
47
53
  };
48
54
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -79,12 +85,31 @@ async function waitForOwner(opts, protocolVersion) {
79
85
  }
80
86
  return null;
81
87
  }
82
- function incompatibleOwnerError(lock) {
83
- if (!lock) return new DaemonOwnerUnavailableError("another legacy CLI daemon owns the lock and could not be retired safely");
88
+ function incompatibleOwnerError(owner, opts, protocolVersion, retirementFailed = false) {
84
89
  return new DaemonOwnerUnavailableError(
85
- `another ${lock.ownerKind} daemon owns the lock but is not compatible with protocol ${lock.protocolVersion}`
90
+ daemonConflictMessage(owner, {
91
+ clientKind: opts.ownerKind,
92
+ clientProtocolVersion: protocolVersion,
93
+ retirementFailed
94
+ }),
95
+ owner,
96
+ protocolVersion
86
97
  );
87
98
  }
99
+ async function retireOrRefuseOwner(owner, opts, protocolVersion) {
100
+ const decision = classifyDaemonCompatibility(owner, protocolVersion);
101
+ if (decision.action !== "retire") {
102
+ throw incompatibleOwnerError(owner, opts, protocolVersion);
103
+ }
104
+ const retired = await retireIncompatibleCliOwner(
105
+ opts,
106
+ protocolVersion,
107
+ opts.ownerTakeoverTimeoutMs ?? OWNER_TAKEOVER_TIMEOUT_MS
108
+ );
109
+ if (!retired) {
110
+ throw incompatibleOwnerError(owner, opts, protocolVersion, true);
111
+ }
112
+ }
88
113
  async function discoverOwner(opts, protocolVersion) {
89
114
  const local = localOwnerFor(opts, protocolVersion);
90
115
  if (local) return local;
@@ -104,25 +129,14 @@ async function discoverOwner(opts, protocolVersion) {
104
129
  current = readLock(opts);
105
130
  }
106
131
  if (current && isAlive(current.pid)) {
107
- const retired = await retireIncompatibleCliOwner(
108
- opts,
109
- protocolVersion,
110
- opts.ownerTakeoverTimeoutMs ?? OWNER_TAKEOVER_TIMEOUT_MS
111
- );
112
- if (retired) return null;
113
- throw incompatibleOwnerError(current);
132
+ await retireOrRefuseOwner(current, opts, protocolVersion);
133
+ return null;
114
134
  }
115
135
  if (current) reclaimDeadLock(opts);
116
136
  const foreign = readForeignLock(opts);
117
137
  if (foreign && isAlive(foreign.pid)) {
118
- if (await retireIncompatibleCliOwner(
119
- opts,
120
- protocolVersion,
121
- opts.ownerTakeoverTimeoutMs ?? OWNER_TAKEOVER_TIMEOUT_MS
122
- )) return null;
123
- throw incompatibleOwnerError(
124
- foreign.ownerKind && foreign.protocolVersion ? { ownerKind: foreign.ownerKind, protocolVersion: foreign.protocolVersion } : null
125
- );
138
+ await retireOrRefuseOwner(foreign, opts, protocolVersion);
139
+ return null;
126
140
  }
127
141
  return null;
128
142
  }
@@ -143,36 +157,23 @@ function reservationFor(opts, version, protocolVersion, capabilities) {
143
157
  };
144
158
  }
145
159
  async function reserveOwnership(opts, reservation) {
146
- if (acquireLock(reservation, opts)) return null;
147
- const winner = await waitForOwner(opts, reservation.protocolVersion);
148
- if (winner) return winner;
149
- const current = readLock(opts);
150
- if (current && isAlive(current.pid)) {
151
- const retired = await retireIncompatibleCliOwner(
152
- opts,
153
- reservation.protocolVersion,
154
- opts.ownerTakeoverTimeoutMs ?? OWNER_TAKEOVER_TIMEOUT_MS
155
- );
156
- if (!retired) throw incompatibleOwnerError(current);
160
+ for (let attempt = 0; attempt < 3; attempt++) {
157
161
  if (acquireLock(reservation, opts)) return null;
158
- const successor2 = await waitForOwner(opts, reservation.protocolVersion);
159
- if (successor2) return successor2;
160
- }
161
- if (current) reclaimDeadLock(opts);
162
- const foreign = readForeignLock(opts);
163
- if (foreign && isAlive(foreign.pid)) {
164
- const retired = await retireIncompatibleCliOwner(
165
- opts,
166
- reservation.protocolVersion,
167
- opts.ownerTakeoverTimeoutMs ?? OWNER_TAKEOVER_TIMEOUT_MS
168
- );
169
- if (!retired) throw incompatibleOwnerError(
170
- foreign.ownerKind && foreign.protocolVersion ? { ownerKind: foreign.ownerKind, protocolVersion: foreign.protocolVersion } : null
171
- );
162
+ const winner = await waitForOwner(opts, reservation.protocolVersion);
163
+ if (winner) return winner;
164
+ const current = readLock(opts);
165
+ if (current && isAlive(current.pid)) {
166
+ await retireOrRefuseOwner(current, opts, reservation.protocolVersion);
167
+ continue;
168
+ }
169
+ if (current) reclaimDeadLock(opts);
170
+ const foreign = readForeignLock(opts);
171
+ if (foreign && isAlive(foreign.pid)) {
172
+ await retireOrRefuseOwner(foreign, opts, reservation.protocolVersion);
173
+ continue;
174
+ }
175
+ reclaimAbandonedLock(opts, 0);
172
176
  }
173
- if (acquireLock(reservation, opts)) return null;
174
- const successor = await waitForOwner(opts, reservation.protocolVersion);
175
- if (successor) return successor;
176
177
  throw new DaemonOwnerUnavailableError("daemon startup is already in progress");
177
178
  }
178
179
  function owned(opts, lock, config, server) {
@@ -212,6 +213,7 @@ async function acquireOrAttachDaemon(opts) {
212
213
  protocolVersion,
213
214
  capabilities
214
215
  );
216
+ await opts.beforeReserve?.();
215
217
  const winner = await reserveOwnership(opts, reservation);
216
218
  if (winner) return attached(winner);
217
219
  let server = null;
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  attachOrSpawn
4
- } from "./chunk-23NB5Z6V.js";
5
- import "./chunk-MMAKKTCU.js";
4
+ } from "./chunk-MIXBF6QU.js";
5
+ import "./chunk-WHOIUSJM.js";
6
6
  import "./chunk-UYPDMVW5.js";
7
7
  import "./chunk-UON47FYF.js";
8
8
  import "./chunk-O7ORALKA.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokmon",
3
- "version": "0.28.6",
3
+ "version": "0.28.7",
4
4
  "description": "Terminal + web dashboard for Claude Code, Codex, Cursor, Grok, opencode, pi, Copilot, Gemini & more — usage, limits, and costs",
5
5
  "type": "module",
6
6
  "bin": {