@addai/node 0.30.6 → 0.30.9

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.
@@ -92,16 +92,45 @@ function runCmd(cmd, args, timeoutMs = 5000) {
92
92
  }
93
93
  });
94
94
  }
95
- // Curated model lists baked into the daemon. The CLIs don't expose a
96
- // stable "list models" command; the daemon reports whatever the
97
- // installed daemon version knows about, so bumping the daemon refreshes
98
- // the UI's dropdown.
95
+ // Curated model lists baked into the daemon. The CLIs still expose no
96
+ // non-interactive "list models" command, so this stays a constant — but
97
+ // WHAT goes in it is what decides whether the UI's dropdown rots.
98
+ //
99
+ // Do not "fix" this by probing the CLI. Checked against Claude Code
100
+ // 2.1.266: there is no `claude models` subcommand and no `claude config
101
+ // list`. Both strings are read as a PROMPT — the CLI starts a billed
102
+ // session and answers in prose. Probing that on every heartbeat would
103
+ // spend money several times a minute and still return nothing parseable.
104
+ //
105
+ // `claude --help` documents the real contract instead: --model takes
106
+ // "an alias for the latest model (e.g. 'fable', 'opus', or 'sonnet') or
107
+ // a model's full name (e.g. 'claude-fable-5')".
108
+ //
109
+ // So the ALIASES lead the list. They are the only entries that cannot go
110
+ // stale — the CLI resolves each to whatever is current in its tier — and
111
+ // staleness is exactly what went wrong before: this list was still
112
+ // offering claude-opus-4-1 and claude-sonnet-4-5 long after they were
113
+ // superseded, and no amount of heartbeating fixes a wrong constant.
114
+ // Pinned ids follow, newest first, for anyone who must hold one model.
115
+ //
116
+ // Availability is plan-gated (these nodes sign in with claude.ai
117
+ // subscriptions, not API keys), so a listed model can still be refused
118
+ // at spawn time — the CLI stays the source of truth for validity, the
119
+ // same way model-filter.ts already assumes.
99
120
  const CLAUDE_MODELS = [
121
+ // Aliases — always the latest in their tier.
122
+ 'opus',
123
+ 'sonnet',
124
+ 'haiku',
125
+ 'fable',
126
+ // Pinned ids, newest first.
127
+ 'claude-opus-5',
128
+ 'claude-sonnet-5',
129
+ 'claude-fable-5-1',
130
+ 'claude-opus-4-8',
100
131
  'claude-opus-4-7',
101
132
  'claude-sonnet-4-6',
102
133
  'claude-haiku-4-5',
103
- 'claude-opus-4-1',
104
- 'claude-sonnet-4-5',
105
134
  ];
106
135
  const CODEX_MODELS = [
107
136
  'gpt-5',
@@ -11,6 +11,7 @@ exports.spawnClaudePrint = spawnClaudePrint;
11
11
  const child_process_1 = require("child_process");
12
12
  const claude_binary_1 = require("./claude-binary");
13
13
  const win_1 = require("./win");
14
+ const claude_system_prompt_1 = require("./claude-system-prompt");
14
15
  /**
15
16
  * The text to emit for one flushed block, given what came before.
16
17
  *
@@ -174,9 +175,13 @@ function spawnClaudePrint(input) {
174
175
  // its node script instead (identity on POSIX). It can throw; so can
175
176
  // spawn() itself (synchronously, on win32). Surface either as a normal
176
177
  // rejected `done` so callers keep their single error path.
178
+ // An entity system prompt of tens of thousands of characters does not fit
179
+ // on a Windows command line — see claude-system-prompt.ts. No-op when it
180
+ // fits, which is every case that works today.
181
+ const sysPrompt = (0, claude_system_prompt_1.offloadLongSystemPrompt)(bin, args);
177
182
  let proc;
178
183
  try {
179
- const inv = (0, win_1.resolveCliInvocation)(bin, args);
184
+ const inv = (0, win_1.resolveCliInvocation)(bin, sysPrompt.args);
180
185
  proc = (0, child_process_1.spawn)(inv.file, inv.args, {
181
186
  cwd: input.workingDirectory,
182
187
  env: { ...process.env, TERM: 'dumb' },
@@ -187,6 +192,7 @@ function spawnClaudePrint(input) {
187
192
  });
188
193
  }
189
194
  catch (err) {
195
+ sysPrompt.cleanup();
190
196
  const done = Promise.reject(err instanceof Error ? err : new Error(String(err)));
191
197
  // Mark handled now — the caller awaits `done` a few ticks later, and an
192
198
  // already-rejected promise with no handler would fire unhandledRejection.
@@ -332,6 +338,7 @@ function spawnClaudePrint(input) {
332
338
  const done = new Promise((resolve, reject) => {
333
339
  proc.once('error', reject);
334
340
  proc.once('exit', (code) => {
341
+ sysPrompt.cleanup();
335
342
  flushThinking();
336
343
  flushText();
337
344
  const trimmed = stderrBuf.trim();
@@ -47,6 +47,7 @@ const path = __importStar(require("path"));
47
47
  const crypto_1 = require("crypto");
48
48
  const claude_binary_1 = require("./claude-binary");
49
49
  const win_1 = require("./win");
50
+ const claude_system_prompt_1 = require("./claude-system-prompt");
50
51
  const mcp_config_1 = require("./mcp-config");
51
52
  const paths_1 = require("./paths");
52
53
  function resolveMcpFilter(workingDirectory, enabled) {
@@ -150,6 +151,11 @@ function spawnClaudeForRuntime(input) {
150
151
  ...entityFlags,
151
152
  ...claudeArgs,
152
153
  ];
154
+ // Same overflow as claude-print: a long --append-system-prompt does not fit
155
+ // on a Windows command line. Applied before the branch so both the ConPTY
156
+ // and the POSIX shell form get the shortened argv; cleaned up in onExit
157
+ // beside the temp MCP config.
158
+ const sysPrompt = (0, claude_system_prompt_1.offloadLongSystemPrompt)(claudeBin, effectiveArgs);
153
159
  let shell;
154
160
  let shellArgs;
155
161
  if (isWin) {
@@ -160,14 +166,14 @@ function spawnClaudeForRuntime(input) {
160
166
  // node script (run via process.execPath) or passes an .exe straight
161
167
  // through; either target parses argv via CommandLineToArgvW, matching
162
168
  // node-pty's quoting. ConPTY handles both fine.
163
- const inv = (0, win_1.resolveCliInvocation)(claudeBin, effectiveArgs);
169
+ const inv = (0, win_1.resolveCliInvocation)(claudeBin, sysPrompt.args);
164
170
  shell = inv.file;
165
171
  shellArgs = inv.args;
166
172
  }
167
173
  else {
168
174
  // `exec "$@"` passes positional args straight through without shell escaping.
169
175
  shell = process.env.SHELL || '/bin/bash';
170
- shellArgs = ['-l', '-c', 'exec "$@"', '_', claudeBin, ...effectiveArgs];
176
+ shellArgs = ['-l', '-c', 'exec "$@"', '_', claudeBin, ...sysPrompt.args];
171
177
  }
172
178
  const proc = pty.spawn(shell, shellArgs, {
173
179
  name: 'xterm-256color',
@@ -196,6 +202,7 @@ function spawnClaudeForRuntime(input) {
196
202
  l(exitCode);
197
203
  }
198
204
  catch { }
205
+ sysPrompt.cleanup();
199
206
  // best-effort cleanup of any temp MCP config
200
207
  if (mcpConfigPath) {
201
208
  try {
@@ -0,0 +1,18 @@
1
+ /** Rough rendered length of `file` + `args` as one command line. Counts two
2
+ * quotes and a separating space per argument — the shape a renderer uses for
3
+ * anything that might contain whitespace. */
4
+ export declare function commandLineLength(file: string, args: string[]): number;
5
+ export interface OffloadResult {
6
+ args: string[];
7
+ /** Removes the temp file, if one was written. Safe to call more than once. */
8
+ cleanup: () => void;
9
+ }
10
+ /**
11
+ * Swap an oversized inline system prompt for a file-backed one.
12
+ *
13
+ * Returns the args unchanged (and a no-op cleanup) whenever the command line
14
+ * already fits, whenever there is no --append-system-prompt to move, or when
15
+ * the temp file cannot be written — in that last case the spawn is left to
16
+ * fail with its own error rather than a worse one invented here.
17
+ */
18
+ export declare function offloadLongSystemPrompt(file: string, args: string[], isWindows?: boolean): OffloadResult;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ // A long --append-system-prompt does not fit on a Windows command line.
3
+ //
4
+ // CreateProcess caps the entire command line at 32,767 characters, and an
5
+ // entity's system prompt is routinely tens of thousands — Hannah's was 31,004,
6
+ // which with the user prompt and the rest of the flags overflowed, and every
7
+ // run died with a bare `spawn ENAMETOOLONG` and no hint of which argument was
8
+ // to blame. POSIX is nowhere near its limit at these sizes (ARG_MAX is ~256KB
9
+ // on macOS, ~2MB on Linux), which is why this only ever bit on Windows — and
10
+ // only once claude could be spawned there at all.
11
+ //
12
+ // claude takes the same text from a file, so when the rendered command line
13
+ // would not fit, the prompt is written to a temp file and the flag becomes
14
+ // --append-system-prompt-file. ONLY when it would not fit: the inline form is
15
+ // what every working install uses today and an older claude may not know the
16
+ // -file variant, so nothing that currently works changes shape.
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.commandLineLength = commandLineLength;
52
+ exports.offloadLongSystemPrompt = offloadLongSystemPrompt;
53
+ const fs = __importStar(require("fs"));
54
+ const os = __importStar(require("os"));
55
+ const path = __importStar(require("path"));
56
+ const crypto_1 = require("crypto");
57
+ const INLINE_FLAG = '--append-system-prompt';
58
+ const FILE_FLAG = '--append-system-prompt-file';
59
+ /** Head-room under the real 32,767 limit. Both node and node-pty escape quotes
60
+ * and backslashes when they render argv into a command line, so what actually
61
+ * reaches CreateProcess is longer than the sum of the arguments. */
62
+ const WINDOWS_BUDGET = 30_000;
63
+ /** POSIX only needs catching if a prompt has gone genuinely pathological. */
64
+ const POSIX_BUDGET = 120_000;
65
+ /** Rough rendered length of `file` + `args` as one command line. Counts two
66
+ * quotes and a separating space per argument — the shape a renderer uses for
67
+ * anything that might contain whitespace. */
68
+ function commandLineLength(file, args) {
69
+ return args.reduce((n, a) => n + a.length + 3, file.length + 3);
70
+ }
71
+ /**
72
+ * Swap an oversized inline system prompt for a file-backed one.
73
+ *
74
+ * Returns the args unchanged (and a no-op cleanup) whenever the command line
75
+ * already fits, whenever there is no --append-system-prompt to move, or when
76
+ * the temp file cannot be written — in that last case the spawn is left to
77
+ * fail with its own error rather than a worse one invented here.
78
+ */
79
+ function offloadLongSystemPrompt(file, args, isWindows = process.platform === 'win32') {
80
+ const noop = { args, cleanup: () => { } };
81
+ const budget = isWindows ? WINDOWS_BUDGET : POSIX_BUDGET;
82
+ if (commandLineLength(file, args) <= budget)
83
+ return noop;
84
+ const i = args.indexOf(INLINE_FLAG);
85
+ if (i < 0 || i + 1 >= args.length)
86
+ return noop;
87
+ const promptPath = path.join(os.tmpdir(), `ainode-sysprompt-${(0, crypto_1.randomUUID)()}.txt`);
88
+ try {
89
+ fs.writeFileSync(promptPath, args[i + 1], 'utf8');
90
+ }
91
+ catch {
92
+ return noop;
93
+ }
94
+ const next = args.slice();
95
+ next[i] = FILE_FLAG;
96
+ next[i + 1] = promptPath;
97
+ let removed = false;
98
+ return {
99
+ args: next,
100
+ cleanup: () => {
101
+ if (removed)
102
+ return;
103
+ removed = true;
104
+ try {
105
+ fs.unlinkSync(promptPath);
106
+ }
107
+ catch { /* swept from tmp anyway */ }
108
+ },
109
+ };
110
+ }
@@ -71,13 +71,25 @@ const IDLE_CHECK_MS = 60_000;
71
71
  * leaves a half-open socket that never fires 'close', so without this the
72
72
  * client sits holding a dead handle and never reconnects. */
73
73
  const PING_INTERVAL_MS = 30_000;
74
+ /** How often to ask the relay which deploy is answering. Cloud Run keeps a
75
+ * retiring instance alive until its in-flight connections end — for this
76
+ * socket, the hourly cut — so after a relay deploy this node sat attached to
77
+ * the OLD instance, pongs and all, while every viewer landed on the new one
78
+ * and was told the machine was offline. The relay's greeting names the
79
+ * instance this socket reached; /health is answered by the live one. When
80
+ * the two differ, move. */
81
+ const REVISION_CHECK_MS = 60_000;
82
+ const RELAY_HTTP_URL = RELAY_URL.replace(/^ws/, 'http');
74
83
  let ws = null;
75
84
  let stopped = true;
76
85
  let backoff = RECONNECT_MIN_MS;
77
86
  let idleTimer = null;
78
- let liveTimer = null;
79
- /** Set on every pong; cleared when a ping goes out. Two misses = dead. */
80
- let missedPongs = 0;
87
+ /** Silences the CURRENT socket's timers; set when a socket opens so
88
+ * stopRelayClient can reach them. Each socket owns its own timers and miss
89
+ * count: a socket closing late used to clear the module-wide timer, which by
90
+ * then belonged to its replacement — leaving the live socket with no ping
91
+ * loop and no way to notice a dead link. */
92
+ let clearCurrentTimers = null;
81
93
  /** viewerId -> that viewer's OWN TCP socket to the desktop's RFB port.
82
94
  * Keyed per viewer, not per desktop: RFB is a stateful 1:1 protocol, so two
83
95
  * people watching the same screen each need their own connection and their
@@ -157,16 +169,51 @@ async function connect() {
157
169
  }
158
170
  const sock = new ws_1.default(`${RELAY_URL}/node?token=${encodeURIComponent(token)}`);
159
171
  ws = sock;
172
+ // Which deploy of the relay this socket reached, from its greeting. Null
173
+ // until the relay says — an older relay never does, and then there is
174
+ // nothing to compare and the check stays quiet.
175
+ let attachedRevision = null;
176
+ /** Set on every pong; cleared when a ping goes out. Two misses = dead. */
177
+ let missedPongs = 0;
178
+ let pingTimer = null;
179
+ let revisionTimer = null;
180
+ const clearTimers = () => {
181
+ if (pingTimer) {
182
+ clearInterval(pingTimer);
183
+ pingTimer = null;
184
+ }
185
+ if (revisionTimer) {
186
+ clearInterval(revisionTimer);
187
+ revisionTimer = null;
188
+ }
189
+ if (clearCurrentTimers === clearTimers)
190
+ clearCurrentTimers = null;
191
+ };
192
+ const checkRevision = async () => {
193
+ if (!attachedRevision || sock.readyState !== ws_1.default.OPEN)
194
+ return;
195
+ let live;
196
+ try {
197
+ const res = await fetch(`${RELAY_HTTP_URL}/health`, { signal: AbortSignal.timeout(8_000) });
198
+ live = (await res.json()).revision;
199
+ }
200
+ catch {
201
+ return;
202
+ } // could not ask; the ping loop still guards the link
203
+ if (live && live !== attachedRevision && sock.readyState === ws_1.default.OPEN) {
204
+ console.warn(`[relay] the relay moved from ${attachedRevision} to ${live} — reconnecting`);
205
+ sock.terminate(); // 'close' → retry → redial, onto the live instance
206
+ }
207
+ };
160
208
  sock.on('open', () => {
161
209
  backoff = RECONNECT_MIN_MS;
162
- missedPongs = 0;
163
- if (liveTimer)
164
- clearInterval(liveTimer);
210
+ clearCurrentTimers?.();
211
+ clearCurrentTimers = clearTimers;
165
212
  // A suspended Mac comes back with a socket that looks open and is not.
166
213
  // Ping until two go unanswered, then tear it down so the normal backoff
167
214
  // path reconnects - the daemon's own sleep detector reports the same
168
215
  // suspends this is guarding against.
169
- liveTimer = setInterval(() => {
216
+ pingTimer = setInterval(() => {
170
217
  if (sock.readyState !== ws_1.default.OPEN) {
171
218
  sock.terminate();
172
219
  return;
@@ -184,6 +231,7 @@ async function connect() {
184
231
  sock.terminate();
185
232
  }
186
233
  }, PING_INTERVAL_MS);
234
+ revisionTimer = setInterval(() => { void checkRevision(); }, REVISION_CHECK_MS);
187
235
  });
188
236
  sock.on('pong', () => { missedPongs = 0; });
189
237
  sock.on('message', async (raw) => {
@@ -208,6 +256,11 @@ async function connect() {
208
256
  sock.send(JSON.stringify({ type: 'gone', viewerId, reason }));
209
257
  }
210
258
  };
259
+ if (msg.type === 'hello') {
260
+ attachedRevision = msg.revision ?? null;
261
+ console.log(`[relay] connected — relay revision ${attachedRevision ?? '?'}`);
262
+ return;
263
+ }
211
264
  if (msg.type === 'attach') {
212
265
  // Claim the viewer BEFORE the await, so a packet arriving during the
213
266
  // lookup is held rather than treated as arriving after a lost bridge.
@@ -312,10 +365,7 @@ async function connect() {
312
365
  }
313
366
  });
314
367
  const retry = () => {
315
- if (liveTimer) {
316
- clearInterval(liveTimer);
317
- liveTimer = null;
318
- }
368
+ clearTimers(); // only ever this socket's own
319
369
  if (ws !== sock)
320
370
  return; // superseded by a newer socket
321
371
  dropBridges();
@@ -338,10 +388,7 @@ function stopRelayClient() {
338
388
  clearTimeout(idleTimer);
339
389
  idleTimer = null;
340
390
  }
341
- if (liveTimer) {
342
- clearInterval(liveTimer);
343
- liveTimer = null;
344
- }
391
+ clearCurrentTimers?.();
345
392
  dropBridges();
346
393
  try {
347
394
  ws?.close();
package/dist/win.d.ts CHANGED
@@ -34,6 +34,11 @@ export declare function resolveCliInvocation(bin: string, args: string[]): CliIn
34
34
  * pnpm/yarn-classic write `"%~dp0\<rel>.js"` (batch parameter expansion —
35
35
  * note: no trailing `%`). */
36
36
  declare function resolveNpmShimScript(shimPath: string): string | null;
37
+ /** Parse a .cmd shim for a `%~dp0`-relative NATIVE executable, for packages
38
+ * that ship one instead of a node script. Same two dialects and the same
39
+ * on-disk check as resolveNpmShimScript; deliberately a separate function so
40
+ * the npm path the auto-updater depends on is untouched by this. */
41
+ declare function resolveShimExecutable(shimPath: string): string | null;
37
42
  /**
38
43
  * Kill an agent child process AND everything it spawned.
39
44
  *
@@ -99,5 +104,6 @@ export declare function linkDir(real: string, dest: string): void;
99
104
  * worth exercising on any platform, not just on a Windows box. */
100
105
  export declare const __testables: {
101
106
  resolveNpmShimScript: typeof resolveNpmShimScript;
107
+ resolveShimExecutable: typeof resolveShimExecutable;
102
108
  };
103
109
  export {};
package/dist/win.js CHANGED
@@ -203,11 +203,24 @@ function resolveCliInvocation(bin, args) {
203
203
  }
204
204
  if (/\.(cmd|bat)$/i.test(target)) {
205
205
  const script = resolveNpmShimScript(target);
206
- if (!script) {
207
- throw new Error(`cannot spawn npm shim safely: ${target} — could not resolve the node script it wraps. ` +
208
- `Install the CLI's native Windows build (an .exe) or reinstall via npm.`);
209
- }
210
- return { file: process.execPath, args: [script, ...args] };
206
+ if (script)
207
+ return { file: process.execPath, args: [script, ...args] };
208
+ // Not every npm shim wraps a node script. A package may ship a native
209
+ // Windows binary and have its shim exec THAT:
210
+ //
211
+ // "%dp0%\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe" %*
212
+ //
213
+ // which is what @anthropic-ai/claude-code does now. There is no .js to
214
+ // find, so this threw — and the capabilities probe swallows the throw and
215
+ // reports the harness `available: false`. On a machine where `claude
216
+ // --version` worked in every shell, claude was simply missing from the
217
+ // picker. The old message's advice ('install the native Windows build')
218
+ // was already satisfied: the .exe sits inside the package the shim names.
219
+ const exe = resolveShimExecutable(target);
220
+ if (exe)
221
+ return { file: exe, args };
222
+ throw new Error(`cannot spawn npm shim safely: ${target} — it wraps neither a node script ` +
223
+ `nor an executable this can find. Reinstall the CLI via npm.`);
211
224
  }
212
225
  return { file: target, args };
213
226
  }
@@ -245,6 +258,28 @@ function resolveNpmShimScript(shimPath) {
245
258
  const re = /%(?:~dp0|dp0%)[\\/]([^"\s]+\.(?:m|c)?js)/gi;
246
259
  for (const m of body.matchAll(re))
247
260
  candidates.push(m[1]);
261
+ // ⚠️ The CLI entry point first — NOT the file's order.
262
+ //
263
+ // npm's own npm.cmd names two %~dp0 scripts, and the helper comes first:
264
+ //
265
+ // SET "NPM_PREFIX_JS=%~dp0\node_modules\npm\bin\npm-prefix.js"
266
+ // SET "NPM_CLI_JS=%~dp0\node_modules\npm\bin\npm-cli.js"
267
+ // ...
268
+ // "%NODE_EXE%" "%NPM_CLI_JS%" %*
269
+ //
270
+ // Only that last line runs, and it runs NPM_CLI_JS. Taking the first that
271
+ // exists therefore spawned npm-prefix.js — and the reason this hid for so
272
+ // long is that doing so does not FAIL: it exits 0 and prints the npm prefix
273
+ // path. npmLatestVersion saw a clean exit whose output was not a version,
274
+ // returned null, and the auto-updater logged "could not read the latest
275
+ // published version" on every tick. A Windows node then sat for ever on the
276
+ // version it was installed with — the machine this was found on was still
277
+ // running 0.30.3 while 0.30.6 was published.
278
+ //
279
+ // Every package bin shim names exactly one script and is unaffected: the
280
+ // sort is stable, so with nothing to promote nothing moves.
281
+ const isCliEntry = (rel) => /(^|[\\/])(npm|npx)-cli\.(m|c)?js$/i.test(rel);
282
+ candidates.sort((a, b) => Number(isCliEntry(b)) - Number(isCliEntry(a)));
248
283
  for (const rel of candidates) {
249
284
  // Shims always write backslashes. Split on either separator and re-join
250
285
  // with the platform's, so the same parsing is exercisable off Windows —
@@ -256,6 +291,26 @@ function resolveNpmShimScript(shimPath) {
256
291
  }
257
292
  return null;
258
293
  }
294
+ /** Parse a .cmd shim for a `%~dp0`-relative NATIVE executable, for packages
295
+ * that ship one instead of a node script. Same two dialects and the same
296
+ * on-disk check as resolveNpmShimScript; deliberately a separate function so
297
+ * the npm path the auto-updater depends on is untouched by this. */
298
+ function resolveShimExecutable(shimPath) {
299
+ let body;
300
+ try {
301
+ body = fs.readFileSync(shimPath, 'utf8');
302
+ }
303
+ catch {
304
+ return null;
305
+ }
306
+ const re = /%(?:~dp0|dp0%)[\\/]([^"\s]+\.exe)/gi;
307
+ for (const m of body.matchAll(re)) {
308
+ const exe = path.join(path.dirname(shimPath), ...m[1].split(/[\\/]+/));
309
+ if (fs.existsSync(exe))
310
+ return exe;
311
+ }
312
+ return null;
313
+ }
259
314
  /** How long a POSIX agent gets to honour SIGINT before we stop asking. */
260
315
  const KILL_ESCALATE_MS = 3000;
261
316
  /** One `ps` snapshot as (pid, ppid) pairs. Empty on any failure — callers
@@ -444,4 +499,4 @@ function linkDir(real, dest) {
444
499
  }
445
500
  /** Exposed for tests only: the shim parsing is pure file-shape logic and is
446
501
  * worth exercising on any platform, not just on a Windows box. */
447
- exports.__testables = { resolveNpmShimScript };
502
+ exports.__testables = { resolveNpmShimScript, resolveShimExecutable };
package/package.json CHANGED
@@ -1,62 +1,62 @@
1
- {
2
- "name": "@addai/node",
3
- "version": "0.30.6",
4
- "description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
5
- "license": "MIT",
6
- "keywords": [
7
- "addai",
8
- "entity-studio",
9
- "claude",
10
- "codex",
11
- "kimi",
12
- "gemini",
13
- "agent",
14
- "daemon",
15
- "supabase",
16
- "mcp"
17
- ],
18
- "repository": {
19
- "type": "git",
20
- "url": "git+https://github.com/just-AddAi/addai-entity-runtime.git"
21
- },
22
- "homepage": "https://github.com/just-AddAi/addai-entity-runtime#readme",
23
- "bugs": {
24
- "url": "https://github.com/just-AddAi/addai-entity-runtime/issues"
25
- },
26
- "type": "commonjs",
27
- "main": "dist/index.js",
28
- "bin": {
29
- "ainode": "dist/cli.js"
30
- },
31
- "publishConfig": {
32
- "access": "public"
33
- },
34
- "files": [
35
- "dist",
36
- "scripts",
37
- "README.md",
38
- "assets"
39
- ],
40
- "scripts": {
41
- "build": "tsc -p tsconfig.json && node scripts/copy-assets.js",
42
- "start": "node dist/cli.js",
43
- "dev": "tsc -p tsconfig.json && node dist/cli.js",
44
- "test": "npm run build && node --test test/*.test.mjs",
45
- "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
46
- "postinstall": "node scripts/fix-pty-helper.js",
47
- "prepublishOnly": "npm run build"
48
- },
49
- "engines": {
50
- "node": ">=18"
51
- },
52
- "dependencies": {
53
- "@addai/node-flows": "^1.0.0",
54
- "node-pty": "^1.1.0",
55
- "ws": "^8.21.3"
56
- },
57
- "devDependencies": {
58
- "@types/node": "^20.0.0",
59
- "@types/ws": "^8.18.1",
60
- "typescript": "^5.6.0"
61
- }
62
- }
1
+ {
2
+ "name": "@addai/node",
3
+ "version": "0.30.9",
4
+ "description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "addai",
8
+ "entity-studio",
9
+ "claude",
10
+ "codex",
11
+ "kimi",
12
+ "gemini",
13
+ "agent",
14
+ "daemon",
15
+ "supabase",
16
+ "mcp"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/just-AddAi/addai-entity-runtime.git"
21
+ },
22
+ "homepage": "https://github.com/just-AddAi/addai-entity-runtime#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/just-AddAi/addai-entity-runtime/issues"
25
+ },
26
+ "type": "commonjs",
27
+ "main": "dist/index.js",
28
+ "bin": {
29
+ "ainode": "dist/cli.js"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "scripts",
37
+ "README.md",
38
+ "assets"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json && node scripts/copy-assets.js",
42
+ "start": "node dist/cli.js",
43
+ "dev": "tsc -p tsconfig.json && node dist/cli.js",
44
+ "test": "npm run build && node --test test/*.test.mjs",
45
+ "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\"",
46
+ "postinstall": "node scripts/fix-pty-helper.js",
47
+ "prepublishOnly": "npm run build"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "dependencies": {
53
+ "@addai/node-flows": "^1.0.0",
54
+ "node-pty": "^1.1.0",
55
+ "ws": "^8.21.3"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^20.0.0",
59
+ "@types/ws": "^8.18.1",
60
+ "typescript": "^5.6.0"
61
+ }
62
+ }