@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
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ // Pre-seeds claude's own config so the TUI doesn't block on
3
+ // interactive dialogs the daemon can't answer.
4
+ //
5
+ // Two dialogs to clear:
6
+ //
7
+ // 1. "WARNING: Claude Code running in Bypass Permissions mode" — fires
8
+ // when --dangerously-skip-permissions (or any bypass-mode flag) is
9
+ // passed. Claude persists user acceptance in
10
+ // ~/.claude/settings.json → "skipDangerousModePermissionPrompt": true
11
+ // Daemon writes this once on boot so the dialog never appears for
12
+ // daemon-spawned claudes.
13
+ //
14
+ // 2. "Are you sure you want to trust this directory?" — per-cwd dialog
15
+ // that fires the first time claude opens a project directory.
16
+ // Acceptance is keyed by absolute path under ~/.claude.json:
17
+ // .projects["<abs-cwd>"].hasTrustDialogAccepted = true
18
+ // Daemon creates a fresh ephemeral cwd per request, so we write that
19
+ // entry just before spawning claude.
20
+ //
21
+ // Both writes are best-effort and idempotent. A read/parse failure
22
+ // causes a silent skip — claude will fall back to the dialog (the
23
+ // existing failure mode), but the daemon won't crash.
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.ensureBypassPermissionsAccepted = ensureBypassPermissionsAccepted;
59
+ exports.preTrustProject = preTrustProject;
60
+ const fs = __importStar(require("fs"));
61
+ const os = __importStar(require("os"));
62
+ const path = __importStar(require("path"));
63
+ const win_1 = require("./win");
64
+ const CLAUDE_CONFIG = path.join(os.homedir(), '.claude.json');
65
+ const CLAUDE_SETTINGS = path.join(os.homedir(), '.claude', 'settings.json');
66
+ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
67
+ function atomicWrite(target, body) {
68
+ const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
69
+ fs.writeFileSync(tmp, body, { mode: 0o600 });
70
+ // ~/.claude.json is often held open by a running claude session — on
71
+ // Windows a rename over an open file fails EPERM/EBUSY; retried there.
72
+ (0, win_1.safeRenameSync)(tmp, target);
73
+ }
74
+ function readJson(target) {
75
+ try {
76
+ const raw = fs.readFileSync(target, 'utf-8');
77
+ return JSON.parse(raw);
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ /**
84
+ * Make sure ~/.claude/settings.json has skipDangerousModePermissionPrompt: true.
85
+ * Idempotent: if the key is already true, no write.
86
+ */
87
+ function ensureBypassPermissionsAccepted() {
88
+ try {
89
+ if (!fs.existsSync(CLAUDE_DIR))
90
+ fs.mkdirSync(CLAUDE_DIR, { recursive: true, mode: 0o700 });
91
+ const current = readJson(CLAUDE_SETTINGS) ?? {};
92
+ if (current.skipDangerousModePermissionPrompt === true)
93
+ return;
94
+ const next = { ...current, skipDangerousModePermissionPrompt: true };
95
+ atomicWrite(CLAUDE_SETTINGS, JSON.stringify(next, null, 2));
96
+ }
97
+ catch (err) {
98
+ console.error(`[claude-config] failed to pre-accept bypass dialog: ${err.message}`);
99
+ }
100
+ }
101
+ /**
102
+ * Add an entry to ~/.claude.json `.projects[cwd]` marking the cwd as
103
+ * trusted and onboarding-seen, so claude TUI doesn't block on either
104
+ * dialog when launched in this directory. Existing fields are preserved.
105
+ */
106
+ function preTrustProject(cwd) {
107
+ try {
108
+ const config = readJson(CLAUDE_CONFIG);
109
+ if (!config) {
110
+ // ~/.claude.json is missing or unparseable. We can't pre-trust
111
+ // without it, but creating one from scratch would clobber state
112
+ // claude expects to manage itself. Log so an unexpected trust
113
+ // dialog isn't a silent mystery.
114
+ if (fs.existsSync(CLAUDE_CONFIG)) {
115
+ console.error(`[claude-config] ~/.claude.json exists but is unparseable; trust dialog will fire for ${cwd}`);
116
+ }
117
+ return;
118
+ }
119
+ const projects = (config.projects && typeof config.projects === 'object'
120
+ ? config.projects
121
+ : {});
122
+ const existing = projects[cwd] || {};
123
+ if (existing.hasTrustDialogAccepted === true && existing.projectOnboardingSeenCount > 0) {
124
+ return; // already pre-trusted
125
+ }
126
+ projects[cwd] = {
127
+ allowedTools: [],
128
+ mcpContextUris: [],
129
+ mcpServers: {},
130
+ enabledMcpjsonServers: [],
131
+ disabledMcpjsonServers: [],
132
+ hasClaudeMdExternalIncludesApproved: false,
133
+ hasClaudeMdExternalIncludesWarningShown: true,
134
+ ...existing,
135
+ hasTrustDialogAccepted: true,
136
+ projectOnboardingSeenCount: Math.max(existing.projectOnboardingSeenCount ?? 0, 1),
137
+ };
138
+ const next = { ...config, projects };
139
+ atomicWrite(CLAUDE_CONFIG, JSON.stringify(next, null, 2));
140
+ }
141
+ catch (err) {
142
+ console.error(`[claude-config] failed to pre-trust ${cwd}: ${err.message}`);
143
+ }
144
+ }
@@ -0,0 +1,35 @@
1
+ import type { RuntimeEvent, RuntimePermissionMode, RuntimeEffortLevel } from './types';
2
+ export interface ClaudePrintInput {
3
+ workingDirectory: string;
4
+ prompt: string;
5
+ permissionMode?: RuntimePermissionMode;
6
+ effortLevel?: RuntimeEffortLevel;
7
+ model?: string;
8
+ mcpConfigPath?: string;
9
+ /** Continue an existing session via --resume <id>. */
10
+ resumeSessionId?: string;
11
+ /** Studio-entity system prompt appended onto Claude's defaults. */
12
+ appendSystemPrompt?: string;
13
+ /** Restrict to a specific set of tools (claude only). null/undefined = inherit. */
14
+ allowedTools?: string[];
15
+ }
16
+ export interface ClaudePrintHandle {
17
+ pid: number | undefined;
18
+ sessionId: string | null;
19
+ kill(): void;
20
+ onEvent(cb: (e: RuntimeEvent) => void): void;
21
+ /** Resolves with exit code; rejects on spawn error. */
22
+ done: Promise<number>;
23
+ }
24
+ /**
25
+ * Parse one stream-json line into 0+ RuntimeEvents.
26
+ *
27
+ * `suppressText`: when partial-message streaming is on, the incremental
28
+ * text_delta stream_events already carry the full text of each turn. Claude
29
+ * ALSO emits the aggregated `assistant` message line at turn end — its text
30
+ * blocks would double the reply (the server appends every assistant_text event
31
+ * to the message content). So we skip the final text blocks and keep only
32
+ * tool_use. Text is delivered purely as deltas.
33
+ */
34
+ export declare function lineToEvents(line: Record<string, unknown>, suppressText?: boolean): RuntimeEvent[];
35
+ export declare function spawnClaudePrint(input: ClaudePrintInput): ClaudePrintHandle;
@@ -0,0 +1,312 @@
1
+ "use strict";
2
+ // claude --print --output-format stream-json adapter.
3
+ //
4
+ // Spawns Claude non-interactively, parses each JSONL line on stdout into
5
+ // our normalised RuntimeEvent shape, and resolves when the process exits.
6
+ // No PTY needed — Claude detects non-TTY and runs headless.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.lineToEvents = lineToEvents;
9
+ exports.spawnClaudePrint = spawnClaudePrint;
10
+ const child_process_1 = require("child_process");
11
+ const claude_binary_1 = require("./claude-binary");
12
+ const win_1 = require("./win");
13
+ /**
14
+ * Parse one stream-json line into 0+ RuntimeEvents.
15
+ *
16
+ * `suppressText`: when partial-message streaming is on, the incremental
17
+ * text_delta stream_events already carry the full text of each turn. Claude
18
+ * ALSO emits the aggregated `assistant` message line at turn end — its text
19
+ * blocks would double the reply (the server appends every assistant_text event
20
+ * to the message content). So we skip the final text blocks and keep only
21
+ * tool_use. Text is delivered purely as deltas.
22
+ */
23
+ function lineToEvents(line, suppressText = false) {
24
+ const type = typeof line.type === 'string' ? line.type : '';
25
+ if (type === 'assistant') {
26
+ const msg = line.message;
27
+ const content = msg?.content;
28
+ const out = [];
29
+ if (Array.isArray(content)) {
30
+ for (const b of content) {
31
+ if (b.type === 'text' && typeof b.text === 'string') {
32
+ if (!suppressText)
33
+ out.push({ type: 'assistant_text', delta: b.text });
34
+ }
35
+ else if (b.type === 'tool_use') {
36
+ out.push({
37
+ type: 'tool_use',
38
+ id: String(b.id ?? ''),
39
+ name: String(b.name ?? ''),
40
+ input: b.input,
41
+ });
42
+ }
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+ if (type === 'user') {
48
+ const msg = line.message;
49
+ const content = msg?.content;
50
+ if (!Array.isArray(content))
51
+ return [];
52
+ const out = [];
53
+ for (const b of content) {
54
+ if (b.type === 'tool_result') {
55
+ out.push({
56
+ type: 'tool_result',
57
+ id: String(b.tool_use_id ?? ''),
58
+ content: b.content,
59
+ isError: b.is_error === true,
60
+ });
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+ if (type === 'result') {
66
+ // claude can exit 0 while reporting a failed result: is_error:true (e.g.
67
+ // a usage-limit banner delivered as the "reply") or an error subtype
68
+ // (error_max_turns / error_during_execution). Mapping every result to a
69
+ // clean turn_complete would finalize that banner as the answer. Surface
70
+ // an error event (→ lastError → non-clean finalize) BEFORE turn_complete
71
+ // so the run is flagged instead of silently completing.
72
+ const subtype = typeof line.subtype === 'string' ? line.subtype : undefined;
73
+ const isErrResult = line.is_error === true ||
74
+ (subtype != null && subtype !== 'success' && subtype.startsWith('error'));
75
+ const events = [];
76
+ if (isErrResult) {
77
+ const text = typeof line.result === 'string' ? line.result.trim() : '';
78
+ events.push({
79
+ type: 'error',
80
+ // `claude_result_*` prefix is how runClaudePrint distinguishes a
81
+ // terminal result-error from transient mid-stream error lines.
82
+ code: subtype ? `claude_result_${subtype}` : 'claude_result_error',
83
+ message: (text || `claude finished with an error result${subtype ? ` (${subtype})` : ''}`).slice(0, 4096),
84
+ });
85
+ }
86
+ events.push({
87
+ type: 'turn_complete',
88
+ usage: line.usage ?? undefined,
89
+ stopReason: subtype ?? (typeof line.stop_reason === 'string' ? line.stop_reason : undefined),
90
+ });
91
+ return events;
92
+ }
93
+ return [{ type: `claude:${type || 'unknown'}`, raw: line }];
94
+ }
95
+ function spawnClaudePrint(input) {
96
+ const bin = (0, claude_binary_1.findClaudeBinary)();
97
+ // No claude on this runtime — fail THIS request cleanly rather than
98
+ // spawning a bare 'claude' that ENOENTs and crashes the daemon. Both
99
+ // callers (runClaudePrint, runGuardrailRetryTurn) wrap this in try/catch
100
+ // and turn the throw into a per-request 'spawn_failed'.
101
+ if (!bin) {
102
+ throw new Error('claude CLI not found on this runtime — install it from Studio → Runtimes → Harnesses, ' +
103
+ 'or run `npm i -g @anthropic-ai/claude-code`.');
104
+ }
105
+ // Token-level streaming. When on, Claude emits incremental content_block_delta
106
+ // stream_events as it generates, so the chat draft grows in real time instead
107
+ // of jumping in whole-turn lumps. Kill switch: set ADDAI_STREAM_PARTIAL=0 and
108
+ // restart the daemon to fall straight back to whole-turn behaviour — no code
109
+ // change or redeploy needed. Default on.
110
+ const partialStream = process.env.ADDAI_STREAM_PARTIAL !== '0';
111
+ const args = [
112
+ '--print',
113
+ '--output-format', 'stream-json',
114
+ '--verbose',
115
+ // 'user'/'local' leak the host's ~/.claude/settings.local.json mcpServers
116
+ // through --strict-mcp-config. Restrict to 'project'.
117
+ '--setting-sources', 'project',
118
+ ];
119
+ if (partialStream)
120
+ args.push('--include-partial-messages');
121
+ if (input.permissionMode === 'bypassPermissions') {
122
+ args.push('--dangerously-skip-permissions');
123
+ }
124
+ else if (input.permissionMode) {
125
+ args.push('--permission-mode', input.permissionMode);
126
+ }
127
+ if (input.effortLevel)
128
+ args.push('--effort', input.effortLevel);
129
+ if (input.model)
130
+ args.push('--model', input.model);
131
+ if (input.mcpConfigPath) {
132
+ args.push('--mcp-config', input.mcpConfigPath, '--strict-mcp-config');
133
+ }
134
+ if (input.resumeSessionId) {
135
+ args.push('--resume', input.resumeSessionId);
136
+ }
137
+ if (input.appendSystemPrompt && input.appendSystemPrompt.trim().length > 0) {
138
+ args.push('--append-system-prompt', input.appendSystemPrompt);
139
+ }
140
+ if (Array.isArray(input.allowedTools) && input.allowedTools.length > 0) {
141
+ // Claude expects a comma-separated list. We skip the flag entirely
142
+ // for an empty array — otherwise `--allowed-tools ` (empty value)
143
+ // would consume the NEXT argv (the prompt) as its value, leaving
144
+ // claude with no prompt and an unparseable tool list. Empty array =
145
+ // "inherit defaults", same as undefined.
146
+ args.push('--allowed-tools', input.allowedTools.join(','));
147
+ }
148
+ // NOTE: no --image flag here — the claude CLI has never supported one
149
+ // (0.2.36 shipped it and every image message died with exit 1). Vision
150
+ // input is handled upstream: session-runner downloads attachment URLs
151
+ // into the cwd (see attachments.ts) and the agent Reads them.
152
+ args.push(input.prompt);
153
+ // On Windows the discovered binary can be an npm .cmd shim, which Node
154
+ // refuses to spawn without a shell (EINVAL) — and a shell would route the
155
+ // prompt text through cmd.exe. resolveCliInvocation unwraps the shim to
156
+ // its node script instead (identity on POSIX). It can throw; so can
157
+ // spawn() itself (synchronously, on win32). Surface either as a normal
158
+ // rejected `done` so callers keep their single error path.
159
+ let proc;
160
+ try {
161
+ const inv = (0, win_1.resolveCliInvocation)(bin, args);
162
+ proc = (0, child_process_1.spawn)(inv.file, inv.args, {
163
+ cwd: input.workingDirectory,
164
+ env: { ...process.env, TERM: 'dumb' },
165
+ stdio: ['ignore', 'pipe', 'pipe'],
166
+ });
167
+ }
168
+ catch (err) {
169
+ const done = Promise.reject(err instanceof Error ? err : new Error(String(err)));
170
+ // Mark handled now — the caller awaits `done` a few ticks later, and an
171
+ // already-rejected promise with no handler would fire unhandledRejection.
172
+ done.catch(() => { });
173
+ return { pid: undefined, sessionId: null, kill: () => { }, onEvent: () => { }, done };
174
+ }
175
+ const listeners = [];
176
+ const emit = (e) => { for (const l of listeners)
177
+ try {
178
+ l(e);
179
+ }
180
+ catch { } };
181
+ let sessionId = null;
182
+ let buffer = '';
183
+ // Text-delta coalescer (partial-stream mode). Token deltas arrive many times
184
+ // per second; emitting one DB event each would flood runtime_event_emit — the
185
+ // same write path that has wedged the daemon under load. Instead we accumulate
186
+ // deltas and flush at most every FLUSH_MS into a single assistant_text event:
187
+ // smooth to watch, ~1-2 orders of magnitude fewer writes. Boundaries (block /
188
+ // message end, turn result, process exit) force a flush so no text is stranded.
189
+ const FLUSH_MS = 300;
190
+ let textBuf = '';
191
+ let flushTimer = null;
192
+ let sawDeltaThisMsg = false;
193
+ // Thinking deltas (extended thinking) — coalesced lazily into 'thinking'
194
+ // events; the chat relay renders them as a collapsed section.
195
+ const THINK_FLUSH_MS = 700;
196
+ let thinkBuf = '';
197
+ let thinkTimer = null;
198
+ const flushThinking = () => {
199
+ if (thinkTimer) {
200
+ clearTimeout(thinkTimer);
201
+ thinkTimer = null;
202
+ }
203
+ if (thinkBuf) {
204
+ const t = thinkBuf;
205
+ thinkBuf = '';
206
+ emit({ type: 'thinking', delta: t });
207
+ }
208
+ };
209
+ const flushText = () => {
210
+ if (flushTimer) {
211
+ clearTimeout(flushTimer);
212
+ flushTimer = null;
213
+ }
214
+ if (textBuf) {
215
+ const t = textBuf;
216
+ textBuf = '';
217
+ emit({ type: 'assistant_text', delta: t });
218
+ }
219
+ };
220
+ const scheduleFlush = () => { if (!flushTimer)
221
+ flushTimer = setTimeout(flushText, FLUSH_MS); };
222
+ const handleLine = (parsed) => {
223
+ const type = typeof parsed.type === 'string' ? parsed.type : '';
224
+ if (partialStream && type === 'stream_event') {
225
+ const sev = parsed.event;
226
+ const st = sev?.type;
227
+ if (st === 'message_start') {
228
+ sawDeltaThisMsg = false;
229
+ return;
230
+ }
231
+ if (st === 'content_block_delta') {
232
+ const d = sev?.delta;
233
+ if (d?.type === 'text_delta' && typeof d.text === 'string' && d.text.length > 0) {
234
+ flushThinking(); // thoughts precede the text they produced
235
+ textBuf += d.text;
236
+ sawDeltaThisMsg = true;
237
+ scheduleFlush();
238
+ }
239
+ else if (d?.type === 'thinking_delta' && typeof d.thinking === 'string' && d.thinking.length > 0) {
240
+ thinkBuf += d.thinking;
241
+ if (thinkBuf.length >= 1500)
242
+ flushThinking();
243
+ else if (!thinkTimer)
244
+ thinkTimer = setTimeout(flushThinking, THINK_FLUSH_MS);
245
+ }
246
+ return; // input_json_delta (tool args) etc. — ignored; tools come from the complete line
247
+ }
248
+ if (st === 'content_block_stop' || st === 'message_stop' || st === 'message_delta') {
249
+ flushThinking();
250
+ flushText();
251
+ return;
252
+ }
253
+ return; // ping and other bookkeeping stream_events
254
+ }
255
+ // Complete message lines. In partial mode, flush buffered deltas first so
256
+ // event order (text → tool_use) is preserved, then suppress the aggregated
257
+ // text blocks the deltas already delivered — unless, defensively, no delta
258
+ // was seen for this message, in which case we keep the block (never lose text).
259
+ if (partialStream && (type === 'assistant' || type === 'user' || type === 'result'))
260
+ flushText();
261
+ const suppressText = partialStream && sawDeltaThisMsg && type === 'assistant';
262
+ for (const ev of lineToEvents(parsed, suppressText))
263
+ emit(ev);
264
+ };
265
+ proc.stdout?.on('data', (chunk) => {
266
+ buffer += chunk.toString('utf8');
267
+ let nl;
268
+ while ((nl = buffer.indexOf('\n')) !== -1) {
269
+ const raw = buffer.slice(0, nl).trim();
270
+ buffer = buffer.slice(nl + 1);
271
+ if (!raw)
272
+ continue;
273
+ let parsed;
274
+ try {
275
+ parsed = JSON.parse(raw);
276
+ }
277
+ catch {
278
+ continue;
279
+ }
280
+ if (typeof parsed.session_id === 'string' && !sessionId)
281
+ sessionId = parsed.session_id;
282
+ handleLine(parsed);
283
+ }
284
+ });
285
+ // Buffer stderr rather than emitting an `error` event per line: claude
286
+ // writes benign warnings (deprecation notices, MCP startup chatter) to
287
+ // stderr on perfectly successful runs, and each one used to render as an
288
+ // error bubble in chat. Surface it only if the run actually fails.
289
+ let stderrBuf = '';
290
+ proc.stderr?.on('data', (chunk) => {
291
+ stderrBuf += chunk.toString('utf8');
292
+ });
293
+ const done = new Promise((resolve, reject) => {
294
+ proc.once('error', reject);
295
+ proc.once('exit', (code) => {
296
+ flushThinking();
297
+ flushText();
298
+ const trimmed = stderrBuf.trim();
299
+ if ((code ?? -1) !== 0 && trimmed) {
300
+ emit({ type: 'error', code: 'claude_stderr', message: trimmed.slice(0, 4096) });
301
+ }
302
+ resolve(code ?? -1);
303
+ });
304
+ });
305
+ return {
306
+ pid: proc.pid,
307
+ get sessionId() { return sessionId; },
308
+ kill: () => { (0, win_1.killProcessTree)(proc); },
309
+ onEvent: (cb) => { listeners.push(cb); },
310
+ done,
311
+ };
312
+ }
@@ -0,0 +1,40 @@
1
+ import type { RuntimePermissionMode, RuntimeEffortLevel } from './types';
2
+ export interface SpawnedClaude {
3
+ ptyId: string;
4
+ sessionId: string;
5
+ jsonlPath: string;
6
+ write: (data: string) => void;
7
+ kill: () => void;
8
+ onData: (cb: (chunk: string) => void) => void;
9
+ onExit: (cb: (code: number) => void) => void;
10
+ }
11
+ export interface SpawnInput {
12
+ workingDirectory: string;
13
+ enabledMcps?: string[];
14
+ permissionMode?: RuntimePermissionMode;
15
+ effortLevel?: RuntimeEffortLevel;
16
+ initialPrompt?: string;
17
+ /** Pre-built .mcp.json path from install.ts. Bypasses the local-catalog
18
+ * resolution so registry-resolved MCPs work without prior install. */
19
+ mcpConfigPathOverride?: string;
20
+ /** When set, attach this session to an existing Claude session id
21
+ * via --resume <id> instead of starting a fresh one. */
22
+ resumeSessionId?: string;
23
+ /** Studio-entity system prompt appended onto Claude's defaults. */
24
+ appendSystemPrompt?: string;
25
+ /** Restrict to a specific set of tools. null/undefined = inherit. */
26
+ allowedTools?: string[];
27
+ /** Override claude's default model. Maps to --model <id>. */
28
+ model?: string | null;
29
+ /**
30
+ * Phase 4 T7: extra env vars layered on top of the daemon's own
31
+ * process.env for THIS spawn only (never mutates process.env itself).
32
+ * Currently used to thread ENTITY_STUDIO_ENTITY_ID / RUNTIME_REQUEST_ID
33
+ * through to the PreCompact hook script (scripts/precompact-capture.js),
34
+ * which runs as a grandchild of this spawn and has no other way to learn
35
+ * which request/entity it's running for. Undefined/omitted for every
36
+ * caller that doesn't pass it — byte-identical spawn env otherwise.
37
+ */
38
+ extraEnv?: Record<string, string>;
39
+ }
40
+ export declare function spawnClaudeForRuntime(input: SpawnInput): SpawnedClaude;