@addai/node 0.30.7 → 0.30.10
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/dist/capabilities.js +35 -6
- package/dist/claude-print.js +8 -1
- package/dist/claude-spawn.js +9 -2
- package/dist/claude-system-prompt.d.ts +18 -0
- package/dist/claude-system-prompt.js +110 -0
- package/dist/cli.js +4 -0
- package/dist/desktop/relay-client.js +62 -15
- package/dist/index.js +52 -19
- package/dist/tui/run.d.ts +27 -0
- package/dist/tui/run.js +93 -16
- package/dist/win.d.ts +6 -0
- package/dist/win.js +39 -6
- package/package.json +62 -62
package/dist/capabilities.js
CHANGED
|
@@ -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
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
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',
|
package/dist/claude-print.js
CHANGED
|
@@ -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();
|
package/dist/claude-spawn.js
CHANGED
|
@@ -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,
|
|
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, ...
|
|
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
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -202,6 +202,10 @@ async function main() {
|
|
|
202
202
|
pid: runtime.pid,
|
|
203
203
|
startedAt: Date.now(),
|
|
204
204
|
version: index_1.VERSION,
|
|
205
|
+
// The daemon is running in this process, so the dashboard needs a way to
|
|
206
|
+
// stop it properly when the user actually asks — and, just as important,
|
|
207
|
+
// a way to tell that case apart from the terminal disappearing.
|
|
208
|
+
stop: runtime.stop,
|
|
205
209
|
});
|
|
206
210
|
return;
|
|
207
211
|
}
|
|
@@ -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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
clearInterval(liveTimer);
|
|
343
|
-
liveTimer = null;
|
|
344
|
-
}
|
|
391
|
+
clearCurrentTimers?.();
|
|
345
392
|
dropBridges();
|
|
346
393
|
try {
|
|
347
394
|
ws?.close();
|
package/dist/index.js
CHANGED
|
@@ -376,7 +376,7 @@ async function sweepStaleSessionDirs() {
|
|
|
376
376
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
377
377
|
const path = require('path');
|
|
378
378
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
379
|
-
const { RUNTIME_SESSIONS_DIR } = require('./paths');
|
|
379
|
+
const { RUNTIME_SESSIONS_DIR, RUNTIME_LOG_FILE } = require('./paths');
|
|
380
380
|
if (!fs.existsSync(RUNTIME_SESSIONS_DIR))
|
|
381
381
|
return;
|
|
382
382
|
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -404,30 +404,63 @@ async function sweepStaleSessionDirs() {
|
|
|
404
404
|
catch { /* unreadable dir — fall back to its own mtime */ }
|
|
405
405
|
return newest;
|
|
406
406
|
};
|
|
407
|
-
|
|
407
|
+
// Deleting a session dir un-homes any chat still resuming a session keyed to
|
|
408
|
+
// it, so the names have to be recoverable — a bare count made that class of
|
|
409
|
+
// breakage invisible. But a node that has been up for months sweeps hundreds
|
|
410
|
+
// at once, and one console line each buried the dashboard under a screenful
|
|
411
|
+
// of scrollback on every single start. So: the full list goes to the daemon
|
|
412
|
+
// log, and the terminal gets a sample and the totals.
|
|
413
|
+
const SHOWN = 5;
|
|
414
|
+
const swept = [];
|
|
415
|
+
const failures = [];
|
|
408
416
|
let kept = 0;
|
|
409
417
|
for (const name of fs.readdirSync(RUNTIME_SESSIONS_DIR)) {
|
|
410
418
|
const p = path.join(RUNTIME_SESSIONS_DIR, name);
|
|
419
|
+
let st;
|
|
411
420
|
try {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
421
|
+
st = fs.statSync(p);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
continue; /* unreadable entry */
|
|
425
|
+
}
|
|
426
|
+
if (!st.isDirectory())
|
|
427
|
+
continue;
|
|
428
|
+
if (effectiveMtimeMs(p, st.mtimeMs) >= cutoff) {
|
|
429
|
+
kept++;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
434
|
+
swept.push(p);
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
// A dir we cannot delete comes back every start. It used to be announced
|
|
438
|
+
// as swept each time, forever, with the real error swallowed — so the
|
|
439
|
+
// one line that would explain a home disk filling up never appeared.
|
|
440
|
+
failures.push(`${p}: ${err.message}`);
|
|
426
441
|
}
|
|
427
|
-
catch { /* skip unreadable entry */ }
|
|
428
442
|
}
|
|
429
|
-
if (
|
|
430
|
-
|
|
443
|
+
if (swept.length === 0 && failures.length === 0)
|
|
444
|
+
return;
|
|
445
|
+
// Append before logging: the console line points at this file, so it had
|
|
446
|
+
// better already say what happened by the time anyone opens it.
|
|
447
|
+
try {
|
|
448
|
+
const stamp = new Date().toISOString();
|
|
449
|
+
const body = [
|
|
450
|
+
...swept.map(p => `${stamp} [startup] swept stale session dir: ${p}`),
|
|
451
|
+
...failures.map(l => `${stamp} [startup] could NOT sweep session dir ${l}`),
|
|
452
|
+
].join('\n');
|
|
453
|
+
fs.appendFileSync(RUNTIME_LOG_FILE, `${body}\n`);
|
|
454
|
+
}
|
|
455
|
+
catch { /* the sweep still happened; losing the record is not fatal */ }
|
|
456
|
+
if (swept.length > 0) {
|
|
457
|
+
const sample = swept.slice(0, SHOWN).map(p => path.basename(p));
|
|
458
|
+
const more = swept.length > SHOWN ? `, +${swept.length - SHOWN} more` : '';
|
|
459
|
+
console.log(`[startup] swept ${swept.length} session dir(s) unused for 7d; ${kept} kept`);
|
|
460
|
+
console.log(`[startup] ${sample.join(', ')}${more} — full list in ${RUNTIME_LOG_FILE}`);
|
|
461
|
+
}
|
|
462
|
+
if (failures.length > 0) {
|
|
463
|
+
console.error(`[startup] ${failures.length} stale session dir(s) could not be deleted — first: ${failures[0]}`);
|
|
431
464
|
}
|
|
432
465
|
}
|
|
433
466
|
/** How long a remote restart waits for live turns to finish before handing
|
package/dist/tui/run.d.ts
CHANGED
|
@@ -3,11 +3,38 @@ export declare function shouldRenderTui(env: {
|
|
|
3
3
|
stdinTTY: boolean;
|
|
4
4
|
noTui: boolean;
|
|
5
5
|
}): boolean;
|
|
6
|
+
/** Why the console gave the terminal back. Only `quit` is the user asking
|
|
7
|
+
* to stop; the rest are the terminal being taken away underneath us. */
|
|
8
|
+
export type LeaveReason = 'quit' | 'SIGHUP' | 'SIGTTIN' | 'SIGTTOU' | 'stdin-error';
|
|
9
|
+
/** What giving the terminal back should cost. */
|
|
10
|
+
export type LeaveAction =
|
|
11
|
+
/** Close the screen and go — nothing of ours is running here. */
|
|
12
|
+
'exit'
|
|
13
|
+
/** Drain the in-process daemon first, then go. */
|
|
14
|
+
| 'shutdown'
|
|
15
|
+
/** Close the screen and keep the daemon running headless. */
|
|
16
|
+
| 'detach';
|
|
17
|
+
/**
|
|
18
|
+
* The rule that decides whether this node survives the night.
|
|
19
|
+
*
|
|
20
|
+
* `ainode` runs the daemon in the SAME process as the dashboard, and every
|
|
21
|
+
* teardown used to end in process.exit(0). So a terminal that went away while
|
|
22
|
+
* nobody was watching — a closed window, an ssh drop, a stray read from a
|
|
23
|
+
* background process group — stopped the node, and it stayed stopped until
|
|
24
|
+
* someone re-ran `ainode` the next morning. Only `quit` is a person asking.
|
|
25
|
+
*
|
|
26
|
+
* Pure and exported so the rule is checkable without a TTY.
|
|
27
|
+
*/
|
|
28
|
+
export declare function leaveAction(reason: LeaveReason, ownsDaemon: boolean): LeaveAction;
|
|
6
29
|
export declare function runDashboard(opts: {
|
|
7
30
|
viewerMode: boolean;
|
|
8
31
|
pid: number | null;
|
|
9
32
|
startedAt: number | null;
|
|
10
33
|
version: string;
|
|
34
|
+
/** The in-process daemon's graceful shutdown, when this process owns one.
|
|
35
|
+
* Its presence is also how the screen knows quitting means stopping the
|
|
36
|
+
* node rather than closing a window onto someone else's. */
|
|
37
|
+
stop?: () => Promise<void>;
|
|
11
38
|
}): Promise<void>;
|
|
12
39
|
/** `ainode harnesses` — straight to the harness screen. */
|
|
13
40
|
export declare function runHarnessesTui(): Promise<void>;
|
package/dist/tui/run.js
CHANGED
|
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
39
39
|
})();
|
|
40
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
41
|
exports.shouldRenderTui = shouldRenderTui;
|
|
42
|
+
exports.leaveAction = leaveAction;
|
|
42
43
|
exports.runDashboard = runDashboard;
|
|
43
44
|
exports.runHarnessesTui = runHarnessesTui;
|
|
44
45
|
const readline = __importStar(require("readline"));
|
|
@@ -62,6 +63,22 @@ function shouldRenderTui(env) {
|
|
|
62
63
|
const TICK_MS = 250;
|
|
63
64
|
/** Spinner cadence. Cheap now that frames are painted as a diff. */
|
|
64
65
|
const SPIN_MS = 120;
|
|
66
|
+
/**
|
|
67
|
+
* The rule that decides whether this node survives the night.
|
|
68
|
+
*
|
|
69
|
+
* `ainode` runs the daemon in the SAME process as the dashboard, and every
|
|
70
|
+
* teardown used to end in process.exit(0). So a terminal that went away while
|
|
71
|
+
* nobody was watching — a closed window, an ssh drop, a stray read from a
|
|
72
|
+
* background process group — stopped the node, and it stayed stopped until
|
|
73
|
+
* someone re-ran `ainode` the next morning. Only `quit` is a person asking.
|
|
74
|
+
*
|
|
75
|
+
* Pure and exported so the rule is checkable without a TTY.
|
|
76
|
+
*/
|
|
77
|
+
function leaveAction(reason, ownsDaemon) {
|
|
78
|
+
if (!ownsDaemon)
|
|
79
|
+
return 'exit';
|
|
80
|
+
return reason === 'quit' ? 'shutdown' : 'detach';
|
|
81
|
+
}
|
|
65
82
|
/**
|
|
66
83
|
* Own the terminal on behalf of a screen stack.
|
|
67
84
|
*
|
|
@@ -69,7 +86,13 @@ const SPIN_MS = 120;
|
|
|
69
86
|
* screen can hand the TTY to a vendor CLI. It returns the root screen.
|
|
70
87
|
*/
|
|
71
88
|
function createConsole(build, hooks = {}) {
|
|
72
|
-
|
|
89
|
+
// A terminal that has gone away makes every write fail with EIO/EPIPE. In
|
|
90
|
+
// daemon mode the node is running in this very process, so a dead screen
|
|
91
|
+
// must never be the thing that takes it down.
|
|
92
|
+
const painter = (0, render_1.createPainter)(s => { try {
|
|
93
|
+
process.stdout.write(s);
|
|
94
|
+
}
|
|
95
|
+
catch { /* screen gone */ } });
|
|
73
96
|
let quitting = false;
|
|
74
97
|
let inputAttached = false;
|
|
75
98
|
// Building the root screen redraws, and that happens before run() has
|
|
@@ -82,7 +105,7 @@ function createConsole(build, hooks = {}) {
|
|
|
82
105
|
// silent: its timers keep firing otherwise and repaint straight over the
|
|
83
106
|
// child's output.
|
|
84
107
|
let suspended = false;
|
|
85
|
-
const app = (0, app_1.createApp)({ id: 'boot', title: '+Ai Node', render: () => [] }, { onQuit: () => teardown(), onRedraw: () => draw() });
|
|
108
|
+
const app = (0, app_1.createApp)({ id: 'boot', title: '+Ai Node', render: () => [] }, { onQuit: () => teardown('quit'), onRedraw: () => draw() });
|
|
86
109
|
function draw() {
|
|
87
110
|
if (!started || quitting || suspended)
|
|
88
111
|
return;
|
|
@@ -92,7 +115,7 @@ function createConsole(build, hooks = {}) {
|
|
|
92
115
|
void app.handleKey(key);
|
|
93
116
|
}
|
|
94
117
|
function onStdinError() {
|
|
95
|
-
teardown();
|
|
118
|
+
teardown('stdin-error');
|
|
96
119
|
}
|
|
97
120
|
function attachInput() {
|
|
98
121
|
if (inputAttached)
|
|
@@ -116,14 +139,24 @@ function createConsole(build, hooks = {}) {
|
|
|
116
139
|
return;
|
|
117
140
|
process.stdin.off('keypress', onKeypress);
|
|
118
141
|
process.stdin.off('error', onStdinError);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
// Putting a terminal back that no longer exists throws — and this runs
|
|
143
|
+
// from the very signal handler that means it no longer exists. Unguarded,
|
|
144
|
+
// that throw skips the rest of teardown, so onLeave never gets to say the
|
|
145
|
+
// node is still up and the reason is lost.
|
|
146
|
+
try {
|
|
147
|
+
if (process.stdin.isTTY)
|
|
148
|
+
process.stdin.setRawMode(false);
|
|
149
|
+
}
|
|
150
|
+
catch { /* gone */ }
|
|
151
|
+
try {
|
|
152
|
+
process.stdin.pause();
|
|
153
|
+
}
|
|
154
|
+
catch { /* gone */ }
|
|
122
155
|
inputAttached = false;
|
|
123
156
|
}
|
|
124
157
|
let pollTimer = null;
|
|
125
158
|
let spinTimer = null;
|
|
126
|
-
function teardown() {
|
|
159
|
+
function teardown(reason) {
|
|
127
160
|
if (quitting)
|
|
128
161
|
return;
|
|
129
162
|
quitting = true;
|
|
@@ -132,9 +165,14 @@ function createConsole(build, hooks = {}) {
|
|
|
132
165
|
if (spinTimer)
|
|
133
166
|
clearInterval(spinTimer);
|
|
134
167
|
detachInput();
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
168
|
+
try {
|
|
169
|
+
(0, render_1.altOff)();
|
|
170
|
+
}
|
|
171
|
+
catch { /* the terminal is already gone */ }
|
|
172
|
+
if (hooks.onLeave)
|
|
173
|
+
hooks.onLeave(reason);
|
|
174
|
+
else
|
|
175
|
+
process.exit(0);
|
|
138
176
|
}
|
|
139
177
|
const suspend = async (fn) => {
|
|
140
178
|
suspended = true;
|
|
@@ -157,12 +195,21 @@ function createConsole(build, hooks = {}) {
|
|
|
157
195
|
// background process group earns SIGTTIN, and the default action is to STOP
|
|
158
196
|
// the process — which is what "zsh: suspended (tty input)" is. Stopped mid-
|
|
159
197
|
// draw it still holds the terminal in raw mode, so the session it was
|
|
160
|
-
// sharing degrades too. Stop reading and leave instead
|
|
161
|
-
//
|
|
162
|
-
process
|
|
163
|
-
|
|
198
|
+
// sharing degrades too. Stop reading and leave instead.
|
|
199
|
+
//
|
|
200
|
+
// Leaving is NOT exiting: under `ainode` the daemon IS this process, so what
|
|
201
|
+
// the screen does when it loses its terminal decides whether the node
|
|
202
|
+
// survives the night. onLeave settles that — see runDashboard.
|
|
203
|
+
process.on('SIGTTIN', () => teardown('SIGTTIN'));
|
|
204
|
+
process.on('SIGTTOU', () => teardown('SIGTTOU'));
|
|
164
205
|
// The terminal went away without the read failing first.
|
|
165
|
-
process.on('SIGHUP', teardown);
|
|
206
|
+
process.on('SIGHUP', () => teardown('SIGHUP'));
|
|
207
|
+
// An async write to a dead terminal surfaces as an 'error' event, and an
|
|
208
|
+
// 'error' with no listener is a throw. Same reasoning as the write guard.
|
|
209
|
+
process.stdout.on('error', () => { });
|
|
210
|
+
// Same for stdin, which keeps its listener past detachInput's removal so a
|
|
211
|
+
// late EIO on a torn-down terminal has somewhere to land.
|
|
212
|
+
process.stdin.on('error', () => { });
|
|
166
213
|
app.replace(build(app, suspend));
|
|
167
214
|
return {
|
|
168
215
|
host: app,
|
|
@@ -205,6 +252,9 @@ function createConsole(build, hooks = {}) {
|
|
|
205
252
|
},
|
|
206
253
|
};
|
|
207
254
|
}
|
|
255
|
+
/** How long a deliberate quit waits for the daemon to drain before it stops
|
|
256
|
+
* being a courtesy and starts being a hung terminal. */
|
|
257
|
+
const QUIT_DRAIN_CEILING_MS = 20_000;
|
|
208
258
|
async function runDashboard(opts) {
|
|
209
259
|
const pairing = (0, store_1.readPairing)();
|
|
210
260
|
const data = (0, data_1.createDataLayer)({ token: pairing?.daemonToken ?? '' });
|
|
@@ -240,11 +290,38 @@ async function runDashboard(opts) {
|
|
|
240
290
|
})),
|
|
241
291
|
});
|
|
242
292
|
}, {
|
|
243
|
-
|
|
293
|
+
// The screen and the daemon used to share one fate: every teardown ended
|
|
294
|
+
// in process.exit(0), so a terminal that went away overnight — SIGHUP from
|
|
295
|
+
// a closed window, an ssh drop, a stray background read — took the node
|
|
296
|
+
// down with it and left it dead until someone re-ran `ainode` in the
|
|
297
|
+
// morning. Losing a screen is not a reason to stop working.
|
|
298
|
+
onLeave: (reason) => {
|
|
299
|
+
const stop = opts.stop;
|
|
300
|
+
const action = leaveAction(reason, !opts.viewerMode && Boolean(stop));
|
|
301
|
+
if (action === 'detach') {
|
|
302
|
+
// The terminal is gone, the daemon is not. Keep capturing rather than
|
|
303
|
+
// restoring: the real console writes to an fd that no longer exists,
|
|
304
|
+
// whereas the capture keeps appending to the daemon log — which is now
|
|
305
|
+
// the only place anyone can watch this node from.
|
|
306
|
+
console.log(`[tui] terminal gone (${reason}) — dashboard closed; the node is still running (pid ${process.pid})`);
|
|
307
|
+
console.log(`[tui] follow it with: tail -f ${paths_1.RUNTIME_LOG_FILE}`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
244
310
|
logs.restore();
|
|
245
311
|
if (logs.count()) {
|
|
246
312
|
console.log(`${logs.count()} daemon log lines this session — ${paths_1.RUNTIME_LOG_FILE}`);
|
|
247
313
|
}
|
|
314
|
+
if (action === 'exit' || !stop) {
|
|
315
|
+
process.exit(0);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
// Asked for, so drain properly: in-flight turns get to post their final
|
|
319
|
+
// status and the server is told we are going, rather than discovering it
|
|
320
|
+
// 90s later through a missed heartbeat. The ceiling is there because a
|
|
321
|
+
// wedged drain must not become a terminal that never comes back.
|
|
322
|
+
console.log('stopping the node…');
|
|
323
|
+
setTimeout(() => process.exit(0), QUIT_DRAIN_CEILING_MS).unref?.();
|
|
324
|
+
void stop().finally(() => process.exit(0));
|
|
248
325
|
},
|
|
249
326
|
});
|
|
250
327
|
// The daemon's own numbers don't come from an RPC.
|
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 (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
}
|
|
@@ -278,6 +291,26 @@ function resolveNpmShimScript(shimPath) {
|
|
|
278
291
|
}
|
|
279
292
|
return null;
|
|
280
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
|
+
}
|
|
281
314
|
/** How long a POSIX agent gets to honour SIGINT before we stop asking. */
|
|
282
315
|
const KILL_ESCALATE_MS = 3000;
|
|
283
316
|
/** One `ps` snapshot as (pid, ppid) pairs. Empty on any failure — callers
|
|
@@ -466,4 +499,4 @@ function linkDir(real, dest) {
|
|
|
466
499
|
}
|
|
467
500
|
/** Exposed for tests only: the shim parsing is pure file-shape logic and is
|
|
468
501
|
* worth exercising on any platform, not just on a Windows box. */
|
|
469
|
-
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.
|
|
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.10",
|
|
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
|
+
}
|