@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,467 @@
1
+ "use strict";
2
+ // Install MCPs + skills + packages into a session's working directory
3
+ // before the agent spawns.
4
+ //
5
+ // MCPs end up in a temp .mcp.json that Claude picks up via
6
+ // --mcp-config <path> --strict-mcp-config
7
+ // (no global ~/.claude.json mutation — installs are session-scoped and
8
+ // vanish when the request finishes).
9
+ //
10
+ // Skills are written into <cwd>/.claude/skills/<slug>/SKILL.md (+ any
11
+ // companion files). Claude's CLI scans that directory automatically.
12
+ //
13
+ // Packages are resolved server-side: runtime_resolve_install_set expands
14
+ // each package into its constituent mcp_ids + skill_ids before returning.
15
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ var desc = Object.getOwnPropertyDescriptor(m, k);
18
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
+ desc = { enumerable: true, get: function() { return m[k]; } };
20
+ }
21
+ Object.defineProperty(o, k2, desc);
22
+ }) : (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ o[k2] = m[k];
25
+ }));
26
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
28
+ }) : function(o, v) {
29
+ o["default"] = v;
30
+ });
31
+ var __importStar = (this && this.__importStar) || (function () {
32
+ var ownKeys = function(o) {
33
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34
+ var ar = [];
35
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
+ return ar;
37
+ };
38
+ return ownKeys(o);
39
+ };
40
+ return function (mod) {
41
+ if (mod && mod.__esModule) return mod;
42
+ var result = {};
43
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
+ __setModuleDefault(result, mod);
45
+ return result;
46
+ };
47
+ })();
48
+ Object.defineProperty(exports, "__esModule", { value: true });
49
+ exports.LEGACY_MCP_CONFIG_FILENAME = exports.MCP_CONFIG_FILENAME = void 0;
50
+ exports.install = install;
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const supabase_client_1 = require("./supabase-client");
54
+ const store_1 = require("./store");
55
+ const win_1 = require("./win");
56
+ /** Hidden per-session MCP config written next to the agent's working dir. */
57
+ exports.MCP_CONFIG_FILENAME = '.ainode.mcp.json';
58
+ /** Pre-rename filename; removed when found so agents don't read a stale copy. */
59
+ exports.LEGACY_MCP_CONFIG_FILENAME = '.entities-runtime.mcp.json';
60
+ const NOOP_RESULT = {
61
+ mcpConfigPath: null,
62
+ skillSlugs: [],
63
+ mcps: [],
64
+ dispose: async () => { },
65
+ };
66
+ function token() {
67
+ return (0, store_1.readPairing)()?.daemonToken ?? null;
68
+ }
69
+ async function fetchResolved(set, requestId) {
70
+ const t = token();
71
+ if (!t)
72
+ return { mcps: [], skills: [] };
73
+ if (set.mcpIds.length === 0 && set.skillIds.length === 0 && set.packageIds.length === 0) {
74
+ return { mcps: [], skills: [] };
75
+ }
76
+ // p_request_id pins credential resolution to THIS request. Without it the
77
+ // server guesses "latest in-flight request on the runtime", which under
78
+ // concurrent runs (schedules + chat turns + distils) can hand one entity
79
+ // ANOTHER request's credentials — the 2026-07-21 wrong-GitHub-PAT incident.
80
+ const baseArgs = {
81
+ p_token: t,
82
+ p_mcp_ids: set.mcpIds,
83
+ p_skill_ids: set.skillIds,
84
+ p_package_ids: set.packageIds,
85
+ };
86
+ let out;
87
+ try {
88
+ out = await (0, supabase_client_1.rpc)('runtime_resolve_install_set', requestId ? { ...baseArgs, p_request_id: requestId } : baseArgs);
89
+ }
90
+ catch (err) {
91
+ // A DB that predates the p_request_id migration rejects the unknown
92
+ // parameter (PostgREST 404 "function not found"). Degrade to the legacy
93
+ // runtime-wide heuristic rather than failing the whole install.
94
+ if (requestId && err instanceof supabase_client_1.RpcError && err.status === 404) {
95
+ out = await (0, supabase_client_1.rpc)('runtime_resolve_install_set', baseArgs);
96
+ }
97
+ else {
98
+ throw err;
99
+ }
100
+ }
101
+ return {
102
+ mcps: Array.isArray(out?.mcps) ? out.mcps : [],
103
+ skills: Array.isArray(out?.skills) ? out.skills : [],
104
+ };
105
+ }
106
+ function writeMcpConfig(cwd, mcps) {
107
+ if (mcps.length === 0)
108
+ return null;
109
+ const servers = {};
110
+ for (const m of mcps) {
111
+ if (!m.slug || !m.command)
112
+ continue;
113
+ // Remote MCP servers: registry rows use command='http' (or 'sse') with
114
+ // args=[url]. An OAuth bearer token rides in the credential as
115
+ // OAUTH_ACCESS_TOKEN and becomes the Authorization header — this is how
116
+ // headless sessions use account-authenticated remote MCPs (e.g. the
117
+ // official Higgsfield server) without an interactive OAuth dance.
118
+ if (m.command === 'http' || m.command === 'sse') {
119
+ const url = Array.isArray(m.args) && typeof m.args[0] === 'string' ? m.args[0] : null;
120
+ if (!url)
121
+ continue;
122
+ const entry = { type: m.command, url };
123
+ const headers = {};
124
+ const token = m.env?.OAUTH_ACCESS_TOKEN;
125
+ if (token)
126
+ headers.Authorization = `Bearer ${token}`;
127
+ // Other env keys become literal headers (so remote MCPs with custom
128
+ // header auth work), but OAUTH_* keys are token bookkeeping (refresh
129
+ // token, expiry) — never send them as request headers.
130
+ for (const [k, v] of Object.entries(m.env || {})) {
131
+ if (!v || k.startsWith('OAUTH_'))
132
+ continue;
133
+ headers[k.replace(/_/g, '-')] = v;
134
+ }
135
+ if (Object.keys(headers).length > 0)
136
+ entry.headers = headers;
137
+ servers[m.slug] = entry;
138
+ continue;
139
+ }
140
+ // On native Windows, registry commands like `npx` are npm .cmd shims
141
+ // that the agent CLI can't CreateProcess directly — the documented fix
142
+ // is a `cmd /c` wrapper. No-op on POSIX.
143
+ const wrapped = (0, win_1.wrapMcpCommandForPlatform)(m.command, Array.isArray(m.args) ? m.args : []);
144
+ const entry = {
145
+ command: wrapped.command,
146
+ args: wrapped.args,
147
+ };
148
+ if (m.env && Object.keys(m.env).length > 0) {
149
+ entry.env = m.env;
150
+ }
151
+ servers[m.slug] = entry;
152
+ }
153
+ if (Object.keys(servers).length === 0)
154
+ return null;
155
+ // Use a hidden sibling file so we can identify + clean up later.
156
+ // The daemon scopes these files per request id; the caller passes a
157
+ // unique cwd directory or we'd race ourselves on concurrent sessions
158
+ // in the same project.
159
+ const target = path.join(cwd, exports.MCP_CONFIG_FILENAME);
160
+ // A session dir reused from before the rename can still hold the old file;
161
+ // leaving it would let an agent read a stale server list.
162
+ try {
163
+ fs.rmSync(path.join(cwd, exports.LEGACY_MCP_CONFIG_FILENAME), { force: true });
164
+ }
165
+ catch { /* best effort */ }
166
+ fs.writeFileSync(target, JSON.stringify({ mcpServers: servers }, null, 2));
167
+ return target;
168
+ }
169
+ function writeSkills(cwd, skills, skillVarValues) {
170
+ if (skills.length === 0)
171
+ return { slugs: [], paths: [] };
172
+ const skillsRoot = path.join(cwd, '.claude', 'skills');
173
+ fs.mkdirSync(skillsRoot, { recursive: true });
174
+ const slugs = [];
175
+ const paths = [];
176
+ for (const s of skills) {
177
+ if (!s.slug)
178
+ continue;
179
+ const dir = path.join(skillsRoot, s.slug);
180
+ fs.mkdirSync(dir, { recursive: true });
181
+ // SKILL.md is the standard entrypoint Claude scans for.
182
+ const skillMd = path.join(dir, 'SKILL.md');
183
+ const body = buildSkillMarkdown(s, skillVarValues?.[s.id]);
184
+ fs.writeFileSync(skillMd, body);
185
+ paths.push(skillMd);
186
+ // Companion files (if any). f.path is server-supplied — resolve and
187
+ // verify containment so '../..'-style or absolute paths can never
188
+ // escape the skill dir (the old prefix-strip only removed ONE '../').
189
+ if (Array.isArray(s.files)) {
190
+ for (const f of s.files) {
191
+ if (!f.path)
192
+ continue;
193
+ const safeRel = f.path.replace(/^\.\.?[\\/]+/g, '').replace(/[\\/]+/g, path.sep);
194
+ const dest = path.resolve(dir, safeRel);
195
+ if (!dest.startsWith(path.resolve(dir) + path.sep)) {
196
+ console.error(`[install] skipping skill file with escaping path: ${f.path} (skill ${s.slug})`);
197
+ continue;
198
+ }
199
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
200
+ fs.writeFileSync(dest, f.content ?? '');
201
+ paths.push(dest);
202
+ }
203
+ }
204
+ slugs.push(s.slug);
205
+ }
206
+ return { slugs, paths };
207
+ }
208
+ function buildSkillMarkdown(s, values) {
209
+ // Substitute {{var.<name>}} tokens before wrapping in frontmatter so
210
+ // values can appear anywhere in the skill body.
211
+ const content = applySkillVars(s.content ?? '', s, values ?? {});
212
+ // Skills typically ship with YAML frontmatter that Claude reads
213
+ // (name + description). Synthesise it if the raw content lacks one.
214
+ const trimmed = content.trimStart();
215
+ if (trimmed.startsWith('---'))
216
+ return content;
217
+ return `---\nname: ${s.slug}\ndescription: ${(s.description ?? '').replace(/\n/g, ' ')}\n---\n\n${content}`;
218
+ }
219
+ /**
220
+ * Substitute {{var.<name>}} tokens in a skill's content with the effective
221
+ * value for that variable. The effective map is the skill's variable
222
+ * DEFAULTS overlaid by the provided per-node values. Unknown tokens (no
223
+ * matching var definition or value) resolve to an empty string and log a
224
+ * console.debug so a typo'd token is traceable.
225
+ */
226
+ function applySkillVars(content, skill, values) {
227
+ // Build the effective map: defaults first, then overlay provided values.
228
+ const effective = {};
229
+ if (Array.isArray(skill.variables)) {
230
+ for (const v of skill.variables) {
231
+ if (v && typeof v.name === 'string') {
232
+ effective[v.name] = v.default != null ? String(v.default) : '';
233
+ }
234
+ }
235
+ }
236
+ for (const [k, val] of Object.entries(values || {})) {
237
+ effective[k] = val != null ? String(val) : '';
238
+ }
239
+ return content.replace(/\{\{\s*var\.([a-z0-9_]+)\s*\}\}/gi, (_m, name) => {
240
+ const key = name.toLowerCase();
241
+ if (Object.prototype.hasOwnProperty.call(effective, key))
242
+ return effective[key];
243
+ console.debug(`[install] skill var token {{var.${name}}} has no matching variable in skill ${skill.slug} (${skill.id})`);
244
+ return '';
245
+ });
246
+ }
247
+ /**
248
+ * Per-cwd install cache. Continuation turns in the same chat usually
249
+ * hit the same cwd with the same install set; re-resolving + rewriting
250
+ * .mcp.json each time wastes ~50-100ms. Cache keyed by (cwd, sorted ids
251
+ * hash, mcp_credentials).
252
+ *
253
+ * Bounded LRU — without this, long-lived daemons would accumulate one
254
+ * entry per distinct cwd ever seen, each holding the full ResolvedMcp /
255
+ * ResolvedSkill payloads (skill content can be tens of KB each). Map
256
+ * iteration order is insertion order, so the LRU dance is delete-and-set
257
+ * on access + delete-oldest when exceeding capacity.
258
+ */
259
+ const INSTALL_CACHE_MAX = 64;
260
+ const installCache = new Map();
261
+ function touchInstallCache(cwd, entry) {
262
+ // Move-to-end: delete + set re-positions in iteration order.
263
+ installCache.delete(cwd);
264
+ installCache.set(cwd, entry);
265
+ while (installCache.size > INSTALL_CACHE_MAX) {
266
+ const oldest = installCache.keys().next().value;
267
+ if (oldest === undefined)
268
+ break;
269
+ installCache.delete(oldest);
270
+ }
271
+ }
272
+ function cacheKey(cwd, set, credentials, envOverrides, skillVarValues) {
273
+ const sorted = (arr) => [...(arr || [])].sort().join(',');
274
+ const credsHash = credentials ? JSON.stringify(credentials) : '';
275
+ const envHash = envOverrides ? JSON.stringify(envOverrides) : '';
276
+ // Hash only the values for skills actually in this install set so two
277
+ // installs with the same effective skill content share a cache entry,
278
+ // and a DIFFERENT set of values forces a fresh SKILL.md write instead of
279
+ // reusing a stale one.
280
+ let varsHash = '';
281
+ if (skillVarValues && set.skillIds.length > 0) {
282
+ const relevant = {};
283
+ for (const id of [...set.skillIds].sort()) {
284
+ if (skillVarValues[id])
285
+ relevant[id] = skillVarValues[id];
286
+ }
287
+ varsHash = JSON.stringify(relevant);
288
+ }
289
+ return `${cwd}|${sorted(set.mcpIds)}|${sorted(set.skillIds)}|${sorted(set.packageIds)}|${credsHash}|${envHash}|${varsHash}`;
290
+ }
291
+ /**
292
+ * Run install side-effects against `cwd` and return paths to clean up
293
+ * later. Safe to call with an empty set — returns a no-op result.
294
+ */
295
+ async function install(cwd, set, credentials,
296
+ /** Per-session env overrides keyed by MCP slug. Merged into each resolved
297
+ * MCP's env BEFORE writing .mcp.json, overriding credential-supplied
298
+ * values for the same env var. Used by widgetstudio for dynamic
299
+ * streaming auth tokens that can't live in a static vault credential. */
300
+ envOverrides,
301
+ /** Per-skill variable values keyed by skill id → { varName: value }.
302
+ * Overlaid onto each skill's variable defaults and substituted into the
303
+ * skill content via {{var.<name>}} tokens before SKILL.md is written. */
304
+ skillVarValues,
305
+ /** The entity_runtime_requests row being installed. Passed to the server
306
+ * so credential resolution is scoped to THIS request instead of the
307
+ * latest in-flight request on the runtime. */
308
+ requestId) {
309
+ if (set.mcpIds.length === 0 && set.skillIds.length === 0 && set.packageIds.length === 0) {
310
+ return NOOP_RESULT;
311
+ }
312
+ // Cache lookup — if this cwd was just installed with the same set,
313
+ // reuse the files on disk. Make the dispose a no-op so back-to-back
314
+ // turns don't tear down + rebuild. envOverrides is part of the cache key
315
+ // because two sessions with different dynamic tokens must NOT share.
316
+ const key = cacheKey(cwd, set, credentials, envOverrides, skillVarValues);
317
+ const cached = installCache.get(cwd);
318
+ if (cached && cached.key === key) {
319
+ // Verify the files actually exist (defensive — could have been
320
+ // cleaned by a previous turn's late dispose).
321
+ if (!cached.result.mcpConfigPath || fs.existsSync(cached.result.mcpConfigPath)) {
322
+ // Two kinds of content resolve from LIVE server state that the cache
323
+ // key does NOT capture, so a plain cache hit can serve stale bytes
324
+ // until the daemon restarts:
325
+ // 1. MCP env values — the key hashes credential IDs, not their
326
+ // values, so a rotated OAuth token (Higgsfield) or a vault-edited
327
+ // PAT (GitHub) reads as a hit against a stale .mcp.json.
328
+ // 2. Skill bodies — the key hashes skill IDs + var values, not the
329
+ // SKILL.md content, so editing a skill body never reaches a
330
+ // running daemon.
331
+ // So on EVERY cache hit re-resolve (one RPC) and rewrite whichever of
332
+ // the two the install has: the .mcp.json and/or the SKILL.md files.
333
+ // fetchResolved returns both, so covering skills adds no extra RPC when
334
+ // MCPs are already being refreshed.
335
+ const hasMcps = (cached.result.mcps || []).length > 0;
336
+ const hasSkills = (cached.result.skillSlugs || []).length > 0;
337
+ if (hasMcps || hasSkills) {
338
+ try {
339
+ const fresh = await fetchResolved(set, requestId);
340
+ let freshMcpPath = cached.result.mcpConfigPath;
341
+ let freshMcps = cached.result.mcps;
342
+ if (hasMcps) {
343
+ if (envOverrides) {
344
+ for (const m of fresh.mcps) {
345
+ const ov = envOverrides[m.slug];
346
+ if (ov)
347
+ m.env = { ...(m.env || {}), ...ov };
348
+ }
349
+ }
350
+ freshMcps = fresh.mcps;
351
+ freshMcpPath = writeMcpConfig(cwd, fresh.mcps) ?? cached.result.mcpConfigPath;
352
+ }
353
+ let freshSkillSlugs = cached.result.skillSlugs;
354
+ if (hasSkills) {
355
+ // Skill files write to slug-deterministic paths, so re-writing
356
+ // overwrites the same files the cached dispose already tracks.
357
+ freshSkillSlugs = writeSkills(cwd, fresh.skills, skillVarValues).slugs;
358
+ }
359
+ const refreshed = {
360
+ ...cached.result,
361
+ mcpConfigPath: freshMcpPath,
362
+ mcps: freshMcps,
363
+ skillSlugs: freshSkillSlugs,
364
+ };
365
+ touchInstallCache(cwd, { result: refreshed, key });
366
+ return { ...refreshed, dispose: async () => { } };
367
+ }
368
+ catch (err) {
369
+ // Transient resolve failure — fall back to the cached config rather
370
+ // than failing the whole install; next run tries again.
371
+ console.error('[install] cache-hit refresh failed, using cached config:', err.message);
372
+ }
373
+ }
374
+ // Mark as recently used so LRU keeps it.
375
+ touchInstallCache(cwd, cached);
376
+ return { ...cached.result, dispose: async () => { } };
377
+ }
378
+ }
379
+ let resolved;
380
+ try {
381
+ resolved = await fetchResolved(set, requestId);
382
+ }
383
+ catch (err) {
384
+ console.error('[install] resolve failed:', err.message);
385
+ return NOOP_RESULT;
386
+ }
387
+ // Merge per-session env overrides into each MCP's env. Override semantics
388
+ // (overrides win on key conflict) — that's the whole point: a dynamic
389
+ // token must replace any stale value from the credential.
390
+ if (envOverrides) {
391
+ for (const m of resolved.mcps) {
392
+ const ov = envOverrides[m.slug];
393
+ if (!ov)
394
+ continue;
395
+ m.env = { ...(m.env || {}), ...ov };
396
+ }
397
+ }
398
+ const mcpConfigPath = writeMcpConfig(cwd, resolved.mcps);
399
+ const skills = writeSkills(cwd, resolved.skills, skillVarValues);
400
+ const result = {
401
+ mcpConfigPath,
402
+ skillSlugs: skills.slugs,
403
+ mcps: resolved.mcps,
404
+ dispose: async () => {
405
+ // We only clean up when a DIFFERENT install set has explicitly
406
+ // replaced us in the cache — i.e. `current.key !== key` AND
407
+ // current is defined. If `current` is undefined (cache was wiped
408
+ // by LRU eviction, daemon restart, etc.) we DO NOT delete: the
409
+ // session dir might still be in active use, and the daemon's
410
+ // startup sweep handles 7-day-old orphans separately.
411
+ //
412
+ // The previous code deleted on `current === undefined`, which
413
+ // bit us when a reclaim retry's dispose fired after the in-memory
414
+ // cache had been wiped (e.g., daemon restart between install +
415
+ // request completion). The .mcp.json got unlinked before the
416
+ // NEXT pickup of the same request could even read it.
417
+ const current = installCache.get(cwd);
418
+ if (!current)
419
+ return; // cache wiped — be defensive, don't delete
420
+ if (current.key === key)
421
+ return; // still us — keep file
422
+ // Cache now points at a different install set for this cwd → safe to clean.
423
+ if (mcpConfigPath) {
424
+ try {
425
+ fs.unlinkSync(mcpConfigPath);
426
+ }
427
+ catch { /* already gone */ }
428
+ }
429
+ for (const p of skills.paths) {
430
+ try {
431
+ fs.unlinkSync(p);
432
+ }
433
+ catch { }
434
+ }
435
+ try {
436
+ rmdirRecursiveIfEmpty(path.join(cwd, '.claude', 'skills'));
437
+ }
438
+ catch { }
439
+ try {
440
+ rmdirRecursiveIfEmpty(path.join(cwd, '.claude'));
441
+ }
442
+ catch { }
443
+ },
444
+ };
445
+ // Replace any previous cache entry for this cwd; the old dispose
446
+ // will run on its own request's `finally`.
447
+ touchInstallCache(cwd, { result, key });
448
+ return result;
449
+ }
450
+ function rmdirRecursiveIfEmpty(dir) {
451
+ if (!fs.existsSync(dir))
452
+ return;
453
+ // First try removing all empty subdirs depth-first.
454
+ for (const name of fs.readdirSync(dir)) {
455
+ const child = path.join(dir, name);
456
+ try {
457
+ const stat = fs.statSync(child);
458
+ if (stat.isDirectory())
459
+ rmdirRecursiveIfEmpty(child);
460
+ }
461
+ catch { }
462
+ }
463
+ try {
464
+ fs.rmdirSync(dir);
465
+ }
466
+ catch { /* not empty — leave it */ }
467
+ }
@@ -0,0 +1,20 @@
1
+ export interface JsonlTailOptions {
2
+ timeoutMs?: number;
3
+ onTimeout?: () => void;
4
+ pollMs?: number;
5
+ /** When true and the file already exists, seek to the end before
6
+ * tailing so prior contents are not replayed. Use this when attaching
7
+ * to a resumed claude session — the JSONL persists across runs and
8
+ * re-emitting old events causes the daemon to misinterpret them as
9
+ * current-turn activity. Default false. */
10
+ seekToEnd?: boolean;
11
+ }
12
+ export interface JsonlTailHandle {
13
+ dispose(): void;
14
+ }
15
+ /**
16
+ * Tail a JSON-lines file. Calls `onLine` for each newline-terminated
17
+ * JSON object as it appears. Survives the file not existing yet (Claude
18
+ * creates the transcript only after the first turn round-trips).
19
+ */
20
+ export declare function startJsonlTail(filePath: string, onLine: (line: Record<string, unknown>) => void, opts?: JsonlTailOptions): JsonlTailHandle;
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.startJsonlTail = startJsonlTail;
37
+ const fs = __importStar(require("fs"));
38
+ /**
39
+ * Tail a JSON-lines file. Calls `onLine` for each newline-terminated
40
+ * JSON object as it appears. Survives the file not existing yet (Claude
41
+ * creates the transcript only after the first turn round-trips).
42
+ */
43
+ function startJsonlTail(filePath, onLine, opts = {}) {
44
+ const pollMs = opts.pollMs ?? 50;
45
+ let offset = 0;
46
+ let buffer = '';
47
+ let disposed = false;
48
+ let watcher;
49
+ let initialPoll;
50
+ let timeoutTimer;
51
+ let fallbackPoll;
52
+ function feed(chunk) {
53
+ buffer += chunk;
54
+ let nl;
55
+ while ((nl = buffer.indexOf('\n')) !== -1) {
56
+ const raw = buffer.slice(0, nl).trim();
57
+ buffer = buffer.slice(nl + 1);
58
+ if (!raw)
59
+ continue;
60
+ try {
61
+ onLine(JSON.parse(raw));
62
+ }
63
+ catch {
64
+ /* tolerate malformed lines */
65
+ }
66
+ }
67
+ }
68
+ function readMore() {
69
+ if (disposed)
70
+ return;
71
+ let fd;
72
+ try {
73
+ const stat = fs.statSync(filePath);
74
+ // File-truncation guard: if the file is smaller than our offset
75
+ // (claude rewrote / rotated the JSONL) we'd otherwise stall
76
+ // forever — `stat.size <= offset` keeps short-circuiting reads.
77
+ // Reset the offset to read from the new top of the file.
78
+ if (stat.size < offset)
79
+ offset = 0;
80
+ if (stat.size <= offset)
81
+ return;
82
+ fd = fs.openSync(filePath, 'r');
83
+ const length = stat.size - offset;
84
+ const buf = Buffer.alloc(length);
85
+ fs.readSync(fd, buf, 0, length, offset);
86
+ offset = stat.size;
87
+ feed(buf.toString('utf8'));
88
+ }
89
+ catch {
90
+ /* deleted or transient */
91
+ }
92
+ finally {
93
+ if (fd !== undefined)
94
+ try {
95
+ fs.closeSync(fd);
96
+ }
97
+ catch { }
98
+ }
99
+ }
100
+ function attachWatcher() {
101
+ if (disposed || watcher)
102
+ return;
103
+ try {
104
+ watcher = fs.watch(filePath, { persistent: false }, () => readMore());
105
+ }
106
+ catch {
107
+ /* fallback poll below covers a watcher that never attached */
108
+ }
109
+ // Safety net alongside (or instead of) the watcher: fs.watch coalesces,
110
+ // delays, or outright drops append events on some platforms — win32's
111
+ // ReadDirectoryChangesW especially, and network volumes everywhere. A
112
+ // cheap statSync every 500ms guarantees the tail always drains, and it
113
+ // also covers the case where fs.watch threw and was never retried.
114
+ if (!fallbackPoll) {
115
+ fallbackPoll = setInterval(() => readMore(), 500);
116
+ fallbackPoll.unref?.();
117
+ }
118
+ }
119
+ function pollForExistence() {
120
+ if (disposed)
121
+ return;
122
+ if (fs.existsSync(filePath)) {
123
+ if (timeoutTimer) {
124
+ clearTimeout(timeoutTimer);
125
+ timeoutTimer = undefined;
126
+ }
127
+ if (opts.seekToEnd) {
128
+ // Skip everything that's already in the file. The watcher will
129
+ // pick up new lines from this point on. Without this, resuming
130
+ // a claude session replays the entire prior transcript as if it
131
+ // were fresh activity — the daemon's session-runner can't tell
132
+ // a replayed `claude:last-prompt` from a real one and fires
133
+ // turn_complete the moment the spawn boots.
134
+ try {
135
+ const stat = fs.statSync(filePath);
136
+ offset = stat.size;
137
+ }
138
+ catch { /* fall back to offset=0 */ }
139
+ }
140
+ else {
141
+ readMore();
142
+ }
143
+ attachWatcher();
144
+ return;
145
+ }
146
+ initialPoll = setTimeout(pollForExistence, pollMs);
147
+ }
148
+ pollForExistence();
149
+ if (opts.timeoutMs && opts.onTimeout) {
150
+ timeoutTimer = setTimeout(() => {
151
+ if (disposed || watcher)
152
+ return;
153
+ // File hasn't appeared yet — surface the timeout as a UX warning
154
+ // but KEEP polling. claude TUI is sometimes slow to write the
155
+ // first line (large system prompt parse, slow MCP startup, agent
156
+ // taking >30s to produce a token); we don't want to permanently
157
+ // abandon the file. The spawn's onExit will dispose the tail
158
+ // when claude eventually exits.
159
+ opts.onTimeout?.();
160
+ }, opts.timeoutMs);
161
+ }
162
+ return {
163
+ dispose() {
164
+ // FINAL SYNCHRONOUS DRAIN before teardown. fs.watch coalesces and the
165
+ // 500ms fallback poll can both miss the very last append, so tearing
166
+ // down here without a final read loses the closing JSONL line — the
167
+ // final assistant_text / result — truncating the reply. readMore()
168
+ // is fully synchronous (statSync/openSync/readSync → feed → onLine)
169
+ // and short-circuits once disposed=true, so it MUST run BEFORE we set
170
+ // the flag. dispose() is called synchronously from the spawn's exit
171
+ // handler (session-runner onExit → tail.dispose()), so by the time
172
+ // the process has exited the file is fully flushed and this reads it
173
+ // to EOF in one pass.
174
+ try {
175
+ readMore();
176
+ }
177
+ catch { /* best-effort: never throw from teardown */ }
178
+ disposed = true;
179
+ if (initialPoll)
180
+ clearTimeout(initialPoll);
181
+ if (timeoutTimer)
182
+ clearTimeout(timeoutTimer);
183
+ if (fallbackPoll)
184
+ clearInterval(fallbackPoll);
185
+ if (watcher)
186
+ try {
187
+ watcher.close();
188
+ }
189
+ catch { }
190
+ },
191
+ };
192
+ }
@@ -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 findKimiBinary(): string | null;