@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.
- package/README.md +96 -0
- package/dist/agent-auth.d.ts +9 -0
- package/dist/agent-auth.js +96 -0
- package/dist/attachments.d.ts +20 -0
- package/dist/attachments.js +164 -0
- package/dist/capabilities.d.ts +64 -0
- package/dist/capabilities.js +406 -0
- package/dist/claude-binary.d.ts +16 -0
- package/dist/claude-binary.js +145 -0
- package/dist/claude-config.d.ts +11 -0
- package/dist/claude-config.js +144 -0
- package/dist/claude-print.d.ts +35 -0
- package/dist/claude-print.js +312 -0
- package/dist/claude-spawn.d.ts +40 -0
- package/dist/claude-spawn.js +220 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +137 -0
- package/dist/codex-binary.d.ts +4 -0
- package/dist/codex-binary.js +70 -0
- package/dist/codex-spawn.d.ts +47 -0
- package/dist/codex-spawn.js +320 -0
- package/dist/command-runner.d.ts +9 -0
- package/dist/command-runner.js +592 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +38 -0
- package/dist/control-server.d.ts +5 -0
- package/dist/control-server.js +292 -0
- package/dist/diskguard.d.ts +96 -0
- package/dist/diskguard.js +435 -0
- package/dist/error-classify.d.ts +20 -0
- package/dist/error-classify.js +159 -0
- package/dist/events.d.ts +26 -0
- package/dist/events.js +97 -0
- package/dist/gemini-binary.d.ts +4 -0
- package/dist/gemini-binary.js +64 -0
- package/dist/gemini-spawn.d.ts +52 -0
- package/dist/gemini-spawn.js +369 -0
- package/dist/git-identity.d.ts +18 -0
- package/dist/git-identity.js +104 -0
- package/dist/grok-binary.d.ts +4 -0
- package/dist/grok-binary.js +70 -0
- package/dist/grok-spawn.d.ts +41 -0
- package/dist/grok-spawn.js +495 -0
- package/dist/guardrails.d.ts +32 -0
- package/dist/guardrails.js +125 -0
- package/dist/hang-watchdog.d.ts +17 -0
- package/dist/hang-watchdog.js +48 -0
- package/dist/harness-registry.d.ts +55 -0
- package/dist/harness-registry.js +180 -0
- package/dist/heartbeat.d.ts +13 -0
- package/dist/heartbeat.js +95 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +440 -0
- package/dist/install.d.ts +63 -0
- package/dist/install.js +467 -0
- package/dist/jsonl-tail.d.ts +20 -0
- package/dist/jsonl-tail.js +192 -0
- package/dist/kimi-binary.d.ts +4 -0
- package/dist/kimi-binary.js +68 -0
- package/dist/kimi-spawn.d.ts +47 -0
- package/dist/kimi-spawn.js +319 -0
- package/dist/lockfile.d.ts +30 -0
- package/dist/lockfile.js +169 -0
- package/dist/mcp-config.d.ts +23 -0
- package/dist/mcp-config.js +133 -0
- package/dist/memory-capture.d.ts +20 -0
- package/dist/memory-capture.js +45 -0
- package/dist/memory-pack.d.ts +34 -0
- package/dist/memory-pack.js +171 -0
- package/dist/model-filter.d.ts +4 -0
- package/dist/model-filter.js +34 -0
- package/dist/node-health.d.ts +18 -0
- package/dist/node-health.js +74 -0
- package/dist/pairing.d.ts +20 -0
- package/dist/pairing.js +129 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +48 -0
- package/dist/precompact-hook.d.ts +27 -0
- package/dist/precompact-hook.js +136 -0
- package/dist/projects.d.ts +25 -0
- package/dist/projects.js +255 -0
- package/dist/prompt-keystrokes.d.ts +3 -0
- package/dist/prompt-keystrokes.js +56 -0
- package/dist/pty-helper.d.ts +26 -0
- package/dist/pty-helper.js +156 -0
- package/dist/request-pump.d.ts +12 -0
- package/dist/request-pump.js +249 -0
- package/dist/run-retry.d.ts +28 -0
- package/dist/run-retry.js +81 -0
- package/dist/self-update.d.ts +75 -0
- package/dist/self-update.js +192 -0
- package/dist/session-runner.d.ts +130 -0
- package/dist/session-runner.js +2271 -0
- package/dist/sleep-detector.d.ts +11 -0
- package/dist/sleep-detector.js +32 -0
- package/dist/state-migrate.d.ts +8 -0
- package/dist/state-migrate.js +91 -0
- package/dist/store.d.ts +5 -0
- package/dist/store.js +82 -0
- package/dist/supabase-client.d.ts +15 -0
- package/dist/supabase-client.js +119 -0
- package/dist/time-context.d.ts +10 -0
- package/dist/time-context.js +71 -0
- package/dist/tokens.d.ts +4 -0
- package/dist/tokens.js +33 -0
- package/dist/tui/app.d.ts +35 -0
- package/dist/tui/app.js +82 -0
- package/dist/tui/console-capture.d.ts +13 -0
- package/dist/tui/console-capture.js +62 -0
- package/dist/tui/dashboard.d.ts +42 -0
- package/dist/tui/dashboard.js +205 -0
- package/dist/tui/data.d.ts +57 -0
- package/dist/tui/data.js +62 -0
- package/dist/tui/harnesses.d.ts +16 -0
- package/dist/tui/harnesses.js +354 -0
- package/dist/tui/render.d.ts +21 -0
- package/dist/tui/render.js +116 -0
- package/dist/tui/requests.d.ts +18 -0
- package/dist/tui/requests.js +127 -0
- package/dist/tui/run.d.ts +13 -0
- package/dist/tui/run.js +178 -0
- package/dist/tui/transcript.d.ts +39 -0
- package/dist/tui/transcript.js +234 -0
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +314 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +3 -0
- package/dist/win.d.ts +67 -0
- package/dist/win.js +317 -0
- package/package.json +60 -0
- package/scripts/fix-pty-helper.js +28 -0
- package/scripts/precompact-capture.js +292 -0
- 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,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>;
|