@addai/node 0.30.9 → 0.30.11
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/claude-print.js +11 -6
- package/dist/claude-system-prompt.d.ts +12 -1
- package/dist/claude-system-prompt.js +49 -5
- package/dist/cli.js +4 -0
- package/dist/codex-spawn.d.ts +13 -0
- package/dist/codex-spawn.js +40 -2
- package/dist/context-files.d.ts +29 -0
- package/dist/context-files.js +150 -0
- package/dist/gemini-spawn.js +5 -0
- package/dist/grok-spawn.js +5 -0
- package/dist/index.js +52 -19
- package/dist/kimi-spawn.js +5 -0
- package/dist/prompt-transport.d.ts +47 -0
- package/dist/prompt-transport.js +121 -0
- package/dist/session-runner.d.ts +2 -0
- package/dist/session-runner.js +32 -9
- package/dist/tui/run.d.ts +18 -0
- package/dist/tui/run.js +93 -16
- package/package.json +1 -1
package/dist/claude-print.js
CHANGED
|
@@ -12,6 +12,7 @@ const child_process_1 = require("child_process");
|
|
|
12
12
|
const claude_binary_1 = require("./claude-binary");
|
|
13
13
|
const win_1 = require("./win");
|
|
14
14
|
const claude_system_prompt_1 = require("./claude-system-prompt");
|
|
15
|
+
const prompt_transport_1 = require("./prompt-transport");
|
|
15
16
|
/**
|
|
16
17
|
* The text to emit for one flushed block, given what came before.
|
|
17
18
|
*
|
|
@@ -175,17 +176,20 @@ function spawnClaudePrint(input) {
|
|
|
175
176
|
// its node script instead (identity on POSIX). It can throw; so can
|
|
176
177
|
// spawn() itself (synchronously, on win32). Surface either as a normal
|
|
177
178
|
// rejected `done` so callers keep their single error path.
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
179
|
+
// One path, every run: the system prompt travels as a file and the user
|
|
180
|
+
// prompt down stdin, whatever their size. Size-dependent branches are where
|
|
181
|
+
// the ENAMETOOLONG failures hid — see prompt-transport.ts. The file form is
|
|
182
|
+
// gated only on this claude knowing the flag (probed once per binary).
|
|
183
|
+
const isWindows = process.platform === 'win32';
|
|
184
|
+
const sysPrompt = (0, claude_system_prompt_1.offloadLongSystemPrompt)(bin, args, isWindows, (0, claude_system_prompt_1.claudeSupportsPromptFile)(bin));
|
|
185
|
+
const transport = (0, prompt_transport_1.offloadLongPrompt)(bin, sysPrompt.args, sysPrompt.args.length - 1, null, isWindows, true);
|
|
182
186
|
let proc;
|
|
183
187
|
try {
|
|
184
|
-
const inv = (0, win_1.resolveCliInvocation)(bin,
|
|
188
|
+
const inv = (0, win_1.resolveCliInvocation)(bin, transport.args);
|
|
185
189
|
proc = (0, child_process_1.spawn)(inv.file, inv.args, {
|
|
186
190
|
cwd: input.workingDirectory,
|
|
187
191
|
env: { ...process.env, TERM: 'dumb' },
|
|
188
|
-
stdio:
|
|
192
|
+
stdio: (0, prompt_transport_1.stdioFor)(transport.stdin),
|
|
189
193
|
// No console window. This is headless work on somebody's machine —
|
|
190
194
|
// on Windows a spawn without this flashes a terminal every run.
|
|
191
195
|
windowsHide: true,
|
|
@@ -199,6 +203,7 @@ function spawnClaudePrint(input) {
|
|
|
199
203
|
done.catch(() => { });
|
|
200
204
|
return { pid: undefined, sessionId: null, kill: () => { }, onEvent: () => { }, done };
|
|
201
205
|
}
|
|
206
|
+
(0, prompt_transport_1.feedStdin)(proc, transport.stdin);
|
|
202
207
|
const listeners = [];
|
|
203
208
|
const emit = (e) => { for (const l of listeners)
|
|
204
209
|
try {
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/** Head-room under the real 32,767 limit. Both node and node-pty escape quotes
|
|
2
|
+
* and backslashes when they render argv into a command line, so what actually
|
|
3
|
+
* reaches CreateProcess is longer than the sum of the arguments. */
|
|
4
|
+
export declare const WINDOWS_BUDGET = 30000;
|
|
5
|
+
/** POSIX only needs catching if a prompt has gone genuinely pathological. */
|
|
6
|
+
export declare const POSIX_BUDGET = 120000;
|
|
1
7
|
/** Rough rendered length of `file` + `args` as one command line. Counts two
|
|
2
8
|
* quotes and a separating space per argument — the shape a renderer uses for
|
|
3
9
|
* anything that might contain whitespace. */
|
|
@@ -15,4 +21,9 @@ export interface OffloadResult {
|
|
|
15
21
|
* the temp file cannot be written — in that last case the spawn is left to
|
|
16
22
|
* fail with its own error rather than a worse one invented here.
|
|
17
23
|
*/
|
|
18
|
-
export declare function offloadLongSystemPrompt(file: string, args: string[], isWindows?: boolean
|
|
24
|
+
export declare function offloadLongSystemPrompt(file: string, args: string[], isWindows?: boolean,
|
|
25
|
+
/** Move it regardless of size — the one-path rule (see prompt-transport.ts).
|
|
26
|
+
* Callers pass claudeSupportsPromptFile(bin) so an older claude that does
|
|
27
|
+
* not know the -file flag keeps the inline form. */
|
|
28
|
+
force?: boolean): OffloadResult;
|
|
29
|
+
export declare function claudeSupportsPromptFile(bin: string, probe?: (bin: string) => string): boolean;
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
// --append-system-prompt-file. ONLY when it would not fit: the inline form is
|
|
15
15
|
// what every working install uses today and an older claude may not know the
|
|
16
16
|
// -file variant, so nothing that currently works changes shape.
|
|
17
|
+
//
|
|
18
|
+
// This only covers the system prompt. The USER prompt can be too long on its
|
|
19
|
+
// own (a fresh session's context refresh carries the recent conversation);
|
|
20
|
+
// prompt-transport.ts moves that one onto stdin, after this has run.
|
|
17
21
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
22
|
if (k2 === undefined) k2 = k;
|
|
19
23
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -48,20 +52,24 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
48
52
|
};
|
|
49
53
|
})();
|
|
50
54
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
+
exports.POSIX_BUDGET = exports.WINDOWS_BUDGET = void 0;
|
|
51
56
|
exports.commandLineLength = commandLineLength;
|
|
52
57
|
exports.offloadLongSystemPrompt = offloadLongSystemPrompt;
|
|
58
|
+
exports.claudeSupportsPromptFile = claudeSupportsPromptFile;
|
|
53
59
|
const fs = __importStar(require("fs"));
|
|
54
60
|
const os = __importStar(require("os"));
|
|
55
61
|
const path = __importStar(require("path"));
|
|
56
62
|
const crypto_1 = require("crypto");
|
|
63
|
+
const child_process_1 = require("child_process");
|
|
64
|
+
const win_1 = require("./win");
|
|
57
65
|
const INLINE_FLAG = '--append-system-prompt';
|
|
58
66
|
const FILE_FLAG = '--append-system-prompt-file';
|
|
59
67
|
/** Head-room under the real 32,767 limit. Both node and node-pty escape quotes
|
|
60
68
|
* and backslashes when they render argv into a command line, so what actually
|
|
61
69
|
* reaches CreateProcess is longer than the sum of the arguments. */
|
|
62
|
-
|
|
70
|
+
exports.WINDOWS_BUDGET = 30_000;
|
|
63
71
|
/** POSIX only needs catching if a prompt has gone genuinely pathological. */
|
|
64
|
-
|
|
72
|
+
exports.POSIX_BUDGET = 120_000;
|
|
65
73
|
/** Rough rendered length of `file` + `args` as one command line. Counts two
|
|
66
74
|
* quotes and a separating space per argument — the shape a renderer uses for
|
|
67
75
|
* anything that might contain whitespace. */
|
|
@@ -76,10 +84,14 @@ function commandLineLength(file, args) {
|
|
|
76
84
|
* the temp file cannot be written — in that last case the spawn is left to
|
|
77
85
|
* fail with its own error rather than a worse one invented here.
|
|
78
86
|
*/
|
|
79
|
-
function offloadLongSystemPrompt(file, args, isWindows = process.platform === 'win32'
|
|
87
|
+
function offloadLongSystemPrompt(file, args, isWindows = process.platform === 'win32',
|
|
88
|
+
/** Move it regardless of size — the one-path rule (see prompt-transport.ts).
|
|
89
|
+
* Callers pass claudeSupportsPromptFile(bin) so an older claude that does
|
|
90
|
+
* not know the -file flag keeps the inline form. */
|
|
91
|
+
force = false) {
|
|
80
92
|
const noop = { args, cleanup: () => { } };
|
|
81
|
-
const budget = isWindows ? WINDOWS_BUDGET : POSIX_BUDGET;
|
|
82
|
-
if (commandLineLength(file, args) <= budget)
|
|
93
|
+
const budget = isWindows ? exports.WINDOWS_BUDGET : exports.POSIX_BUDGET;
|
|
94
|
+
if (!force && commandLineLength(file, args) <= budget)
|
|
83
95
|
return noop;
|
|
84
96
|
const i = args.indexOf(INLINE_FLAG);
|
|
85
97
|
if (i < 0 || i + 1 >= args.length)
|
|
@@ -108,3 +120,35 @@ function offloadLongSystemPrompt(file, args, isWindows = process.platform === 'w
|
|
|
108
120
|
},
|
|
109
121
|
};
|
|
110
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Does this claude know --append-system-prompt-file? Asked once per binary
|
|
125
|
+
* and remembered for the life of the daemon. The answer decides whether the
|
|
126
|
+
* system prompt ALWAYS travels as a file — one path, exercised by every run —
|
|
127
|
+
* or only when it would not fit inline, the old rare path kept for a claude
|
|
128
|
+
* too old to know the flag. A probe that fails answers "no", which is the
|
|
129
|
+
* behaviour every working install had before.
|
|
130
|
+
*/
|
|
131
|
+
const promptFileSupport = new Map();
|
|
132
|
+
function claudeSupportsPromptFile(bin, probe = claudeHelpText) {
|
|
133
|
+
const cached = promptFileSupport.get(bin);
|
|
134
|
+
if (cached !== undefined)
|
|
135
|
+
return cached;
|
|
136
|
+
let ok = false;
|
|
137
|
+
// claude's own help lists the pair as `--append-system-prompt[-file]`, so a
|
|
138
|
+
// literal search for the long flag would say no to a claude that knows it.
|
|
139
|
+
try {
|
|
140
|
+
const help = probe(bin);
|
|
141
|
+
ok = help.includes(FILE_FLAG) || help.includes('--append-system-prompt[-file]');
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
ok = false;
|
|
145
|
+
}
|
|
146
|
+
promptFileSupport.set(bin, ok);
|
|
147
|
+
return ok;
|
|
148
|
+
}
|
|
149
|
+
function claudeHelpText(bin) {
|
|
150
|
+
const inv = (0, win_1.resolveCliInvocation)(bin, ['--help']);
|
|
151
|
+
return (0, child_process_1.execFileSync)(inv.file, inv.args, {
|
|
152
|
+
encoding: 'utf8', timeout: 20_000, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
153
|
+
});
|
|
154
|
+
}
|
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
|
}
|
package/dist/codex-spawn.d.ts
CHANGED
|
@@ -33,6 +33,19 @@ export declare function writeCodexHome(servers: CodexMcpServer[], workingDirecto
|
|
|
33
33
|
home: string;
|
|
34
34
|
extraEnv: Record<string, string>;
|
|
35
35
|
};
|
|
36
|
+
/** First line of an AGENTS.md this daemon wrote. A project's own AGENTS.md
|
|
37
|
+
* never carries it and is never touched. */
|
|
38
|
+
export declare const AGENTS_MD_MARKER = "<!-- ainode: entity system prompt \u2014 rewritten before every turn; edits here do not persist -->";
|
|
39
|
+
/**
|
|
40
|
+
* Put the entity's system prompt where codex natively looks for guidance:
|
|
41
|
+
* AGENTS.md in the working directory. codex has no --append-system-prompt,
|
|
42
|
+
* and gluing 30K characters of persona onto every user turn was the
|
|
43
|
+
* alternative. Returns false — and writes nothing — when the folder already
|
|
44
|
+
* has an AGENTS.md that is not ours, so a chat bound to a real repository
|
|
45
|
+
* can never have its project file overwritten; the caller then falls back to
|
|
46
|
+
* the glued form.
|
|
47
|
+
*/
|
|
48
|
+
export declare function writeCodexAgentsMd(workingDirectory: string, systemPrompt: string): boolean;
|
|
36
49
|
export interface CodexHandle {
|
|
37
50
|
pid: number | undefined;
|
|
38
51
|
threadId: string | null;
|
package/dist/codex-spawn.js
CHANGED
|
@@ -40,7 +40,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
40
40
|
};
|
|
41
41
|
})();
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.AGENTS_MD_MARKER = void 0;
|
|
43
44
|
exports.writeCodexHome = writeCodexHome;
|
|
45
|
+
exports.writeCodexAgentsMd = writeCodexAgentsMd;
|
|
44
46
|
exports.codexEffort = codexEffort;
|
|
45
47
|
exports.spawnCodex = spawnCodex;
|
|
46
48
|
const child_process_1 = require("child_process");
|
|
@@ -52,6 +54,7 @@ const codex_binary_1 = require("./codex-binary");
|
|
|
52
54
|
const diskguard_1 = require("./diskguard");
|
|
53
55
|
const win_1 = require("./win");
|
|
54
56
|
const events_1 = require("./events");
|
|
57
|
+
const prompt_transport_1 = require("./prompt-transport");
|
|
55
58
|
/** Quote a string as a TOML basic string (double-quoted, escape backslash + quote). */
|
|
56
59
|
function tomlString(s) {
|
|
57
60
|
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
|
|
@@ -156,6 +159,33 @@ function writeCodexHome(servers, workingDirectory) {
|
|
|
156
159
|
}
|
|
157
160
|
return { home: dir, extraEnv };
|
|
158
161
|
}
|
|
162
|
+
/** First line of an AGENTS.md this daemon wrote. A project's own AGENTS.md
|
|
163
|
+
* never carries it and is never touched. */
|
|
164
|
+
exports.AGENTS_MD_MARKER = '<!-- ainode: entity system prompt — rewritten before every turn; edits here do not persist -->';
|
|
165
|
+
/**
|
|
166
|
+
* Put the entity's system prompt where codex natively looks for guidance:
|
|
167
|
+
* AGENTS.md in the working directory. codex has no --append-system-prompt,
|
|
168
|
+
* and gluing 30K characters of persona onto every user turn was the
|
|
169
|
+
* alternative. Returns false — and writes nothing — when the folder already
|
|
170
|
+
* has an AGENTS.md that is not ours, so a chat bound to a real repository
|
|
171
|
+
* can never have its project file overwritten; the caller then falls back to
|
|
172
|
+
* the glued form.
|
|
173
|
+
*/
|
|
174
|
+
function writeCodexAgentsMd(workingDirectory, systemPrompt) {
|
|
175
|
+
const target = path.join(workingDirectory, 'AGENTS.md');
|
|
176
|
+
try {
|
|
177
|
+
if (fs.existsSync(target)) {
|
|
178
|
+
const head = fs.readFileSync(target, 'utf8').slice(0, exports.AGENTS_MD_MARKER.length + 16);
|
|
179
|
+
if (!head.includes(exports.AGENTS_MD_MARKER))
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
fs.writeFileSync(target, `${exports.AGENTS_MD_MARKER}\n\n${systemPrompt.trim()}\n`, 'utf8');
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
159
189
|
function lineToEvents(line) {
|
|
160
190
|
const type = typeof line.type === 'string' ? line.type : '';
|
|
161
191
|
// Errors come as { type: 'error', message: string } — surface them.
|
|
@@ -221,6 +251,10 @@ function spawnCodex(input) {
|
|
|
221
251
|
// `exec` command, before the `resume` subcommand. Putting them after
|
|
222
252
|
// resume returns "unexpected argument" and exits 2.
|
|
223
253
|
const baseFlags = ['--cd', input.workingDirectory, '--json'];
|
|
254
|
+
// AGENTS.md in the working directory carries the entity's system prompt (see
|
|
255
|
+
// writeCodexAgentsMd). codex truncates project docs at 32 KiB by default,
|
|
256
|
+
// which a persona plus its app instructions routinely exceeds.
|
|
257
|
+
baseFlags.push('-c', 'project_doc_max_bytes=262144');
|
|
224
258
|
if (input.skipGitRepoCheck)
|
|
225
259
|
baseFlags.push('--skip-git-repo-check');
|
|
226
260
|
if (input.bypassPermissions)
|
|
@@ -292,15 +326,18 @@ function spawnCodex(input) {
|
|
|
292
326
|
kill() { },
|
|
293
327
|
};
|
|
294
328
|
}
|
|
329
|
+
// One path, every run: the prompt goes down stdin (`codex exec -`, fresh and
|
|
330
|
+
// resume forms alike) whatever its size. See prompt-transport.ts.
|
|
331
|
+
const transport = (0, prompt_transport_1.offloadLongPrompt)(bin, args, args.length - 1, '-', process.platform === 'win32', true);
|
|
295
332
|
let proc;
|
|
296
333
|
try {
|
|
297
334
|
// npm .cmd shim → node script (win32); identity on POSIX. Throws on an
|
|
298
335
|
// unresolvable shim — caught below alongside spawn's own sync errors.
|
|
299
|
-
const inv = (0, win_1.resolveCliInvocation)(bin, args);
|
|
336
|
+
const inv = (0, win_1.resolveCliInvocation)(bin, transport.args);
|
|
300
337
|
proc = (0, child_process_1.spawn)(inv.file, inv.args, {
|
|
301
338
|
cwd: input.workingDirectory,
|
|
302
339
|
env: childEnv,
|
|
303
|
-
stdio:
|
|
340
|
+
stdio: (0, prompt_transport_1.stdioFor)(transport.stdin),
|
|
304
341
|
// No console window. This is headless work on somebody's machine —
|
|
305
342
|
// on Windows a spawn without this flashes a terminal every run.
|
|
306
343
|
windowsHide: true,
|
|
@@ -322,6 +359,7 @@ function spawnCodex(input) {
|
|
|
322
359
|
kill() { },
|
|
323
360
|
};
|
|
324
361
|
}
|
|
362
|
+
(0, prompt_transport_1.feedStdin)(proc, transport.stdin);
|
|
325
363
|
// Models do not reliably keep reasoning on their own stream — they drop
|
|
326
364
|
// back to ordinary text mid-run and carry on thinking inside a <think>
|
|
327
365
|
// tag. Split it back out before it reaches the room. See think-split.ts.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** The one folder we write into. */
|
|
2
|
+
export declare const CONTEXT_DIR = "context";
|
|
3
|
+
export interface ContextFilesResult {
|
|
4
|
+
/** Relative paths written, in the order they arrived. */
|
|
5
|
+
written: string[];
|
|
6
|
+
/** Relative paths refused or failed, with the reason. */
|
|
7
|
+
skipped: Array<{
|
|
8
|
+
path: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Validate `files` as a {relative path: markdown} object. Pure; exported for tests.
|
|
14
|
+
* Returns the entries that may be written and the ones that were refused.
|
|
15
|
+
*/
|
|
16
|
+
export declare function planContextFiles(files: unknown): {
|
|
17
|
+
ok: Array<[string, string]>;
|
|
18
|
+
refused: Array<{
|
|
19
|
+
path: string;
|
|
20
|
+
reason: string;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Write the context files into `cwd`. Never throws.
|
|
25
|
+
*
|
|
26
|
+
* Existing files are replaced whole: each turn carries the current picture of
|
|
27
|
+
* the conversation, and a stale conversation.md is worse than none.
|
|
28
|
+
*/
|
|
29
|
+
export declare function writeContextFiles(cwd: string, files: unknown): ContextFilesResult;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Context files: depth the entity can read when it needs it.
|
|
3
|
+
//
|
|
4
|
+
// The dispatcher keeps the inline briefing small — who, where, what has
|
|
5
|
+
// happened here since the last turn — and puts the deeper material into a few
|
|
6
|
+
// markdown files under context/ in the run's working folder: the last 50
|
|
7
|
+
// messages of the conversation, a card about the sender, a card about the
|
|
8
|
+
// chat. They cost nothing until the entity opens one, and every agent CLI can
|
|
9
|
+
// read a local file without a network round trip.
|
|
10
|
+
//
|
|
11
|
+
// The files arrive on the request row as `context_files`: a JSON object of
|
|
12
|
+
// relative path → markdown. Only plain names under context/ are accepted, so a
|
|
13
|
+
// row can never write outside the folder or clobber the agent's own files.
|
|
14
|
+
// The folder is marked as ours on creation (context/.ainode) and ignored by
|
|
15
|
+
// git (context/.gitignore): a chat bound to a real project directory must
|
|
16
|
+
// never gain stray files in the repo, and a project that already HAS a
|
|
17
|
+
// context/ folder of its own is left completely alone.
|
|
18
|
+
//
|
|
19
|
+
// Everything here is best-effort: the inline briefing is complete on its own,
|
|
20
|
+
// so a write failure must never block a turn — it just means the pointer in
|
|
21
|
+
// the prompt leads to a missing file, which the agent reports and moves on.
|
|
22
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
25
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
26
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(o, k2, desc);
|
|
29
|
+
}) : (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
o[k2] = m[k];
|
|
32
|
+
}));
|
|
33
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
34
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
35
|
+
}) : function(o, v) {
|
|
36
|
+
o["default"] = v;
|
|
37
|
+
});
|
|
38
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
39
|
+
var ownKeys = function(o) {
|
|
40
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
41
|
+
var ar = [];
|
|
42
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
43
|
+
return ar;
|
|
44
|
+
};
|
|
45
|
+
return ownKeys(o);
|
|
46
|
+
};
|
|
47
|
+
return function (mod) {
|
|
48
|
+
if (mod && mod.__esModule) return mod;
|
|
49
|
+
var result = {};
|
|
50
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
51
|
+
__setModuleDefault(result, mod);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
55
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
|
+
exports.CONTEXT_DIR = void 0;
|
|
57
|
+
exports.planContextFiles = planContextFiles;
|
|
58
|
+
exports.writeContextFiles = writeContextFiles;
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const path = __importStar(require("path"));
|
|
61
|
+
/** The one folder we write into. */
|
|
62
|
+
exports.CONTEXT_DIR = 'context';
|
|
63
|
+
/** Present only in a context/ folder this daemon created. */
|
|
64
|
+
const OWNER_MARKER = '.ainode';
|
|
65
|
+
/** Relative paths we will write: context/<name>.md, one level, safe characters only. */
|
|
66
|
+
const ALLOWED = /^context\/[A-Za-z0-9][A-Za-z0-9._-]{0,80}\.md$/;
|
|
67
|
+
/** No single file beyond this; the dispatcher caps well under it. */
|
|
68
|
+
const MAX_FILE_CHARS = 200_000;
|
|
69
|
+
/**
|
|
70
|
+
* Validate `files` as a {relative path: markdown} object. Pure; exported for tests.
|
|
71
|
+
* Returns the entries that may be written and the ones that were refused.
|
|
72
|
+
*/
|
|
73
|
+
function planContextFiles(files) {
|
|
74
|
+
const ok = [];
|
|
75
|
+
const refused = [];
|
|
76
|
+
if (!files || typeof files !== 'object' || Array.isArray(files))
|
|
77
|
+
return { ok, refused };
|
|
78
|
+
for (const [rel, body] of Object.entries(files)) {
|
|
79
|
+
if (!ALLOWED.test(rel)) {
|
|
80
|
+
refused.push({ path: rel, reason: 'path not allowed' });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (typeof body !== 'string') {
|
|
84
|
+
refused.push({ path: rel, reason: 'content is not text' });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (body.length > MAX_FILE_CHARS) {
|
|
88
|
+
refused.push({ path: rel, reason: `over ${MAX_FILE_CHARS} chars` });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
ok.push([rel, body]);
|
|
92
|
+
}
|
|
93
|
+
return { ok, refused };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Make sure `<cwd>/context/` exists and is ours. Returns a reason to refuse,
|
|
97
|
+
* or null when it is safe to write there.
|
|
98
|
+
*/
|
|
99
|
+
function claimContextDir(cwd) {
|
|
100
|
+
const dir = path.join(cwd, exports.CONTEXT_DIR);
|
|
101
|
+
const marker = path.join(dir, OWNER_MARKER);
|
|
102
|
+
try {
|
|
103
|
+
if (fs.existsSync(dir)) {
|
|
104
|
+
if (!fs.statSync(dir).isDirectory())
|
|
105
|
+
return `${exports.CONTEXT_DIR} exists and is not a directory`;
|
|
106
|
+
if (!fs.existsSync(marker))
|
|
107
|
+
return `${exports.CONTEXT_DIR}/ already exists and was not created by the node`;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
111
|
+
fs.writeFileSync(marker, 'This folder is written by the +Ai Node before every turn. Edits here do not persist.\n');
|
|
112
|
+
fs.writeFileSync(path.join(dir, '.gitignore'), '*\n');
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
return err.message;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Write the context files into `cwd`. Never throws.
|
|
121
|
+
*
|
|
122
|
+
* Existing files are replaced whole: each turn carries the current picture of
|
|
123
|
+
* the conversation, and a stale conversation.md is worse than none.
|
|
124
|
+
*/
|
|
125
|
+
function writeContextFiles(cwd, files) {
|
|
126
|
+
const plan = planContextFiles(files);
|
|
127
|
+
const result = { written: [], skipped: [...plan.refused] };
|
|
128
|
+
if (plan.ok.length === 0)
|
|
129
|
+
return result;
|
|
130
|
+
const refusal = claimContextDir(cwd);
|
|
131
|
+
if (refusal) {
|
|
132
|
+
for (const [rel] of plan.ok)
|
|
133
|
+
result.skipped.push({ path: rel, reason: refusal });
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
for (const [rel, body] of plan.ok) {
|
|
137
|
+
const abs = path.join(cwd, rel);
|
|
138
|
+
try {
|
|
139
|
+
// Write beside, then rename: a reader never sees a half-written file.
|
|
140
|
+
const tmp = `${abs}.${process.pid}.tmp`;
|
|
141
|
+
fs.writeFileSync(tmp, body, 'utf8');
|
|
142
|
+
fs.renameSync(tmp, abs);
|
|
143
|
+
result.written.push(rel);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
result.skipped.push({ path: rel, reason: err.message });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
package/dist/gemini-spawn.js
CHANGED
|
@@ -54,6 +54,7 @@ const os = __importStar(require("os"));
|
|
|
54
54
|
const path = __importStar(require("path"));
|
|
55
55
|
const gemini_binary_1 = require("./gemini-binary");
|
|
56
56
|
const win_1 = require("./win");
|
|
57
|
+
const prompt_transport_1 = require("./prompt-transport");
|
|
57
58
|
const events_1 = require("./events");
|
|
58
59
|
const think_split_1 = require("./think-split");
|
|
59
60
|
const mcp_headers_1 = require("./mcp-headers");
|
|
@@ -352,6 +353,10 @@ function spawnGemini(input) {
|
|
|
352
353
|
// npm .cmd shim → node script (win32); identity on POSIX. Both this
|
|
353
354
|
// resolution and spawn() itself can throw synchronously on Windows —
|
|
354
355
|
// fail the request cleanly instead of crashing the daemon.
|
|
356
|
+
// Not yet verified to read the prompt from stdin, so it stays inline — and
|
|
357
|
+
// an oversized command line is refused here with a reason, instead of by
|
|
358
|
+
// the OS with a bare errno. See prompt-transport.ts.
|
|
359
|
+
(0, prompt_transport_1.assertCommandLineFits)('gemini', bin, args);
|
|
355
360
|
const inv = (0, win_1.resolveCliInvocation)(bin, args);
|
|
356
361
|
proc = (0, child_process_1.spawn)(inv.file, inv.args, {
|
|
357
362
|
cwd: input.workingDirectory,
|
package/dist/grok-spawn.js
CHANGED
|
@@ -72,6 +72,7 @@ const os = __importStar(require("os"));
|
|
|
72
72
|
const path = __importStar(require("path"));
|
|
73
73
|
const grok_binary_1 = require("./grok-binary");
|
|
74
74
|
const win_1 = require("./win");
|
|
75
|
+
const prompt_transport_1 = require("./prompt-transport");
|
|
75
76
|
const events_1 = require("./events");
|
|
76
77
|
const think_split_1 = require("./think-split");
|
|
77
78
|
/** Real config dir for the logged-in account. GROK_HOME points here so the
|
|
@@ -344,6 +345,10 @@ function spawnGrok(input) {
|
|
|
344
345
|
// npm .cmd shim → node script (win32); identity on POSIX. Both this
|
|
345
346
|
// resolution and spawn() itself can throw synchronously on Windows —
|
|
346
347
|
// fail the request cleanly instead of crashing the daemon.
|
|
348
|
+
// Not yet verified to read the prompt from stdin, so it stays inline — and
|
|
349
|
+
// an oversized command line is refused here with a reason, instead of by
|
|
350
|
+
// the OS with a bare errno. See prompt-transport.ts.
|
|
351
|
+
(0, prompt_transport_1.assertCommandLineFits)('grok', bin, args);
|
|
347
352
|
const inv = (0, win_1.resolveCliInvocation)(bin, args);
|
|
348
353
|
proc = (0, child_process_1.spawn)(inv.file, inv.args, {
|
|
349
354
|
cwd: input.workingDirectory,
|
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/kimi-spawn.js
CHANGED
|
@@ -70,6 +70,7 @@ const path = __importStar(require("path"));
|
|
|
70
70
|
const kimi_binary_1 = require("./kimi-binary");
|
|
71
71
|
const harness_registry_1 = require("./harness-registry");
|
|
72
72
|
const win_1 = require("./win");
|
|
73
|
+
const prompt_transport_1 = require("./prompt-transport");
|
|
73
74
|
const events_1 = require("./events");
|
|
74
75
|
const think_split_1 = require("./think-split");
|
|
75
76
|
const mcp_headers_1 = require("./mcp-headers");
|
|
@@ -385,6 +386,10 @@ function spawnKimi(input) {
|
|
|
385
386
|
// npm .cmd shim → node script (win32); identity on POSIX. Both this
|
|
386
387
|
// resolution and spawn() itself can throw synchronously on Windows —
|
|
387
388
|
// fail the request cleanly instead of crashing the daemon.
|
|
389
|
+
// Not yet verified to read the prompt from stdin, so it stays inline — and
|
|
390
|
+
// an oversized command line is refused here with a reason, instead of by
|
|
391
|
+
// the OS with a bare errno. See prompt-transport.ts.
|
|
392
|
+
(0, prompt_transport_1.assertCommandLineFits)('kimi', bin, args);
|
|
388
393
|
const inv = (0, win_1.resolveCliInvocation)(bin, args);
|
|
389
394
|
proc = (0, child_process_1.spawn)(inv.file, inv.args, {
|
|
390
395
|
cwd: input.workingDirectory,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ChildProcess } from 'child_process';
|
|
2
|
+
export interface PromptTransport {
|
|
3
|
+
/** argv to spawn with. The very same array when nothing moved. */
|
|
4
|
+
args: string[];
|
|
5
|
+
/** Text to write to the child's stdin and then close it. null = nothing
|
|
6
|
+
* moved; leave stdin ignored exactly as before. */
|
|
7
|
+
stdin: string | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Take the prompt at `promptIndex` off the command line.
|
|
11
|
+
*
|
|
12
|
+
* With `force` the prompt always moves (the one-path rule). Without it, it
|
|
13
|
+
* moves only when the rendered command line would not fit the platform's
|
|
14
|
+
* budget — the original behaviour, kept for callers that have no stdin route.
|
|
15
|
+
*
|
|
16
|
+
* `placeholder` is what the CLI wants left in the prompt's place: `'-'` for
|
|
17
|
+
* codex, `null` for claude (which reads stdin only when given no prompt).
|
|
18
|
+
*/
|
|
19
|
+
export declare function offloadLongPrompt(file: string, args: string[], promptIndex: number, placeholder: string | null, isWindows?: boolean, force?: boolean): PromptTransport;
|
|
20
|
+
/** stdio for spawn(): a pipe for stdin only when there is a prompt to feed. */
|
|
21
|
+
export declare function stdioFor(stdin: string | null): ['pipe' | 'ignore', 'pipe', 'pipe'];
|
|
22
|
+
/**
|
|
23
|
+
* Write the offloaded prompt to the child's stdin and close it, so the CLI
|
|
24
|
+
* sees EOF and starts. A child that exits before reading (bad binary, an
|
|
25
|
+
* immediate crash) turns the write into EPIPE; that outcome already surfaces
|
|
26
|
+
* through the process's own exit/error path, so the stream error is swallowed
|
|
27
|
+
* here rather than escaping as an unhandled 'error' event that would take the
|
|
28
|
+
* whole daemon down with it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function feedStdin(proc: Pick<ChildProcess, 'stdin'>, text: string | null): void;
|
|
31
|
+
/**
|
|
32
|
+
* Would this command line be refused by the operating system? Returns a plain
|
|
33
|
+
* sentence naming the limit that would be hit, or null when it fits.
|
|
34
|
+
*
|
|
35
|
+
* The limits differ in kind, not just size: Windows caps the WHOLE command
|
|
36
|
+
* line (32,767 characters, CreateProcess); Linux caps each SINGLE argument
|
|
37
|
+
* (131,072 bytes, MAX_ARG_STRLEN) and the total at a generous 2 MB; macOS caps
|
|
38
|
+
* the total including the environment at 1 MB, so 900,000 leaves room for it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function commandLineProblem(file: string, args: string[], platform?: NodeJS.Platform): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* Refuse, with a reason a person can act on, before the OS refuses with an
|
|
43
|
+
* errno. For launchers whose stdin route is not yet verified. The throw lands
|
|
44
|
+
* in the launcher's existing spawn-failure path, so the provider ladder still
|
|
45
|
+
* moves on to an agent that can take the prompt.
|
|
46
|
+
*/
|
|
47
|
+
export declare function assertCommandLineFits(agent: string, file: string, args: string[], platform?: NodeJS.Platform): void;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// How a prompt reaches an agent: one path, used on every run.
|
|
3
|
+
//
|
|
4
|
+
// claude-system-prompt.ts moved an oversized --append-system-prompt into a
|
|
5
|
+
// file, and that was enough for Hannah, whose USER prompt was small. It was
|
|
6
|
+
// not enough for Brenda on 10 Sep 2026: a fresh session opened with a
|
|
7
|
+
// "context refresh" plus a global activity diary inside the user prompt —
|
|
8
|
+
// 36,421 characters for one DM, 67,658 for another — and that single argument
|
|
9
|
+
// was on its own over CreateProcess's 32,767-character limit. claude died with
|
|
10
|
+
// `spawn ENAMETOOLONG`, the provider ladder hopped to codex, whose command
|
|
11
|
+
// line carried the system prompt AND the user prompt as one argument, and
|
|
12
|
+
// codex died the same way. The entity posted "spawn ENAMETOOLONG" twice and
|
|
13
|
+
// the run dead-lettered.
|
|
14
|
+
//
|
|
15
|
+
// The rare route is where that bug hid: a size-dependent branch nobody had
|
|
16
|
+
// exercised for the user prompt. So the prompt now ALWAYS goes down the
|
|
17
|
+
// child's stdin for the launchers that read it there — `claude --print` when
|
|
18
|
+
// given no positional prompt, `codex exec` (and `codex exec resume <id>`)
|
|
19
|
+
// when the prompt is `-`. Both were verified on Windows on 10 Sep 2026. Size
|
|
20
|
+
// stops being a thing that can decide whether an entity starts, and the path
|
|
21
|
+
// is tested by every normal turn instead of by the one that overflows.
|
|
22
|
+
//
|
|
23
|
+
// The `force` flag is what makes the route unconditional; without it the
|
|
24
|
+
// helper keeps its original "only when it would not fit" behaviour, which is
|
|
25
|
+
// what the TUI path still uses. Launchers that have NOT been verified to read
|
|
26
|
+
// stdin (grok, kimi, gemini) keep the inline form and get assertCommandLineFits
|
|
27
|
+
// instead: a refusal that names the oversized argument, in place of a bare
|
|
28
|
+
// errno from the OS.
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.offloadLongPrompt = offloadLongPrompt;
|
|
31
|
+
exports.stdioFor = stdioFor;
|
|
32
|
+
exports.feedStdin = feedStdin;
|
|
33
|
+
exports.commandLineProblem = commandLineProblem;
|
|
34
|
+
exports.assertCommandLineFits = assertCommandLineFits;
|
|
35
|
+
const claude_system_prompt_1 = require("./claude-system-prompt");
|
|
36
|
+
/**
|
|
37
|
+
* Take the prompt at `promptIndex` off the command line.
|
|
38
|
+
*
|
|
39
|
+
* With `force` the prompt always moves (the one-path rule). Without it, it
|
|
40
|
+
* moves only when the rendered command line would not fit the platform's
|
|
41
|
+
* budget — the original behaviour, kept for callers that have no stdin route.
|
|
42
|
+
*
|
|
43
|
+
* `placeholder` is what the CLI wants left in the prompt's place: `'-'` for
|
|
44
|
+
* codex, `null` for claude (which reads stdin only when given no prompt).
|
|
45
|
+
*/
|
|
46
|
+
function offloadLongPrompt(file, args, promptIndex, placeholder, isWindows = process.platform === 'win32', force = false) {
|
|
47
|
+
const noop = { args, stdin: null };
|
|
48
|
+
if (promptIndex < 0 || promptIndex >= args.length)
|
|
49
|
+
return noop;
|
|
50
|
+
if (!force) {
|
|
51
|
+
const budget = isWindows ? claude_system_prompt_1.WINDOWS_BUDGET : claude_system_prompt_1.POSIX_BUDGET;
|
|
52
|
+
if ((0, claude_system_prompt_1.commandLineLength)(file, args) <= budget)
|
|
53
|
+
return noop;
|
|
54
|
+
}
|
|
55
|
+
const prompt = args[promptIndex];
|
|
56
|
+
const next = args.slice();
|
|
57
|
+
if (placeholder === null)
|
|
58
|
+
next.splice(promptIndex, 1);
|
|
59
|
+
else
|
|
60
|
+
next[promptIndex] = placeholder;
|
|
61
|
+
return { args: next, stdin: prompt };
|
|
62
|
+
}
|
|
63
|
+
/** stdio for spawn(): a pipe for stdin only when there is a prompt to feed. */
|
|
64
|
+
function stdioFor(stdin) {
|
|
65
|
+
return [stdin === null ? 'ignore' : 'pipe', 'pipe', 'pipe'];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Write the offloaded prompt to the child's stdin and close it, so the CLI
|
|
69
|
+
* sees EOF and starts. A child that exits before reading (bad binary, an
|
|
70
|
+
* immediate crash) turns the write into EPIPE; that outcome already surfaces
|
|
71
|
+
* through the process's own exit/error path, so the stream error is swallowed
|
|
72
|
+
* here rather than escaping as an unhandled 'error' event that would take the
|
|
73
|
+
* whole daemon down with it.
|
|
74
|
+
*/
|
|
75
|
+
function feedStdin(proc, text) {
|
|
76
|
+
if (text === null)
|
|
77
|
+
return;
|
|
78
|
+
const stdin = proc.stdin;
|
|
79
|
+
if (!stdin)
|
|
80
|
+
return;
|
|
81
|
+
stdin.on('error', () => { });
|
|
82
|
+
stdin.end(text, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Would this command line be refused by the operating system? Returns a plain
|
|
86
|
+
* sentence naming the limit that would be hit, or null when it fits.
|
|
87
|
+
*
|
|
88
|
+
* The limits differ in kind, not just size: Windows caps the WHOLE command
|
|
89
|
+
* line (32,767 characters, CreateProcess); Linux caps each SINGLE argument
|
|
90
|
+
* (131,072 bytes, MAX_ARG_STRLEN) and the total at a generous 2 MB; macOS caps
|
|
91
|
+
* the total including the environment at 1 MB, so 900,000 leaves room for it.
|
|
92
|
+
*/
|
|
93
|
+
function commandLineProblem(file, args, platform = process.platform) {
|
|
94
|
+
const total = (0, claude_system_prompt_1.commandLineLength)(file, args);
|
|
95
|
+
const longest = args.reduce((m, a) => Math.max(m, a.length), 0);
|
|
96
|
+
if (platform === 'win32') {
|
|
97
|
+
return total > 32_767
|
|
98
|
+
? `the command line would be ${total} characters and Windows allows 32,767`
|
|
99
|
+
: null;
|
|
100
|
+
}
|
|
101
|
+
if (platform === 'linux' && longest > 131_072) {
|
|
102
|
+
return `one argument would be ${longest} characters and Linux allows 131,072 per argument`;
|
|
103
|
+
}
|
|
104
|
+
if (total > 900_000) {
|
|
105
|
+
return `the command line would be ${total} characters and this OS allows about 1,000,000 including the environment`;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Refuse, with a reason a person can act on, before the OS refuses with an
|
|
111
|
+
* errno. For launchers whose stdin route is not yet verified. The throw lands
|
|
112
|
+
* in the launcher's existing spawn-failure path, so the provider ladder still
|
|
113
|
+
* moves on to an agent that can take the prompt.
|
|
114
|
+
*/
|
|
115
|
+
function assertCommandLineFits(agent, file, args, platform = process.platform) {
|
|
116
|
+
const problem = commandLineProblem(file, args, platform);
|
|
117
|
+
if (!problem)
|
|
118
|
+
return;
|
|
119
|
+
throw new Error(`${agent} cannot be started: ${problem}. The prompt is too long to pass on the command line ` +
|
|
120
|
+
`and this launcher does not yet read it from stdin; the provider ladder will try the next agent.`);
|
|
121
|
+
}
|
package/dist/session-runner.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ export interface RuntimeRequestRow {
|
|
|
60
60
|
* tokens before SKILL.md is written. Optional — older rows omit it and
|
|
61
61
|
* the daemon defaults to {} (no substitution beyond skill defaults). */
|
|
62
62
|
skill_var_values?: Record<string, Record<string, string>> | null;
|
|
63
|
+
/** Conversation context files {relative path: markdown} to write into cwd before spawn (context-files.ts). */
|
|
64
|
+
context_files?: Record<string, string> | null;
|
|
63
65
|
/** Self-improve flag for autonomous entities. When truthy AND the run
|
|
64
66
|
* reaches a `completed` terminal status, the daemon fires a best-effort
|
|
65
67
|
* POST to the `entity-self-improve` edge function so the entity can
|
package/dist/session-runner.js
CHANGED
|
@@ -75,6 +75,7 @@ const hang_watchdog_1 = require("./hang-watchdog");
|
|
|
75
75
|
const install_1 = require("./install");
|
|
76
76
|
const memory_capture_1 = require("./memory-capture");
|
|
77
77
|
const memory_pack_1 = require("./memory-pack");
|
|
78
|
+
const context_files_1 = require("./context-files");
|
|
78
79
|
const guardrails_1 = require("./guardrails");
|
|
79
80
|
const time_context_1 = require("./time-context");
|
|
80
81
|
// Idle window after the LAST mid-turn event before we declare the turn
|
|
@@ -1676,7 +1677,15 @@ async function runCodex(req, cwd, installed) {
|
|
|
1676
1677
|
// codex has no --append-system-prompt; we prepend it to the prompt as
|
|
1677
1678
|
// a "system instruction" preamble so entities still influence the run.
|
|
1678
1679
|
const codexSystem = (0, time_context_1.appendTimeContext)(req.system_prompt);
|
|
1679
|
-
|
|
1680
|
+
// codex has no --append-system-prompt, but it reads AGENTS.md from the working
|
|
1681
|
+
// directory as project guidance — the native home for an entity's system
|
|
1682
|
+
// prompt, and it keeps 30K characters of persona out of the user turn. The
|
|
1683
|
+
// glued form stays as the fallback when the file cannot be written (a project
|
|
1684
|
+
// folder with its own AGENTS.md, a read-only disk).
|
|
1685
|
+
const agentsMd = (0, codex_spawn_1.writeCodexAgentsMd)(cwd, codexSystem);
|
|
1686
|
+
const finalPrompt = agentsMd
|
|
1687
|
+
? req.prompt
|
|
1688
|
+
: `System instructions:\n${codexSystem.trim()}\n\nUser:\n${req.prompt}`;
|
|
1680
1689
|
// Opportunistic reclaim right before we allocate a new scratch CODEX_HOME,
|
|
1681
1690
|
// so a burst of runs frees space proactively rather than waiting for the
|
|
1682
1691
|
// next periodic sweep. No-op below the high-water mark.
|
|
@@ -2081,12 +2090,7 @@ async function buildDriveManifest(installed) {
|
|
|
2081
2090
|
lines.push(` - ${sub.name}/${f.name}`);
|
|
2082
2091
|
}
|
|
2083
2092
|
const body = lines.length ? lines.join('\n') : ' (empty — nothing saved yet)';
|
|
2084
|
-
return
|
|
2085
|
-
'## Your +Ai Drive',
|
|
2086
|
-
"You have a private +Ai Drive — file storage for working files and artifacts (documents, exports, data you produce) that survives across runs. This is for FILES, not memory: to remember facts, preferences, decisions, or what you learned, use your DocFlows memory (see below), never the Drive and never a local file. Files currently in your Drive:",
|
|
2087
|
-
body,
|
|
2088
|
-
"Use your +Ai Drive tools (list_files, read_file, write_file, make_folder, …) to read, add, and organise files.",
|
|
2089
|
-
].join('\n');
|
|
2093
|
+
return { body, count: lines.length };
|
|
2090
2094
|
}
|
|
2091
2095
|
catch {
|
|
2092
2096
|
return null;
|
|
@@ -2194,6 +2198,15 @@ async function runRequest(req) {
|
|
|
2194
2198
|
}
|
|
2195
2199
|
catch { /* keep original prompt */ }
|
|
2196
2200
|
}
|
|
2201
|
+
// Conversation-scoped context files (context/*.md) from the dispatcher: the
|
|
2202
|
+
// last 50 messages, the sender, the chat. Best-effort — the inline briefing
|
|
2203
|
+
// is complete without them. See context-files.ts.
|
|
2204
|
+
if (req.context_files) {
|
|
2205
|
+
const ctx = (0, context_files_1.writeContextFiles)(cwd, req.context_files);
|
|
2206
|
+
if (ctx.skipped.length > 0) {
|
|
2207
|
+
console.error(`[context-files] ${ctx.skipped.length} not written [reqId=${req.id}]: ${ctx.skipped.map(s => `${s.path} (${s.reason})`).join('; ')}`);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2197
2210
|
// Resolve and install MCPs / skills / packages before spawn.
|
|
2198
2211
|
const installSet = resolveInstallSet(req, resolved);
|
|
2199
2212
|
const installed = await (0, install_1.install)(cwd, {
|
|
@@ -2218,8 +2231,18 @@ async function runRequest(req) {
|
|
|
2218
2231
|
// it begins the run already aware of the files it has. All spawn paths read
|
|
2219
2232
|
// req.system_prompt, so mutating it here reaches every agent variant.
|
|
2220
2233
|
try {
|
|
2221
|
-
const
|
|
2222
|
-
if (
|
|
2234
|
+
const drive = await buildDriveManifest(installed);
|
|
2235
|
+
if (drive) {
|
|
2236
|
+
const intro = "You have a private +Ai Drive — file storage for working files and artifacts (documents, exports, data you produce) that survives across runs. This is for FILES, not memory: to remember facts, preferences, decisions, or what you learned, use your DocFlows memory (see below), never the Drive and never a local file.";
|
|
2237
|
+
const tools = 'Use your +Ai Drive tools (list_files, read_file, write_file, make_folder, …) to read, add, and organise files.';
|
|
2238
|
+
// The listing can run to hundreds of lines and changes whenever a file
|
|
2239
|
+
// does, so it lives in a file the entity reads when it needs one, and the
|
|
2240
|
+
// system prompt keeps a two-line pointer that stays stable turn to turn.
|
|
2241
|
+
// If the file cannot be written, the full listing goes inline as before.
|
|
2242
|
+
const wrote = (0, context_files_1.writeContextFiles)(cwd, { 'context/drive.md': `# Your +Ai Drive\n\nFiles currently in your Drive:\n${drive.body}\n` }).written.length > 0;
|
|
2243
|
+
const driveManifest = wrote
|
|
2244
|
+
? ['## Your +Ai Drive', intro, `Your current file list (${drive.count} entries) is in ./context/drive.md — read it when you need a file.`, tools].join('\n')
|
|
2245
|
+
: ['## Your +Ai Drive', `${intro} Files currently in your Drive:`, drive.body, tools].join('\n');
|
|
2223
2246
|
req.system_prompt =
|
|
2224
2247
|
req.system_prompt && req.system_prompt.trim().length > 0
|
|
2225
2248
|
? `${req.system_prompt}\n\n${driveManifest}`
|
package/dist/tui/run.d.ts
CHANGED
|
@@ -3,11 +3,29 @@ export declare function shouldRenderTui(env: {
|
|
|
3
3
|
stdinTTY: boolean;
|
|
4
4
|
noTui: boolean;
|
|
5
5
|
}): boolean;
|
|
6
|
+
/** What the screen does when it is leaving. */
|
|
7
|
+
export type LeaveAction = 'exit' | 'shutdown' | 'detach';
|
|
8
|
+
/**
|
|
9
|
+
* The rule that decides whether this node survives the night.
|
|
10
|
+
*
|
|
11
|
+
* `ainode` runs the daemon in the SAME process as the dashboard, and every
|
|
12
|
+
* teardown used to end in process.exit(0). So a terminal that went away while
|
|
13
|
+
* nobody was watching — a closed window, an ssh drop, a stray read from a
|
|
14
|
+
* background process group — stopped the node, and it stayed stopped until
|
|
15
|
+
* someone re-ran `ainode` the next morning. Only `quit` is a person asking.
|
|
16
|
+
*
|
|
17
|
+
* Pure and exported so the rule is checkable without a TTY.
|
|
18
|
+
*/
|
|
19
|
+
export declare function leaveAction(reason: string, ownsDaemon: boolean): LeaveAction;
|
|
6
20
|
export declare function runDashboard(opts: {
|
|
7
21
|
viewerMode: boolean;
|
|
8
22
|
pid: number | null;
|
|
9
23
|
startedAt: number | null;
|
|
10
24
|
version: string;
|
|
25
|
+
/** Present when the daemon runs in THIS process: how to stop it properly
|
|
26
|
+
* when the user actually asks — and how to tell that apart from the
|
|
27
|
+
* terminal merely disappearing. */
|
|
28
|
+
stop?: () => Promise<void>;
|
|
11
29
|
}): Promise<void>;
|
|
12
30
|
/** `ainode harnesses` — straight to the harness screen. */
|
|
13
31
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.11",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|