@addai/node 0.30.10 → 0.30.12
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/codex-spawn.d.ts +32 -3
- package/dist/codex-spawn.js +163 -20
- 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/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 +6 -15
- 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/codex-spawn.d.ts
CHANGED
|
@@ -25,14 +25,43 @@ export interface CodexInput {
|
|
|
25
25
|
* Mirrors the claude --strict-mcp-config behavior. */
|
|
26
26
|
mcpServers?: CodexMcpServer[];
|
|
27
27
|
}
|
|
28
|
+
export interface CodexHomeOptions {
|
|
29
|
+
/** Forward a remote server's credentials as headers via env_http_headers.
|
|
30
|
+
* spawnCodex passes codexSupportsEnvHttpHeaders(bin), so a codex too old to
|
|
31
|
+
* know the key keeps exactly the config it had before. Default true. */
|
|
32
|
+
envHttpHeaders?: boolean;
|
|
33
|
+
}
|
|
28
34
|
/** Build a config.toml that declares only the supplied MCP servers and
|
|
29
35
|
* return the CODEX_HOME directory plus any env vars the child needs
|
|
30
|
-
* (bearer tokens for remote servers). Caller is
|
|
31
|
-
* (we drop it under os.tmpdir() so it gets reaped). */
|
|
32
|
-
export declare function writeCodexHome(servers: CodexMcpServer[], workingDirectory: string): {
|
|
36
|
+
* (bearer tokens and header values for remote servers). Caller is
|
|
37
|
+
* responsible for cleanup (we drop it under os.tmpdir() so it gets reaped). */
|
|
38
|
+
export declare function writeCodexHome(servers: CodexMcpServer[], workingDirectory: string, opts?: CodexHomeOptions): {
|
|
33
39
|
home: string;
|
|
34
40
|
extraEnv: Record<string, string>;
|
|
35
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Does this codex know env_http_headers? Asked by having it parse a throwaway
|
|
44
|
+
* config (`codex mcp list --json`, which reads config and contacts nothing)
|
|
45
|
+
* and checking that the header survived. Remembered per binary. Any failure
|
|
46
|
+
* answers "no", which keeps the bearer-only config every install ran before.
|
|
47
|
+
*/
|
|
48
|
+
export declare function codexSupportsEnvHttpHeaders(bin: string, probe?: (bin: string, codexHome: string) => string): boolean;
|
|
49
|
+
/** Read `codex mcp list --json` output: did the probe server's header survive?
|
|
50
|
+
* Exported for tests. */
|
|
51
|
+
export declare function codexListShowsEnvHeaders(stdout: string): boolean;
|
|
52
|
+
/** First line of an AGENTS.md this daemon wrote. A project's own AGENTS.md
|
|
53
|
+
* never carries it and is never touched. */
|
|
54
|
+
export declare const AGENTS_MD_MARKER = "<!-- ainode: entity system prompt \u2014 rewritten before every turn; edits here do not persist -->";
|
|
55
|
+
/**
|
|
56
|
+
* Put the entity's system prompt where codex natively looks for guidance:
|
|
57
|
+
* AGENTS.md in the working directory. codex has no --append-system-prompt,
|
|
58
|
+
* and gluing 30K characters of persona onto every user turn was the
|
|
59
|
+
* alternative. Returns false — and writes nothing — when the folder already
|
|
60
|
+
* has an AGENTS.md that is not ours, so a chat bound to a real repository
|
|
61
|
+
* can never have its project file overwritten; the caller then falls back to
|
|
62
|
+
* the glued form.
|
|
63
|
+
*/
|
|
64
|
+
export declare function writeCodexAgentsMd(workingDirectory: string, systemPrompt: string): boolean;
|
|
36
65
|
export interface CodexHandle {
|
|
37
66
|
pid: number | undefined;
|
|
38
67
|
threadId: string | null;
|
package/dist/codex-spawn.js
CHANGED
|
@@ -40,7 +40,11 @@ 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.codexSupportsEnvHttpHeaders = codexSupportsEnvHttpHeaders;
|
|
46
|
+
exports.codexListShowsEnvHeaders = codexListShowsEnvHeaders;
|
|
47
|
+
exports.writeCodexAgentsMd = writeCodexAgentsMd;
|
|
44
48
|
exports.codexEffort = codexEffort;
|
|
45
49
|
exports.spawnCodex = spawnCodex;
|
|
46
50
|
const child_process_1 = require("child_process");
|
|
@@ -52,6 +56,8 @@ const codex_binary_1 = require("./codex-binary");
|
|
|
52
56
|
const diskguard_1 = require("./diskguard");
|
|
53
57
|
const win_1 = require("./win");
|
|
54
58
|
const events_1 = require("./events");
|
|
59
|
+
const prompt_transport_1 = require("./prompt-transport");
|
|
60
|
+
const mcp_headers_1 = require("./mcp-headers");
|
|
55
61
|
/** Quote a string as a TOML basic string (double-quoted, escape backslash + quote). */
|
|
56
62
|
function tomlString(s) {
|
|
57
63
|
return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
|
|
@@ -60,11 +66,17 @@ function tomlString(s) {
|
|
|
60
66
|
* Only the NAME goes in config.toml — the value rides on the child env, so
|
|
61
67
|
* the token never lands on disk. */
|
|
62
68
|
const bearerEnvVar = (slug) => `AINODE_MCP_${slug.replace(/[^A-Za-z0-9]+/g, '_').toUpperCase()}_TOKEN`;
|
|
69
|
+
/** Env var name carrying one HTTP header's value into the codex process.
|
|
70
|
+
* config.toml references it by name via env_http_headers, so the value never
|
|
71
|
+
* lands on disk either. */
|
|
72
|
+
const headerEnvVar = (slug, header) => `AINODE_MCP_${slug.replace(/[^A-Za-z0-9]+/g, '_').toUpperCase()}` +
|
|
73
|
+
`_H_${header.replace(/[^A-Za-z0-9]+/g, '_').toUpperCase()}`;
|
|
63
74
|
/** Build a config.toml that declares only the supplied MCP servers and
|
|
64
75
|
* return the CODEX_HOME directory plus any env vars the child needs
|
|
65
|
-
* (bearer tokens for remote servers). Caller is
|
|
66
|
-
* (we drop it under os.tmpdir() so it gets reaped). */
|
|
67
|
-
function writeCodexHome(servers, workingDirectory) {
|
|
76
|
+
* (bearer tokens and header values for remote servers). Caller is
|
|
77
|
+
* responsible for cleanup (we drop it under os.tmpdir() so it gets reaped). */
|
|
78
|
+
function writeCodexHome(servers, workingDirectory, opts = {}) {
|
|
79
|
+
const envHttpHeaders = opts.envHttpHeaders !== false;
|
|
68
80
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'entities-codex-'));
|
|
69
81
|
const extraEnv = {};
|
|
70
82
|
let toml = '# Generated by ainode — sandboxes codex against host config\n\n';
|
|
@@ -89,6 +101,8 @@ function writeCodexHome(servers, workingDirectory) {
|
|
|
89
101
|
toml += `url = ${tomlString(url)}\n`;
|
|
90
102
|
// codex reads the bearer from its OWN environment (--env is stdio-only),
|
|
91
103
|
// so the value goes on the child env and only the var name in the file.
|
|
104
|
+
// The bearer is exactly what this adapter always sent, kept so anything
|
|
105
|
+
// that works on codex today keeps working.
|
|
92
106
|
const creds = Object.entries(s.env || {})
|
|
93
107
|
.filter(([k, v]) => typeof v === 'string' && v.length > 0 && !k.startsWith('OAUTH_REFRESH'));
|
|
94
108
|
const bearer = creds.find(([k]) => k === 'OAUTH_ACCESS_TOKEN') ?? creds[0];
|
|
@@ -96,15 +110,34 @@ function writeCodexHome(servers, workingDirectory) {
|
|
|
96
110
|
const varName = bearerEnvVar(s.slug);
|
|
97
111
|
extraEnv[varName] = String(bearer[1]);
|
|
98
112
|
toml += `bearer_token_env_var = ${tomlString(varName)}\n`;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
}
|
|
114
|
+
// The bearer alone was never enough. The servers read each credential
|
|
115
|
+
// from its own header (the claude path sends them all, see
|
|
116
|
+
// remoteMcpHeaders) and fall back to the bearer for any that is missing,
|
|
117
|
+
// so +Ai Drive, which carries three, used its API key as its workspace
|
|
118
|
+
// id and answered "Access denied to workspace" on every codex run.
|
|
119
|
+
// codex does send custom headers: env_http_headers maps a header name to
|
|
120
|
+
// the NAME of an env var, so the values stay off disk like the bearer.
|
|
121
|
+
const headers = (0, mcp_headers_1.remoteMcpHeaders)(s.env);
|
|
122
|
+
// An OAuth access token is already the bearer above.
|
|
123
|
+
delete headers.Authorization;
|
|
124
|
+
const entries = Object.entries(headers);
|
|
125
|
+
if (entries.length > 0 && envHttpHeaders) {
|
|
126
|
+
// A sub-table must follow the parent table's scalar keys.
|
|
127
|
+
toml += `[mcp_servers.${name}.env_http_headers]\n`;
|
|
128
|
+
for (const [header, value] of entries) {
|
|
129
|
+
const varName = headerEnvVar(s.slug, header);
|
|
130
|
+
extraEnv[varName] = value;
|
|
131
|
+
toml += `${tomlString(header)} = ${tomlString(varName)}\n`;
|
|
106
132
|
}
|
|
107
133
|
}
|
|
134
|
+
else if (creds.length > 1) {
|
|
135
|
+
// A codex too old to know env_http_headers carries the bearer only.
|
|
136
|
+
// Saying so beats a server that refuses for no visible reason.
|
|
137
|
+
console.error(`[codex] MCP ${s.slug}: this codex cannot send custom headers, so only ` +
|
|
138
|
+
`${bearer?.[0]} is forwarded and ${creds.length - 1} other credential ` +
|
|
139
|
+
`field(s) were dropped`);
|
|
140
|
+
}
|
|
108
141
|
toml += '\n';
|
|
109
142
|
continue;
|
|
110
143
|
}
|
|
@@ -156,6 +189,103 @@ function writeCodexHome(servers, workingDirectory) {
|
|
|
156
189
|
}
|
|
157
190
|
return { home: dir, extraEnv };
|
|
158
191
|
}
|
|
192
|
+
const PROBE_SERVER = 'ainode_probe';
|
|
193
|
+
const PROBE_HEADER = 'X-Ainode-Probe';
|
|
194
|
+
// A real answer holds until the binary might have been updated; a probe that
|
|
195
|
+
// could not run at all is retried sooner.
|
|
196
|
+
const PROBE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
197
|
+
const PROBE_FAILED_TTL_MS = 10 * 60 * 1000;
|
|
198
|
+
const envHeaderSupport = new Map();
|
|
199
|
+
/**
|
|
200
|
+
* Does this codex know env_http_headers? Asked by having it parse a throwaway
|
|
201
|
+
* config (`codex mcp list --json`, which reads config and contacts nothing)
|
|
202
|
+
* and checking that the header survived. Remembered per binary. Any failure
|
|
203
|
+
* answers "no", which keeps the bearer-only config every install ran before.
|
|
204
|
+
*/
|
|
205
|
+
function codexSupportsEnvHttpHeaders(bin, probe = codexMcpListJson) {
|
|
206
|
+
const hit = envHeaderSupport.get(bin);
|
|
207
|
+
if (hit && hit.until > Date.now())
|
|
208
|
+
return hit.ok;
|
|
209
|
+
let ok = false;
|
|
210
|
+
let failed = false;
|
|
211
|
+
let home = null;
|
|
212
|
+
try {
|
|
213
|
+
home = fs.mkdtempSync(path.join(os.tmpdir(), 'ainode-codex-probe-'));
|
|
214
|
+
fs.writeFileSync(path.join(home, 'config.toml'), `[mcp_servers.${PROBE_SERVER}]\n` +
|
|
215
|
+
`url = "http://127.0.0.1:9/mcp"\n` +
|
|
216
|
+
// A bearer keeps codex from looking for stored OAuth credentials.
|
|
217
|
+
`bearer_token_env_var = "AINODE_PROBE_TOKEN"\n` +
|
|
218
|
+
`[mcp_servers.${PROBE_SERVER}.env_http_headers]\n` +
|
|
219
|
+
`"${PROBE_HEADER}" = "AINODE_PROBE_HEADER"\n`, { mode: 0o600 });
|
|
220
|
+
ok = codexListShowsEnvHeaders(probe(bin, home));
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
ok = false;
|
|
224
|
+
failed = true;
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
if (home) {
|
|
228
|
+
try {
|
|
229
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
230
|
+
}
|
|
231
|
+
catch { /* reaped from tmp */ }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
envHeaderSupport.set(bin, { ok, until: Date.now() + (failed ? PROBE_FAILED_TTL_MS : PROBE_TTL_MS) });
|
|
235
|
+
return ok;
|
|
236
|
+
}
|
|
237
|
+
/** Read `codex mcp list --json` output: did the probe server's header survive?
|
|
238
|
+
* Exported for tests. */
|
|
239
|
+
function codexListShowsEnvHeaders(stdout) {
|
|
240
|
+
const start = stdout.search(/^\s*\[/m);
|
|
241
|
+
if (start < 0)
|
|
242
|
+
return false;
|
|
243
|
+
let list;
|
|
244
|
+
try {
|
|
245
|
+
list = JSON.parse(stdout.slice(start));
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
if (!Array.isArray(list))
|
|
251
|
+
return false;
|
|
252
|
+
const entry = list.find((e) => e && e.name === PROBE_SERVER);
|
|
253
|
+
return entry?.transport?.env_http_headers?.[PROBE_HEADER] === 'AINODE_PROBE_HEADER';
|
|
254
|
+
}
|
|
255
|
+
function codexMcpListJson(bin, codexHome) {
|
|
256
|
+
const inv = (0, win_1.resolveCliInvocation)(bin, ['mcp', 'list', '--json']);
|
|
257
|
+
return (0, child_process_1.execFileSync)(inv.file, inv.args, {
|
|
258
|
+
encoding: 'utf8', timeout: 20_000, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
259
|
+
env: { ...process.env, CODEX_HOME: codexHome },
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/** First line of an AGENTS.md this daemon wrote. A project's own AGENTS.md
|
|
263
|
+
* never carries it and is never touched. */
|
|
264
|
+
exports.AGENTS_MD_MARKER = '<!-- ainode: entity system prompt — rewritten before every turn; edits here do not persist -->';
|
|
265
|
+
/**
|
|
266
|
+
* Put the entity's system prompt where codex natively looks for guidance:
|
|
267
|
+
* AGENTS.md in the working directory. codex has no --append-system-prompt,
|
|
268
|
+
* and gluing 30K characters of persona onto every user turn was the
|
|
269
|
+
* alternative. Returns false — and writes nothing — when the folder already
|
|
270
|
+
* has an AGENTS.md that is not ours, so a chat bound to a real repository
|
|
271
|
+
* can never have its project file overwritten; the caller then falls back to
|
|
272
|
+
* the glued form.
|
|
273
|
+
*/
|
|
274
|
+
function writeCodexAgentsMd(workingDirectory, systemPrompt) {
|
|
275
|
+
const target = path.join(workingDirectory, 'AGENTS.md');
|
|
276
|
+
try {
|
|
277
|
+
if (fs.existsSync(target)) {
|
|
278
|
+
const head = fs.readFileSync(target, 'utf8').slice(0, exports.AGENTS_MD_MARKER.length + 16);
|
|
279
|
+
if (!head.includes(exports.AGENTS_MD_MARKER))
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
fs.writeFileSync(target, `${exports.AGENTS_MD_MARKER}\n\n${systemPrompt.trim()}\n`, 'utf8');
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
159
289
|
function lineToEvents(line) {
|
|
160
290
|
const type = typeof line.type === 'string' ? line.type : '';
|
|
161
291
|
// Errors come as { type: 'error', message: string } — surface them.
|
|
@@ -221,6 +351,10 @@ function spawnCodex(input) {
|
|
|
221
351
|
// `exec` command, before the `resume` subcommand. Putting them after
|
|
222
352
|
// resume returns "unexpected argument" and exits 2.
|
|
223
353
|
const baseFlags = ['--cd', input.workingDirectory, '--json'];
|
|
354
|
+
// AGENTS.md in the working directory carries the entity's system prompt (see
|
|
355
|
+
// writeCodexAgentsMd). codex truncates project docs at 32 KiB by default,
|
|
356
|
+
// which a persona plus its app instructions routinely exceeds.
|
|
357
|
+
baseFlags.push('-c', 'project_doc_max_bytes=262144');
|
|
224
358
|
if (input.skipGitRepoCheck)
|
|
225
359
|
baseFlags.push('--skip-git-repo-check');
|
|
226
360
|
if (input.bypassPermissions)
|
|
@@ -236,10 +370,19 @@ function spawnCodex(input) {
|
|
|
236
370
|
const args = input.resumeThreadId
|
|
237
371
|
? ['exec', ...baseFlags, 'resume', input.resumeThreadId, input.prompt]
|
|
238
372
|
: ['exec', ...baseFlags, input.prompt];
|
|
373
|
+
// Resolve binary path before spawn. Previously called spawn('codex', ...)
|
|
374
|
+
// bare, which threw ENOENT under launchd / desktop-app PATH inheritance
|
|
375
|
+
// and rejected `done` before any session_end event fired → request
|
|
376
|
+
// stuck at 'running' on the DB. Resolved first now, because the config
|
|
377
|
+
// below depends on what this codex understands.
|
|
378
|
+
const bin = (0, codex_binary_1.findCodexBinary)();
|
|
239
379
|
// Sandbox codex against an isolated CODEX_HOME so the host's
|
|
240
380
|
// ~/.codex/config.toml mcp_servers don't leak in. TERM=dumb keeps
|
|
241
381
|
// codex's TTY heuristics happy (no fancy terminal output).
|
|
242
|
-
const
|
|
382
|
+
const servers = input.mcpServers ?? [];
|
|
383
|
+
// Only a remote server that carries credentials needs the header probe.
|
|
384
|
+
const wantsHeaders = servers.some((s) => (0, mcp_headers_1.isRemoteMcp)(s.command) && Object.keys(s.env ?? {}).length > 0);
|
|
385
|
+
const { home: codexHome, extraEnv: mcpBearerEnv } = writeCodexHome(servers, input.workingDirectory, { envHttpHeaders: wantsHeaders && bin ? codexSupportsEnvHttpHeaders(bin) : false });
|
|
243
386
|
// Register the scratch dir with DiskGuard: marks it active (so a
|
|
244
387
|
// concurrent sweep won't evict a live run) and writes a PID lock so a
|
|
245
388
|
// different runtime process can tell it's still owned. dispose(true)
|
|
@@ -250,15 +393,11 @@ function spawnCodex(input) {
|
|
|
250
393
|
...process.env,
|
|
251
394
|
TERM: 'dumb',
|
|
252
395
|
CODEX_HOME: codexHome,
|
|
253
|
-
// Bearer tokens for remote (http/sse) MCP servers.
|
|
254
|
-
// vars via bearer_token_env_var
|
|
396
|
+
// Bearer tokens and header values for remote (http/sse) MCP servers.
|
|
397
|
+
// config.toml names these vars via bearer_token_env_var and
|
|
398
|
+
// env_http_headers; the values only ever live here.
|
|
255
399
|
...mcpBearerEnv,
|
|
256
400
|
};
|
|
257
|
-
// Resolve binary path before spawn. Previously called spawn('codex', ...)
|
|
258
|
-
// bare, which threw ENOENT under launchd / desktop-app PATH inheritance
|
|
259
|
-
// and rejected `done` before any session_end event fired → request
|
|
260
|
-
// stuck at 'running' on the DB.
|
|
261
|
-
const bin = (0, codex_binary_1.findCodexBinary)();
|
|
262
401
|
const listeners = [];
|
|
263
402
|
const emit = (e) => { for (const l of listeners)
|
|
264
403
|
try {
|
|
@@ -292,15 +431,18 @@ function spawnCodex(input) {
|
|
|
292
431
|
kill() { },
|
|
293
432
|
};
|
|
294
433
|
}
|
|
434
|
+
// One path, every run: the prompt goes down stdin (`codex exec -`, fresh and
|
|
435
|
+
// resume forms alike) whatever its size. See prompt-transport.ts.
|
|
436
|
+
const transport = (0, prompt_transport_1.offloadLongPrompt)(bin, args, args.length - 1, '-', process.platform === 'win32', true);
|
|
295
437
|
let proc;
|
|
296
438
|
try {
|
|
297
439
|
// npm .cmd shim → node script (win32); identity on POSIX. Throws on an
|
|
298
440
|
// unresolvable shim — caught below alongside spawn's own sync errors.
|
|
299
|
-
const inv = (0, win_1.resolveCliInvocation)(bin, args);
|
|
441
|
+
const inv = (0, win_1.resolveCliInvocation)(bin, transport.args);
|
|
300
442
|
proc = (0, child_process_1.spawn)(inv.file, inv.args, {
|
|
301
443
|
cwd: input.workingDirectory,
|
|
302
444
|
env: childEnv,
|
|
303
|
-
stdio:
|
|
445
|
+
stdio: (0, prompt_transport_1.stdioFor)(transport.stdin),
|
|
304
446
|
// No console window. This is headless work on somebody's machine —
|
|
305
447
|
// on Windows a spawn without this flashes a terminal every run.
|
|
306
448
|
windowsHide: true,
|
|
@@ -322,6 +464,7 @@ function spawnCodex(input) {
|
|
|
322
464
|
kill() { },
|
|
323
465
|
};
|
|
324
466
|
}
|
|
467
|
+
(0, prompt_transport_1.feedStdin)(proc, transport.stdin);
|
|
325
468
|
// Models do not reliably keep reasoning on their own stream — they drop
|
|
326
469
|
// back to ordinary text mid-run and carry on thinking inside a <think>
|
|
327
470
|
// 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/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,17 +3,8 @@ export declare function shouldRenderTui(env: {
|
|
|
3
3
|
stdinTTY: boolean;
|
|
4
4
|
noTui: boolean;
|
|
5
5
|
}): boolean;
|
|
6
|
-
/**
|
|
7
|
-
|
|
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';
|
|
6
|
+
/** What the screen does when it is leaving. */
|
|
7
|
+
export type LeaveAction = 'exit' | 'shutdown' | 'detach';
|
|
17
8
|
/**
|
|
18
9
|
* The rule that decides whether this node survives the night.
|
|
19
10
|
*
|
|
@@ -25,15 +16,15 @@ export type LeaveAction =
|
|
|
25
16
|
*
|
|
26
17
|
* Pure and exported so the rule is checkable without a TTY.
|
|
27
18
|
*/
|
|
28
|
-
export declare function leaveAction(reason:
|
|
19
|
+
export declare function leaveAction(reason: string, ownsDaemon: boolean): LeaveAction;
|
|
29
20
|
export declare function runDashboard(opts: {
|
|
30
21
|
viewerMode: boolean;
|
|
31
22
|
pid: number | null;
|
|
32
23
|
startedAt: number | null;
|
|
33
24
|
version: string;
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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. */
|
|
37
28
|
stop?: () => Promise<void>;
|
|
38
29
|
}): Promise<void>;
|
|
39
30
|
/** `ainode harnesses` — straight to the harness screen. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.12",
|
|
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": [
|