@addai/node 0.4.0

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.
Files changed (133) hide show
  1. package/README.md +96 -0
  2. package/dist/agent-auth.d.ts +9 -0
  3. package/dist/agent-auth.js +96 -0
  4. package/dist/attachments.d.ts +20 -0
  5. package/dist/attachments.js +164 -0
  6. package/dist/capabilities.d.ts +64 -0
  7. package/dist/capabilities.js +406 -0
  8. package/dist/claude-binary.d.ts +16 -0
  9. package/dist/claude-binary.js +145 -0
  10. package/dist/claude-config.d.ts +11 -0
  11. package/dist/claude-config.js +144 -0
  12. package/dist/claude-print.d.ts +35 -0
  13. package/dist/claude-print.js +312 -0
  14. package/dist/claude-spawn.d.ts +40 -0
  15. package/dist/claude-spawn.js +220 -0
  16. package/dist/cli.d.ts +2 -0
  17. package/dist/cli.js +137 -0
  18. package/dist/codex-binary.d.ts +4 -0
  19. package/dist/codex-binary.js +70 -0
  20. package/dist/codex-spawn.d.ts +47 -0
  21. package/dist/codex-spawn.js +320 -0
  22. package/dist/command-runner.d.ts +9 -0
  23. package/dist/command-runner.js +592 -0
  24. package/dist/config.d.ts +7 -0
  25. package/dist/config.js +38 -0
  26. package/dist/control-server.d.ts +5 -0
  27. package/dist/control-server.js +292 -0
  28. package/dist/diskguard.d.ts +96 -0
  29. package/dist/diskguard.js +435 -0
  30. package/dist/error-classify.d.ts +20 -0
  31. package/dist/error-classify.js +159 -0
  32. package/dist/events.d.ts +26 -0
  33. package/dist/events.js +97 -0
  34. package/dist/gemini-binary.d.ts +4 -0
  35. package/dist/gemini-binary.js +64 -0
  36. package/dist/gemini-spawn.d.ts +52 -0
  37. package/dist/gemini-spawn.js +369 -0
  38. package/dist/git-identity.d.ts +18 -0
  39. package/dist/git-identity.js +104 -0
  40. package/dist/grok-binary.d.ts +4 -0
  41. package/dist/grok-binary.js +70 -0
  42. package/dist/grok-spawn.d.ts +41 -0
  43. package/dist/grok-spawn.js +495 -0
  44. package/dist/guardrails.d.ts +32 -0
  45. package/dist/guardrails.js +125 -0
  46. package/dist/hang-watchdog.d.ts +17 -0
  47. package/dist/hang-watchdog.js +48 -0
  48. package/dist/harness-registry.d.ts +55 -0
  49. package/dist/harness-registry.js +180 -0
  50. package/dist/heartbeat.d.ts +13 -0
  51. package/dist/heartbeat.js +95 -0
  52. package/dist/index.d.ts +12 -0
  53. package/dist/index.js +440 -0
  54. package/dist/install.d.ts +63 -0
  55. package/dist/install.js +467 -0
  56. package/dist/jsonl-tail.d.ts +20 -0
  57. package/dist/jsonl-tail.js +192 -0
  58. package/dist/kimi-binary.d.ts +4 -0
  59. package/dist/kimi-binary.js +68 -0
  60. package/dist/kimi-spawn.d.ts +47 -0
  61. package/dist/kimi-spawn.js +319 -0
  62. package/dist/lockfile.d.ts +30 -0
  63. package/dist/lockfile.js +169 -0
  64. package/dist/mcp-config.d.ts +23 -0
  65. package/dist/mcp-config.js +133 -0
  66. package/dist/memory-capture.d.ts +20 -0
  67. package/dist/memory-capture.js +45 -0
  68. package/dist/memory-pack.d.ts +34 -0
  69. package/dist/memory-pack.js +171 -0
  70. package/dist/model-filter.d.ts +4 -0
  71. package/dist/model-filter.js +34 -0
  72. package/dist/node-health.d.ts +18 -0
  73. package/dist/node-health.js +74 -0
  74. package/dist/pairing.d.ts +20 -0
  75. package/dist/pairing.js +129 -0
  76. package/dist/paths.d.ts +9 -0
  77. package/dist/paths.js +48 -0
  78. package/dist/precompact-hook.d.ts +27 -0
  79. package/dist/precompact-hook.js +136 -0
  80. package/dist/projects.d.ts +25 -0
  81. package/dist/projects.js +255 -0
  82. package/dist/prompt-keystrokes.d.ts +3 -0
  83. package/dist/prompt-keystrokes.js +56 -0
  84. package/dist/pty-helper.d.ts +26 -0
  85. package/dist/pty-helper.js +156 -0
  86. package/dist/request-pump.d.ts +12 -0
  87. package/dist/request-pump.js +249 -0
  88. package/dist/run-retry.d.ts +28 -0
  89. package/dist/run-retry.js +81 -0
  90. package/dist/self-update.d.ts +75 -0
  91. package/dist/self-update.js +192 -0
  92. package/dist/session-runner.d.ts +130 -0
  93. package/dist/session-runner.js +2271 -0
  94. package/dist/sleep-detector.d.ts +11 -0
  95. package/dist/sleep-detector.js +32 -0
  96. package/dist/state-migrate.d.ts +8 -0
  97. package/dist/state-migrate.js +91 -0
  98. package/dist/store.d.ts +5 -0
  99. package/dist/store.js +82 -0
  100. package/dist/supabase-client.d.ts +15 -0
  101. package/dist/supabase-client.js +119 -0
  102. package/dist/time-context.d.ts +10 -0
  103. package/dist/time-context.js +71 -0
  104. package/dist/tokens.d.ts +4 -0
  105. package/dist/tokens.js +33 -0
  106. package/dist/tui/app.d.ts +35 -0
  107. package/dist/tui/app.js +82 -0
  108. package/dist/tui/console-capture.d.ts +13 -0
  109. package/dist/tui/console-capture.js +62 -0
  110. package/dist/tui/dashboard.d.ts +42 -0
  111. package/dist/tui/dashboard.js +205 -0
  112. package/dist/tui/data.d.ts +57 -0
  113. package/dist/tui/data.js +62 -0
  114. package/dist/tui/harnesses.d.ts +16 -0
  115. package/dist/tui/harnesses.js +354 -0
  116. package/dist/tui/render.d.ts +21 -0
  117. package/dist/tui/render.js +116 -0
  118. package/dist/tui/requests.d.ts +18 -0
  119. package/dist/tui/requests.js +127 -0
  120. package/dist/tui/run.d.ts +13 -0
  121. package/dist/tui/run.js +178 -0
  122. package/dist/tui/transcript.d.ts +39 -0
  123. package/dist/tui/transcript.js +234 -0
  124. package/dist/tui.d.ts +1 -0
  125. package/dist/tui.js +314 -0
  126. package/dist/types.d.ts +65 -0
  127. package/dist/types.js +3 -0
  128. package/dist/win.d.ts +67 -0
  129. package/dist/win.js +317 -0
  130. package/package.json +60 -0
  131. package/scripts/fix-pty-helper.js +28 -0
  132. package/scripts/precompact-capture.js +292 -0
  133. package/scripts/smoke-test.sh +74 -0
package/dist/events.js ADDED
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bufferedEvents = bufferedEvents;
4
+ exports.normalizeJsonlLine = normalizeJsonlLine;
5
+ /**
6
+ * Event source for degenerate spawn handles (binary not installed / spawn
7
+ * threw synchronously). The old pattern — queueMicrotask over a listeners
8
+ * array — silently DROPPED the events: session-runner attaches onEvent only
9
+ * after `await setStatus(...)`, and the microtask fires at that first await,
10
+ * when the listeners array is still empty. The request then completed with
11
+ * no error ever surfaced to the chat.
12
+ *
13
+ * This buffers the events and delivers them (async, preserving the old
14
+ * post-return timing contract) to each listener when it attaches.
15
+ */
16
+ function bufferedEvents(events) {
17
+ const pending = [...events];
18
+ const listeners = [];
19
+ const drain = () => {
20
+ while (pending.length) {
21
+ const e = pending.shift();
22
+ for (const l of listeners) {
23
+ try {
24
+ l(e);
25
+ }
26
+ catch { /* listener error */ }
27
+ }
28
+ }
29
+ };
30
+ return {
31
+ onEvent: (cb) => {
32
+ listeners.push(cb);
33
+ queueMicrotask(drain);
34
+ },
35
+ };
36
+ }
37
+ /**
38
+ * Pure transform from one line of Claude's JSONL transcript into zero or
39
+ * more RuntimeEvent objects.
40
+ *
41
+ * - `user` lines: extract any tool_result content blocks (caller's own
42
+ * prompt echoes are dropped — they already know what they sent).
43
+ * - `assistant` lines: text blocks → assistant_text, tool_use blocks
44
+ * → tool_use. String content → single assistant_text.
45
+ * - Everything else → claude:<type> forward-compat passthrough.
46
+ */
47
+ function normalizeJsonlLine(line) {
48
+ const type = typeof line.type === 'string' ? line.type : undefined;
49
+ if (!type)
50
+ return [];
51
+ if (type === 'user') {
52
+ const message = line.message;
53
+ const content = message?.content;
54
+ if (!Array.isArray(content))
55
+ return [];
56
+ const events = [];
57
+ for (const block of content) {
58
+ if (block?.type === 'tool_result') {
59
+ events.push({
60
+ type: 'tool_result',
61
+ id: String(block.tool_use_id ?? ''),
62
+ content: block.content,
63
+ isError: block.is_error === true,
64
+ });
65
+ }
66
+ }
67
+ return events;
68
+ }
69
+ if (type === 'assistant') {
70
+ const message = line.message;
71
+ if (!message)
72
+ return [];
73
+ const content = message.content;
74
+ if (typeof content === 'string') {
75
+ return [{ type: 'assistant_text', delta: content }];
76
+ }
77
+ if (Array.isArray(content)) {
78
+ const events = [];
79
+ for (const block of content) {
80
+ if (block?.type === 'text' && typeof block.text === 'string') {
81
+ events.push({ type: 'assistant_text', delta: block.text });
82
+ }
83
+ else if (block?.type === 'tool_use') {
84
+ events.push({
85
+ type: 'tool_use',
86
+ id: String(block.id ?? ''),
87
+ name: String(block.name ?? ''),
88
+ input: block.input,
89
+ });
90
+ }
91
+ }
92
+ return events;
93
+ }
94
+ return [];
95
+ }
96
+ return [{ type: `claude:${type}`, raw: line }];
97
+ }
@@ -0,0 +1,4 @@
1
+ /** Forget the cached lookup — call after an install so a fresh probe
2
+ * can see a binary that was missing at daemon start. */
3
+ export declare function resetCache(): void;
4
+ export declare function findGeminiBinary(): string | null;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ // Locate the `gemini` binary (Google's gemini-cli) on the host.
3
+ // `npm install -g @google/gemini-cli` puts it on PATH as `gemini`
4
+ // (a .cmd shim under %APPDATA%\npm on Windows).
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.resetCache = resetCache;
40
+ exports.findGeminiBinary = findGeminiBinary;
41
+ const path = __importStar(require("path"));
42
+ const os = __importStar(require("os"));
43
+ const win_1 = require("./win");
44
+ const POSIX_CANDIDATES = [
45
+ '/usr/local/bin/gemini',
46
+ '/opt/homebrew/bin/gemini',
47
+ path.join(os.homedir(), '.npm-global/bin/gemini'),
48
+ ];
49
+ const WIN32_CANDIDATES = [
50
+ path.join(os.homedir(), '.npm-global', 'gemini.cmd'),
51
+ ];
52
+ /** Forget the cached lookup — call after an install so a fresh probe
53
+ * can see a binary that was missing at daemon start. */
54
+ function resetCache() { cached = undefined; }
55
+ let cached;
56
+ function findGeminiBinary() {
57
+ if (cached !== undefined)
58
+ return cached;
59
+ cached = (0, win_1.findCliBinary)('gemini', {
60
+ posixCandidates: POSIX_CANDIDATES,
61
+ win32Candidates: WIN32_CANDIDATES,
62
+ });
63
+ return cached;
64
+ }
@@ -0,0 +1,52 @@
1
+ import type { RuntimeEvent, RuntimeEffortLevel } from './types';
2
+ export interface GeminiMcpServer {
3
+ slug: string;
4
+ command: string;
5
+ args?: string[] | null;
6
+ env?: Record<string, string> | null;
7
+ }
8
+ export interface GeminiInput {
9
+ workingDirectory: string;
10
+ prompt: string;
11
+ model?: string;
12
+ resumeSessionId?: string;
13
+ appendSystemPrompt?: string;
14
+ mcpServers?: GeminiMcpServer[];
15
+ /** Per-run reasoning effort. gemini-cli has no verified reasoning-effort
16
+ * CLI flag, so we do NOT forward one (an unsupported flag would fail the
17
+ * spawn). The level is exported as GEMINI_REASONING_EFFORT so an opt-in
18
+ * config/wrapper can read it; if gemini ignores it, behaviour is
19
+ * unchanged. */
20
+ effortLevel?: RuntimeEffortLevel;
21
+ }
22
+ export interface GeminiHandle {
23
+ pid: number | undefined;
24
+ sessionId: string | null;
25
+ kill(): void;
26
+ onEvent(cb: (e: RuntimeEvent) => void): void;
27
+ done: Promise<number>;
28
+ }
29
+ /** gemini-cli session ids are v4 UUIDs (createSessionId → node randomUUID).
30
+ * Only pass a plausibly-gemini id to `--resume`; a foreign id would just be
31
+ * reported "Invalid session identifier" and the run degrades to fresh via the
32
+ * runner's isSessionInvalid clear-and-retry path (see error-classify). */
33
+ export declare function isGeminiSessionId(id: string): boolean;
34
+ /**
35
+ * Parse one gemini-cli v0.52 stream-json line into 0+ RuntimeEvents.
36
+ *
37
+ * VERIFIED against @google/gemini-cli@0.52.0's StreamJsonFormatter
38
+ * (bundle/gemini-*.js). The wire shapes are:
39
+ * INIT { type:'init', session_id, model } ← session id captured at spawn
40
+ * assistant { type:'message', role:'assistant', content:'<text>', delta:true }
41
+ * prompt echo { type:'message', role:'user', ... } ← MUST be skipped
42
+ * tool call { type:'tool_use', tool_id, tool_name, parameters }
43
+ * tool result { type:'tool_result', tool_id, output, status }
44
+ * fatal { type:'result', status:'error', error:{type,message}, stats }
45
+ *
46
+ * The OLD parser matched invented types (content/text/assistant_delta) and
47
+ * invented field names (id/tool_call_id, name/tool, arguments/input,
48
+ * result/content) — none of which gemini emits — so every reply was silently
49
+ * empty and every tool event was mis-shaped.
50
+ */
51
+ export declare function lineToEvents(line: Record<string, unknown>): RuntimeEvent[];
52
+ export declare function spawnGemini(input: GeminiInput): GeminiHandle;
@@ -0,0 +1,369 @@
1
+ "use strict";
2
+ // gemini -p <prompt> --output-format stream-json adapter.
3
+ //
4
+ // Mirrors the kimi / codex / claude adapters: spawns gemini-cli
5
+ // non-interactively, parses the JSONL stream into RuntimeEvents,
6
+ // resolves on exit. The host's ~/.gemini/settings.json is hidden via
7
+ // HOME override so MCP servers from the user's global config don't
8
+ // leak into chats — only the MCPs we explicitly pass land.
9
+ //
10
+ // gemini-cli reads MCP servers from ~/.gemini/settings.json under the
11
+ // `mcpServers` key. We write a sandbox settings file with only our
12
+ // servers and point gemini at it via HOME redirect.
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.isGeminiSessionId = isGeminiSessionId;
48
+ exports.lineToEvents = lineToEvents;
49
+ exports.spawnGemini = spawnGemini;
50
+ const child_process_1 = require("child_process");
51
+ const fs = __importStar(require("fs"));
52
+ const os = __importStar(require("os"));
53
+ const path = __importStar(require("path"));
54
+ const gemini_binary_1 = require("./gemini-binary");
55
+ const win_1 = require("./win");
56
+ const events_1 = require("./events");
57
+ /**
58
+ * Write a workspace-scope `<cwd>/.gemini/settings.json` for THIS spawn.
59
+ *
60
+ * gemini-cli's settings hierarchy is workspace > user — a workspace
61
+ * settings file wins over `~/.gemini/settings.json` field-by-field, so
62
+ * we can override `mcpServers` (the field we care about) without
63
+ * touching the user's home dir.
64
+ *
65
+ * The OLD approach pointed HOME at a temp dir to suppress the host's
66
+ * `~/.gemini/settings.json`. That was destructive: gemini-cli shells out
67
+ * to git, npm, node, and those tools read `~/.gitconfig`, `~/.npmrc`,
68
+ * `~/.netrc` from HOME. Pointing HOME at an empty dir blew them all
69
+ * away → silent tool failures (git push without creds, etc.).
70
+ */
71
+ function writeGeminiWorkspaceSettings(cwd, servers) {
72
+ const dir = path.join(cwd, '.gemini');
73
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
74
+ const mcpServers = {};
75
+ for (const s of servers) {
76
+ if (!s.slug || !s.command)
77
+ continue;
78
+ // `cmd /c` wrapper for npm-shim commands on native Windows (no-op on POSIX).
79
+ const wrapped = (0, win_1.wrapMcpCommandForPlatform)(s.command, Array.isArray(s.args) ? s.args : []);
80
+ mcpServers[s.slug] = {
81
+ command: wrapped.command,
82
+ ...(wrapped.args.length > 0 ? { args: wrapped.args } : {}),
83
+ ...(s.env && Object.keys(s.env).length > 0 ? { env: s.env } : {}),
84
+ };
85
+ }
86
+ // Start with the host's settings so auth method (security.auth.selectedType
87
+ // = oauth-personal etc.) and theme carry through. Override just mcpServers.
88
+ const hostSettingsPath = path.join(os.homedir(), '.gemini', 'settings.json');
89
+ let baseSettings = {};
90
+ if (fs.existsSync(hostSettingsPath)) {
91
+ try {
92
+ baseSettings = JSON.parse(fs.readFileSync(hostSettingsPath, 'utf8'));
93
+ }
94
+ catch { /* corrupt — start fresh */ }
95
+ }
96
+ const merged = { ...baseSettings, mcpServers };
97
+ const settingsPath = path.join(dir, 'settings.json');
98
+ fs.writeFileSync(settingsPath, JSON.stringify(merged, null, 2), { mode: 0o600 });
99
+ return settingsPath;
100
+ }
101
+ /** gemini-cli session ids are v4 UUIDs (createSessionId → node randomUUID).
102
+ * Only pass a plausibly-gemini id to `--resume`; a foreign id would just be
103
+ * reported "Invalid session identifier" and the run degrades to fresh via the
104
+ * runner's isSessionInvalid clear-and-retry path (see error-classify). */
105
+ function isGeminiSessionId(id) {
106
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
107
+ }
108
+ /**
109
+ * Parse one gemini-cli v0.52 stream-json line into 0+ RuntimeEvents.
110
+ *
111
+ * VERIFIED against @google/gemini-cli@0.52.0's StreamJsonFormatter
112
+ * (bundle/gemini-*.js). The wire shapes are:
113
+ * INIT { type:'init', session_id, model } ← session id captured at spawn
114
+ * assistant { type:'message', role:'assistant', content:'<text>', delta:true }
115
+ * prompt echo { type:'message', role:'user', ... } ← MUST be skipped
116
+ * tool call { type:'tool_use', tool_id, tool_name, parameters }
117
+ * tool result { type:'tool_result', tool_id, output, status }
118
+ * fatal { type:'result', status:'error', error:{type,message}, stats }
119
+ *
120
+ * The OLD parser matched invented types (content/text/assistant_delta) and
121
+ * invented field names (id/tool_call_id, name/tool, arguments/input,
122
+ * result/content) — none of which gemini emits — so every reply was silently
123
+ * empty and every tool event was mis-shaped.
124
+ */
125
+ function lineToEvents(line) {
126
+ const type = typeof line.type === 'string' ? line.type : '';
127
+ // Assistant reply. gemini also echoes the user prompt as a role='user'
128
+ // message on the same 'message' type — skip it so we never replay the
129
+ // prompt back as if it were the answer. content is already a plain string.
130
+ if (type === 'message') {
131
+ const role = typeof line.role === 'string' ? line.role : '';
132
+ if (role !== 'assistant')
133
+ return []; // 'user' prompt-echo or other → drop
134
+ const text = typeof line.content === 'string' ? line.content : '';
135
+ return text ? [{ type: 'assistant_text', delta: text }] : [];
136
+ }
137
+ // Tool use — real fields: tool_id, tool_name, parameters.
138
+ if (type === 'tool_use') {
139
+ return [{
140
+ type: 'tool_use',
141
+ id: String(line.tool_id ?? ''),
142
+ name: String(line.tool_name ?? ''),
143
+ input: line.parameters,
144
+ }];
145
+ }
146
+ // Tool result — real fields: tool_id, output, status. Pairs with the
147
+ // tool_use above by tool_id.
148
+ if (type === 'tool_result') {
149
+ const status = typeof line.status === 'string' ? line.status.toLowerCase() : '';
150
+ return [{
151
+ type: 'tool_result',
152
+ id: String(line.tool_id ?? ''),
153
+ content: line.output,
154
+ // Only 'success' is a clean result; anything else (error/cancelled/…)
155
+ // marks the bubble as failed. An absent status stays non-error.
156
+ isError: status !== '' && status !== 'success',
157
+ }];
158
+ }
159
+ // Final result. On a fatal turn gemini sets status='error' and carries the
160
+ // real reason under error.message — surface it as an error event so the
161
+ // runner's lastError reports "429 / auth / model error" instead of exit_N.
162
+ if (type === 'result') {
163
+ const status = typeof line.status === 'string' ? line.status : '';
164
+ if (status === 'error') {
165
+ const message = line.error?.message
166
+ || (typeof line.message === 'string' ? line.message : 'gemini error');
167
+ return [
168
+ { type: 'error', code: 'gemini_error', message },
169
+ { type: 'turn_complete', stopReason: 'failed' },
170
+ ];
171
+ }
172
+ return [{
173
+ type: 'turn_complete',
174
+ usage: line.stats ?? line.usage ?? undefined,
175
+ stopReason: typeof line.stop_reason === 'string' ? line.stop_reason : undefined,
176
+ }];
177
+ }
178
+ // Standalone stream 'error' event. VERIFIED against the v0.52 bundle: this
179
+ // is gemini's NON-FATAL feedback/warning channel. It carries a `severity`
180
+ // of 'warning' OR 'error', and BOTH severities just push to a warnings[]
181
+ // list and CONTINUE the turn (context-compression, policy block,
182
+ // malformed-tool retry, loop-detected, max-session-turns, deprecation
183
+ // notices). Genuine terminal failures arrive as {type:'result',
184
+ // status:'error'} (handled above) or crash the process. The OLD code forced
185
+ // an `error` event + turn_complete{stopReason:'failed'} on EVERY one of
186
+ // these — fabricating a failed turn (and a bogus lastError / chat-UI error
187
+ // bubble) on runs that actually succeeded.
188
+ //
189
+ // Now: never synthesize turn_complete — let the process exit code drive
190
+ // terminal status (session-runner finalizes on exit; lastError is only
191
+ // consulted on a NON-zero exit). Only severity==='error' records a reason
192
+ // (so a subsequent fatal exit — e.g. a stale-resume "Invalid session
193
+ // identifier" printed just before process.exit — reports the real cause and
194
+ // is matchable by isSessionInvalid). severity 'warning'/absent is a benign,
195
+ // log-only passthrough.
196
+ if (type === 'error') {
197
+ const severity = typeof line.severity === 'string' ? line.severity : '';
198
+ if (severity === 'error') {
199
+ const message = line.error?.message
200
+ ?? (typeof line.message === 'string' ? line.message : 'gemini error');
201
+ return [{ type: 'error', code: 'gemini_error', message }];
202
+ }
203
+ return [{ type: 'gemini:warning', raw: line }];
204
+ }
205
+ // Unknown event types (incl. 'init') are tagged with the agent namespace so
206
+ // downstream consumers can filter them. The session_id on 'init' is captured
207
+ // by the stdout reader, not here.
208
+ return [{ type: `gemini:${type || 'unknown'}`, raw: line }];
209
+ }
210
+ function spawnGemini(input) {
211
+ const bin = (0, gemini_binary_1.findGeminiBinary)();
212
+ if (!bin) {
213
+ // bufferedEvents delivers even though the runner attaches its
214
+ // listener after an await (the old queueMicrotask pattern dropped these).
215
+ const events = (0, events_1.bufferedEvents)([
216
+ { type: 'error', code: 'gemini_not_installed', message: 'gemini CLI is not installed on this runtime. Install: npm install -g @google/gemini-cli' },
217
+ { type: 'turn_complete', stopReason: 'failed' },
218
+ ]);
219
+ return {
220
+ pid: undefined,
221
+ sessionId: null,
222
+ kill: () => { },
223
+ onEvent: events.onEvent,
224
+ done: Promise.resolve(127),
225
+ };
226
+ }
227
+ // Write workspace-scope settings (overrides host mcpServers without
228
+ // touching HOME). Auth files in ~/.gemini stay where they are — no
229
+ // symlinking needed since gemini reads them via $HOME, which is now
230
+ // the real one.
231
+ writeGeminiWorkspaceSettings(input.workingDirectory, input.mcpServers ?? []);
232
+ const args = [
233
+ '-p', input.appendSystemPrompt && input.appendSystemPrompt.trim().length > 0
234
+ ? `[system]\n${input.appendSystemPrompt.trim()}\n[/system]\n\n${input.prompt}`
235
+ : input.prompt,
236
+ '--output-format', 'stream-json',
237
+ ];
238
+ if (input.model)
239
+ args.push('-m', input.model);
240
+ // Bypass gemini's "trusted folders" gate. We're a headless runtime
241
+ // running in ephemeral session cwds — there's no user to interactively
242
+ // approve. Either --skip-trust on the CLI OR the env var works; the
243
+ // env var avoids touching the prompt args.
244
+ args.push('--skip-trust');
245
+ // Resume a prior turn's session. gemini-cli v0.52 DOES support this:
246
+ // `--resume <uuid>` (SessionSelector.findSession matches by session id
247
+ // first, then index). The session store is keyed by the cwd-derived project
248
+ // hash, and continuation turns reuse the prior turn's working_directory
249
+ // (session-runner resolveCwdAndProject), so the prior session's chats/*.jsonl
250
+ // is discoverable — same mechanism claude/grok resume rely on. If the id is
251
+ // stale/foreign, gemini prints "Invalid session identifier" and exits
252
+ // non-zero → the runner's isSessionInvalid clears the id and retries fresh.
253
+ if (input.resumeSessionId && isGeminiSessionId(input.resumeSessionId)) {
254
+ args.push('--resume', input.resumeSessionId);
255
+ }
256
+ // No HOME override — gemini's auth + shell-out tools all need the
257
+ // real $HOME. Workspace-scope settings.json (written above) is enough
258
+ // to override the host's mcpServers without poisoning everything else.
259
+ const childEnv = {
260
+ ...process.env,
261
+ TERM: 'dumb',
262
+ // Belt and suspenders alongside --skip-trust — some gemini code
263
+ // paths check this env var directly.
264
+ GEMINI_CLI_TRUST_WORKSPACE: 'true',
265
+ // Surface the requested reasoning effort without betting on a CLI flag
266
+ // that may not exist; ignored harmlessly if gemini doesn't read it.
267
+ ...(input.effortLevel ? { GEMINI_REASONING_EFFORT: input.effortLevel } : {}),
268
+ };
269
+ const listeners = [];
270
+ const emit = (e) => { for (const l of listeners)
271
+ try {
272
+ l(e);
273
+ }
274
+ catch { } };
275
+ let sessionId = null;
276
+ let buffer = '';
277
+ let proc;
278
+ try {
279
+ // npm .cmd shim → node script (win32); identity on POSIX. Both this
280
+ // resolution and spawn() itself can throw synchronously on Windows —
281
+ // fail the request cleanly instead of crashing the daemon.
282
+ const inv = (0, win_1.resolveCliInvocation)(bin, args);
283
+ proc = (0, child_process_1.spawn)(inv.file, inv.args, {
284
+ cwd: input.workingDirectory,
285
+ env: childEnv,
286
+ stdio: ['ignore', 'pipe', 'pipe'],
287
+ });
288
+ }
289
+ catch (err) {
290
+ const msg = err.message || String(err);
291
+ const events = (0, events_1.bufferedEvents)([
292
+ { type: 'error', code: 'gemini_spawn_failed', message: msg },
293
+ { type: 'turn_complete', stopReason: 'failed' },
294
+ ]);
295
+ return {
296
+ pid: undefined,
297
+ sessionId: null,
298
+ kill: () => { },
299
+ onEvent: events.onEvent,
300
+ done: Promise.resolve(-1),
301
+ };
302
+ }
303
+ proc.stdout?.on('data', (chunk) => {
304
+ buffer += chunk.toString('utf8');
305
+ let nl;
306
+ while ((nl = buffer.indexOf('\n')) !== -1) {
307
+ const raw = buffer.slice(0, nl).trim();
308
+ buffer = buffer.slice(nl + 1);
309
+ if (!raw)
310
+ continue;
311
+ let parsed;
312
+ try {
313
+ parsed = JSON.parse(raw);
314
+ }
315
+ catch {
316
+ continue;
317
+ }
318
+ if (typeof parsed.session_id === 'string' && !sessionId)
319
+ sessionId = parsed.session_id;
320
+ for (const ev of lineToEvents(parsed))
321
+ emit(ev);
322
+ }
323
+ });
324
+ proc.stderr?.on('data', (chunk) => {
325
+ const txt = chunk.toString('utf8').trim();
326
+ if (txt)
327
+ emit({ type: 'error', code: 'gemini_stderr', message: txt.slice(0, 4096) });
328
+ });
329
+ const done = new Promise((resolve, reject) => {
330
+ proc.once('error', reject);
331
+ let exitCode = -1;
332
+ proc.once('exit', (code) => { exitCode = code ?? -1; });
333
+ // Resolve on 'close' (all stdio EOF), NOT 'exit'. gemini prints its fatal
334
+ // reason — most importantly a stale-resume "Error resuming session:
335
+ // Invalid session identifier" / "No previous sessions found" — as a FINAL
336
+ // stdout stream-json error line and then process.exit()s immediately.
337
+ // Resolving on 'exit' can finalize the request before that last stdout
338
+ // 'data' event is processed, leaving lastError null → the runner reports a
339
+ // bare exit_N and isSessionInvalid never sees the message to self-heal.
340
+ // 'close' fires only after stdout/stderr have been fully delivered, so the
341
+ // error line is parsed and lastError is set first.
342
+ proc.once('close', () => {
343
+ // Flush any trailing line that had no newline terminator.
344
+ const raw = buffer.trim();
345
+ buffer = '';
346
+ if (raw) {
347
+ try {
348
+ const parsed = JSON.parse(raw);
349
+ if (typeof parsed.session_id === 'string' && !sessionId)
350
+ sessionId = parsed.session_id;
351
+ for (const ev of lineToEvents(parsed))
352
+ emit(ev);
353
+ }
354
+ catch { /* ignore a partial/non-JSON tail */ }
355
+ }
356
+ // No HOME-temp-dir to clean up anymore. The workspace settings file in
357
+ // <cwd>/.gemini/settings.json sits with the rest of the ephemeral cwd
358
+ // and is cleaned by the session dispose path.
359
+ resolve(exitCode);
360
+ });
361
+ });
362
+ return {
363
+ pid: proc.pid,
364
+ get sessionId() { return sessionId; },
365
+ kill: () => { (0, win_1.killProcessTree)(proc); },
366
+ onEvent: (cb) => { listeners.push(cb); },
367
+ done,
368
+ };
369
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Give a freshly cloned repo the ENTITY's git identity, and optionally credit
3
+ * the person who asked for the work.
4
+ *
5
+ * Without this, commits land under whatever global git config the host machine
6
+ * happens to have — usually the machine's owner, which is wrong and confusing
7
+ * when several entities share a node.
8
+ *
9
+ * Identity comes from runtime_get_git_identity rather than the request row: six
10
+ * different RPCs create those rows and threading two more columns through all of
11
+ * them is needless fragility.
12
+ *
13
+ * On avatars: GitHub derives a commit's avatar from the email, so an entity's
14
+ * own photo cannot appear there without a real GitHub account per entity. The
15
+ * co-author trailer is what puts a real avatar and a real GitHub identity on the
16
+ * commit, which is why it defaults on.
17
+ */
18
+ export declare function applyGitIdentity(dir: string, requestId: string, token: string): Promise<void>;