@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/install.js
ADDED
|
@@ -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
|
+
}
|