@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,592 @@
1
+ "use strict";
2
+ // Executes runtime_commands issued from Entity Studio: install a harness
3
+ // (npm i -g) or log one in (PTY-bridged OAuth / API key). Woken by the
4
+ // heartbeat's pending_commands hint — no always-on poller. One command
5
+ // runs at a time; while one is live we poll its row every ~2s so the
6
+ // login conversation (URL out, code back) feels interactive.
7
+ //
8
+ // Truth contract: login "success" is only reported after a capabilities
9
+ // re-probe says authed=true. The CLI's exit code is a hint, the probe is
10
+ // the verdict.
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.setRestartHook = setRestartHook;
46
+ exports.wake = wake;
47
+ const child_process_1 = require("child_process");
48
+ const fs = __importStar(require("fs"));
49
+ const os = __importStar(require("os"));
50
+ const path = __importStar(require("path"));
51
+ const pty = __importStar(require("node-pty"));
52
+ const supabase_client_1 = require("./supabase-client");
53
+ const store_1 = require("./store");
54
+ const capabilities_1 = require("./capabilities");
55
+ const heartbeat_1 = require("./heartbeat");
56
+ const win_1 = require("./win");
57
+ const harness_registry_1 = require("./harness-registry");
58
+ const self_update_1 = require("./self-update");
59
+ const paths_1 = require("./paths");
60
+ const pty_helper_1 = require("./pty-helper");
61
+ const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
62
+ /** Own version, read the same way index.ts does. Resolved here rather than
63
+ * imported from index.ts, which already imports this module. */
64
+ const VERSION = (() => {
65
+ try {
66
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
67
+ const pkg = require('../package.json');
68
+ if (typeof pkg.version === 'string' && pkg.version.length > 0)
69
+ return pkg.version;
70
+ }
71
+ catch { /* fall through */ }
72
+ return '0.0.0';
73
+ })();
74
+ /** Where did npm -g just put the bin? `npm prefix -g` + /bin/<name>
75
+ * (posix) or <prefix>\\<name>.cmd (win). Returns the path if it exists. */
76
+ async function npmGlobalBinFor(name) {
77
+ const inv = (0, win_1.resolveCliInvocation)('npm', ['prefix', '-g']);
78
+ const prefix = await new Promise(resolve => {
79
+ try {
80
+ const child = (0, child_process_1.spawn)(inv.file, inv.args, { env: process.env, windowsHide: true, timeout: 15_000 });
81
+ let out = '';
82
+ child.stdout?.on('data', b => { out += b.toString('utf8'); });
83
+ child.on('error', () => resolve(null));
84
+ child.on('exit', code => resolve(code === 0 ? out.trim() : null));
85
+ }
86
+ catch {
87
+ resolve(null);
88
+ }
89
+ });
90
+ if (!prefix)
91
+ return null;
92
+ const candidates = process.platform === 'win32'
93
+ ? [path.join(prefix, `${name}.cmd`), path.join(prefix, `${name}.exe`)]
94
+ : [path.join(prefix, 'bin', name)];
95
+ for (const c of candidates) {
96
+ try {
97
+ if (fs.statSync(c).isFile())
98
+ return c;
99
+ }
100
+ catch { /* next */ }
101
+ }
102
+ return null;
103
+ }
104
+ const INPUT_POLL_MS = 2_000;
105
+ const LOG_TAIL_CHARS = 4_000;
106
+ const LOG_PUSH_MS = 1_000;
107
+ let running = false;
108
+ let restartHook = null;
109
+ function setRestartHook(fn) { restartHook = fn; }
110
+ function token() {
111
+ return (0, store_1.readPairing)()?.daemonToken ?? null;
112
+ }
113
+ async function update(id, status, payload, error) {
114
+ const t = token();
115
+ if (!t)
116
+ return;
117
+ try {
118
+ await (0, supabase_client_1.rpc)('runtime_command_update', {
119
+ p_token: t, p_id: id,
120
+ p_status: status, p_payload: payload ?? null, p_error: error ?? null,
121
+ });
122
+ }
123
+ catch (err) {
124
+ console.error('[commands] update failed:', err.message);
125
+ }
126
+ }
127
+ async function pollRow(id) {
128
+ const t = token();
129
+ if (!t)
130
+ return null;
131
+ try {
132
+ // runtime_command_poll RETURNS TABLE → PostgREST responds with an
133
+ // ARRAY of rows. Treating it as a bare object made status/input read
134
+ // as undefined, so the daemon polled forever, blind to pasted codes
135
+ // and cancels (live incident: gemini Google login stuck on
136
+ // awaiting_input with the code sitting on the row).
137
+ const rows = await (0, supabase_client_1.rpc)('runtime_command_poll', { p_token: t, p_id: id });
138
+ if (Array.isArray(rows))
139
+ return rows[0] ?? null;
140
+ return rows ?? null;
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ }
146
+ /** Wait until Studio supplies input (or cancel/timeout). Returns null on
147
+ * cancel or deadline; polls every INPUT_POLL_MS. */
148
+ async function awaitInput(id, deadline, have) {
149
+ for (;;) {
150
+ if (Date.now() > deadline)
151
+ return null;
152
+ const row = await pollRow(id);
153
+ if (!row || row.status === 'canceled')
154
+ return null;
155
+ const v = row.input ? have(row.input) : undefined;
156
+ if (v && v.trim())
157
+ return v.trim();
158
+ await new Promise(r => setTimeout(r, INPUT_POLL_MS));
159
+ }
160
+ }
161
+ async function reprobeAuthed(id) {
162
+ const caps = await (0, capabilities_1.probeCapabilities)();
163
+ const h = caps[id];
164
+ return { authed: !!h?.authed, version: h?.version };
165
+ }
166
+ /* ── install ─────────────────────────────────────────────────────────── */
167
+ async function runInstall(cmd, spec) {
168
+ if (!(0, harness_registry_1.isInstallable)(spec.id)) {
169
+ await update(cmd.id, 'failed', { instructions: spec.login.instructions }, `${spec.label} has no npm installer — install it on the device.`);
170
+ return;
171
+ }
172
+ await update(cmd.id, 'running', { step: 'installing', log: `$ npm install -g ${spec.npmPackage}\n` });
173
+ const state = { log: `$ npm install -g ${spec.npmPackage}\n`, lastPush: 0 };
174
+ const runNpm = (extraArgs) => new Promise(resolve => {
175
+ let settled = false;
176
+ const done = (code) => { if (!settled) {
177
+ settled = true;
178
+ resolve(code);
179
+ } };
180
+ let child;
181
+ const inv = (0, win_1.resolveCliInvocation)('npm', ['install', '-g', ...extraArgs, spec.npmPackage]);
182
+ try {
183
+ child = (0, child_process_1.spawn)(inv.file, inv.args, { env: process.env, windowsHide: true });
184
+ }
185
+ catch (err) {
186
+ state.log += `spawn failed: ${err.message}\n`;
187
+ done(-1);
188
+ return;
189
+ }
190
+ const timer = setTimeout(() => { try {
191
+ child.kill('SIGKILL');
192
+ }
193
+ catch { /* gone */ } done(-2); }, COMMAND_TIMEOUT_MS);
194
+ const onChunk = (b) => {
195
+ state.log = (state.log + b.toString('utf8')).slice(-LOG_TAIL_CHARS);
196
+ const now = Date.now();
197
+ if (now - state.lastPush > LOG_PUSH_MS) {
198
+ state.lastPush = now;
199
+ void update(cmd.id, null, { log: state.log });
200
+ }
201
+ };
202
+ child.stdout?.on('data', onChunk);
203
+ child.stderr?.on('data', onChunk);
204
+ child.on('error', err => { state.log += `\n${err.message}\n`; clearTimeout(timer); done(-1); });
205
+ child.on('exit', code => { clearTimeout(timer); done(code ?? -1); });
206
+ });
207
+ let exitCode = await runNpm([]);
208
+ // Root-owned global prefix (classic Mac /usr/local): retry into the
209
+ // daemon's own toolchain prefix — the binary finders search its bin dir.
210
+ if (exitCode !== 0 && /EACCES/.test(state.log)) {
211
+ const toolsPrefix = path.join(paths_1.RUNTIME_HOME, 'tools');
212
+ try {
213
+ fs.mkdirSync(toolsPrefix, { recursive: true });
214
+ }
215
+ catch { /* spawn will surface it */ }
216
+ state.log = state.log.slice(-1500)
217
+ + `\n\nGlobal npm prefix is not writable — retrying into ${toolsPrefix}\n`;
218
+ await update(cmd.id, null, { log: state.log });
219
+ exitCode = await runNpm(['--prefix', toolsPrefix]);
220
+ }
221
+ if (exitCode !== 0) {
222
+ await update(cmd.id, 'failed', { log: state.log }, exitCode === -2 ? 'install timed out after 10 minutes' : `npm exited with code ${exitCode}`);
223
+ return;
224
+ }
225
+ // Verify the binary actually resolves now — npm can "succeed" into a
226
+ // PATH the daemon doesn't search. Probe first (it resets the finder
227
+ // caches, which otherwise still hold the pre-install miss), then fall
228
+ // back to asking npm where its global bin dir actually is.
229
+ const probe = await reprobeAuthed(spec.id);
230
+ let bin = spec.findBinary();
231
+ if (!bin)
232
+ bin = await npmGlobalBinFor(spec.id);
233
+ await (0, heartbeat_1.beat)();
234
+ if (!bin) {
235
+ await update(cmd.id, 'failed', { log: state.log }, 'npm reported success but the binary was not found on this machine’s PATH.');
236
+ return;
237
+ }
238
+ await update(cmd.id, 'completed', {
239
+ log: state.log, version: probe.version ?? null,
240
+ next: probe.authed ? null : 'login',
241
+ });
242
+ }
243
+ /* ── login ───────────────────────────────────────────────────────────── */
244
+ async function runLoginPty(cmd, spec, login = spec.login) {
245
+ (0, harness_registry_1.resetBinaryCaches)(); // a just-installed binary must be loginable immediately
246
+ const bin = spec.findBinary();
247
+ if (!bin) {
248
+ await update(cmd.id, 'failed', {}, `${spec.label} is not installed.`);
249
+ return;
250
+ }
251
+ await update(cmd.id, 'running', { step: 'starting-login', instructions: login.instructions });
252
+ const deadline = Date.now() + COMMAND_TIMEOUT_MS;
253
+ const ptyOpts = {
254
+ name: 'xterm-256color', cols: 120, rows: 30,
255
+ cwd: os.homedir(),
256
+ env: { ...process.env, ...(login.env ?? {}) },
257
+ };
258
+ let child;
259
+ try {
260
+ child = pty.spawn(bin, login.loginArgs ?? [], ptyOpts);
261
+ }
262
+ catch (err) {
263
+ // node-pty's "posix_spawnp failed." is almost always its own
264
+ // spawn-helper missing +x, not a problem with the harness binary.
265
+ // Repair and retry once so the operator's click succeeds now rather
266
+ // than after a daemon restart.
267
+ const repair = (0, pty_helper_1.ensurePtyHelperExecutable)();
268
+ if (repair.repaired.length === 0) {
269
+ await update(cmd.id, 'failed', {}, `could not start ${spec.label} login: ${err.message}` +
270
+ (repair.dir ? ` (node-pty at ${repair.dir}; spawn-helper looks executable)` : ' (node-pty could not be located)'));
271
+ return;
272
+ }
273
+ console.log((0, pty_helper_1.describeRepair)(repair));
274
+ try {
275
+ child = pty.spawn(bin, login.loginArgs ?? [], ptyOpts);
276
+ }
277
+ catch (err2) {
278
+ await update(cmd.id, 'failed', {}, `could not start ${spec.label} login after repairing node-pty's spawn-helper: ${err2.message}`);
279
+ return;
280
+ }
281
+ }
282
+ let buffer = '';
283
+ let urlSent = false;
284
+ let promptSeen = false;
285
+ let codeSent = false;
286
+ let exited = false;
287
+ let exitCode = -1;
288
+ child.onData((chunk) => { buffer = (buffer + chunk).slice(-16_000); });
289
+ child.onExit(({ exitCode: c }) => { exited = true; exitCode = c; });
290
+ const kill = () => { try {
291
+ child.kill();
292
+ }
293
+ catch { /* gone */ } };
294
+ for (;;) {
295
+ if (Date.now() > deadline) {
296
+ kill();
297
+ await update(cmd.id, 'failed', {}, 'login timed out after 10 minutes');
298
+ return;
299
+ }
300
+ const loginSpec = { ...spec, login };
301
+ if (!urlSent) {
302
+ const url = (0, harness_registry_1.extractAuthUrl)(loginSpec, buffer);
303
+ if (url) {
304
+ urlSent = true;
305
+ // Device-code flows print a short user code to type on the site.
306
+ const userCode = /\b([A-Z0-9]{4,8}-[A-Z0-9]{4,8})\b/.exec((0, harness_registry_1.stripAnsi)(buffer))?.[1] ?? null;
307
+ await update(cmd.id, null, { url, user_code: userCode, instructions: login.instructions });
308
+ }
309
+ }
310
+ if (urlSent && !promptSeen && (0, harness_registry_1.seesCodePrompt)(loginSpec, buffer)) {
311
+ promptSeen = true;
312
+ await update(cmd.id, 'awaiting_input', { needs: 'code' });
313
+ }
314
+ if (promptSeen && !codeSent) {
315
+ const code = await awaitInput(cmd.id, deadline, i => i.code);
316
+ if (code === null) {
317
+ kill();
318
+ await update(cmd.id, 'canceled', {}, 'canceled or timed out waiting for the code');
319
+ return;
320
+ }
321
+ codeSent = true;
322
+ child.write(code + '\r');
323
+ await update(cmd.id, 'running', { step: 'verifying' });
324
+ }
325
+ // TUI-style CLIs (gemini) never exit after auth — once the code went
326
+ // in, poll the probe and declare success the moment authed flips.
327
+ if (codeSent) {
328
+ const probe = await reprobeAuthed(spec.id);
329
+ if (probe.authed) {
330
+ kill();
331
+ await (0, heartbeat_1.beat)();
332
+ await update(cmd.id, 'completed', { step: 'done' });
333
+ return;
334
+ }
335
+ }
336
+ if (exited || (0, harness_registry_1.seesSuccess)(loginSpec, buffer)) {
337
+ kill();
338
+ const probe = await reprobeAuthed(spec.id);
339
+ await (0, heartbeat_1.beat)();
340
+ if (probe.authed) {
341
+ await update(cmd.id, 'completed', { step: 'done' });
342
+ }
343
+ else {
344
+ await update(cmd.id, 'failed', { tail: (0, harness_registry_1.stripAnsi)(buffer).slice(-600) }, exited && exitCode !== 0
345
+ ? `login exited with code ${exitCode}`
346
+ : 'login flow finished but the CLI still reports unauthenticated');
347
+ }
348
+ return;
349
+ }
350
+ // Also react to a Studio-side cancel while we wait for output.
351
+ const row = await pollRow(cmd.id);
352
+ if (row?.status === 'canceled') {
353
+ kill();
354
+ return;
355
+ }
356
+ await new Promise(r => setTimeout(r, 1_000));
357
+ }
358
+ }
359
+ function writeEnvVars(rel, vars) {
360
+ try {
361
+ const envPath = path.join(os.homedir(), rel);
362
+ fs.mkdirSync(path.dirname(envPath), { recursive: true });
363
+ let body = '';
364
+ try {
365
+ body = fs.readFileSync(envPath, 'utf8');
366
+ }
367
+ catch { /* new file */ }
368
+ const names = Object.keys(vars);
369
+ const lines = body.split('\n').filter(l => l.trim() && !names.some(n => l.startsWith(`${n}=`)));
370
+ for (const [k, v] of Object.entries(vars))
371
+ lines.push(`${k}=${v}`);
372
+ fs.writeFileSync(envPath, lines.join('\n') + '\n', { mode: 0o600 });
373
+ return null;
374
+ }
375
+ catch (err) {
376
+ return err.message;
377
+ }
378
+ }
379
+ async function applyApiKey(spec, key, method) {
380
+ if (spec.id === 'codex') {
381
+ (0, harness_registry_1.resetBinaryCaches)();
382
+ const bin = spec.findBinary();
383
+ if (!bin)
384
+ return 'codex is not installed';
385
+ // Current codex reads the key from STDIN (`codex login --with-api-key`).
386
+ const inv = (0, win_1.resolveCliInvocation)(bin, ['login', '--with-api-key']);
387
+ return await new Promise(resolve => {
388
+ const child = (0, child_process_1.spawn)(inv.file, inv.args, { env: process.env, windowsHide: true, timeout: 60_000 });
389
+ let err = '';
390
+ child.stderr?.on('data', b => { err += b.toString('utf8'); });
391
+ child.on('error', e => resolve(e.message));
392
+ child.on('exit', code => resolve(code === 0 ? null : (err.trim() || `exit ${code}`)));
393
+ child.stdin?.write(key + '\n');
394
+ child.stdin?.end();
395
+ });
396
+ }
397
+ if (spec.id === 'gemini') {
398
+ return method === 'vertex'
399
+ ? writeEnvVars('.gemini/.env', { GOOGLE_API_KEY: key, GOOGLE_GENAI_USE_VERTEXAI: 'true' })
400
+ : writeEnvVars('.gemini/.env', { GEMINI_API_KEY: key });
401
+ }
402
+ return `no api-key handler for ${spec.id}`;
403
+ }
404
+ async function runLoginApiKey(cmd, spec, method, instructions) {
405
+ await update(cmd.id, 'awaiting_input', { needs: 'api_key', instructions: instructions ?? spec.login.instructions });
406
+ const deadline = Date.now() + COMMAND_TIMEOUT_MS;
407
+ const key = cmd.input?.api_key?.trim() || await awaitInput(cmd.id, deadline, i => i.api_key);
408
+ if (key === null) {
409
+ await update(cmd.id, 'canceled', {}, 'canceled or timed out waiting for the key');
410
+ return;
411
+ }
412
+ await update(cmd.id, 'running', { step: 'applying-key' });
413
+ const err = await applyApiKey(spec, key, method);
414
+ if (err) {
415
+ await update(cmd.id, 'failed', {}, err);
416
+ return;
417
+ }
418
+ const probe = await reprobeAuthed(spec.id);
419
+ await (0, heartbeat_1.beat)();
420
+ if (probe.authed)
421
+ await update(cmd.id, 'completed', { step: 'done' });
422
+ else
423
+ await update(cmd.id, 'failed', {}, 'key applied but the CLI still reports unauthenticated');
424
+ }
425
+ async function runLogout(cmd, spec) {
426
+ if (!spec.logout) {
427
+ await update(cmd.id, 'failed', { instructions: spec.login.instructions }, `${spec.label} can only be logged out on the device.`);
428
+ return;
429
+ }
430
+ await update(cmd.id, 'running', { step: 'logging-out' });
431
+ (0, harness_registry_1.resetBinaryCaches)();
432
+ if (spec.logout.cmd) {
433
+ const bin = spec.findBinary();
434
+ if (!bin) {
435
+ await update(cmd.id, 'failed', {}, `${spec.label} is not installed.`);
436
+ return;
437
+ }
438
+ const inv = (0, win_1.resolveCliInvocation)(bin, spec.logout.cmd);
439
+ const err = await new Promise(resolve => {
440
+ try {
441
+ const child = (0, child_process_1.spawn)(inv.file, inv.args, { env: process.env, windowsHide: true, timeout: 60_000 });
442
+ let stderr = '';
443
+ child.stderr?.on('data', b => { stderr += b.toString('utf8'); });
444
+ child.on('error', e => resolve(e.message));
445
+ child.on('exit', code => resolve(code === 0 ? null : (stderr.trim() || `exit ${code}`)));
446
+ }
447
+ catch (e) {
448
+ resolve(e.message);
449
+ }
450
+ });
451
+ if (err) {
452
+ await update(cmd.id, 'failed', {}, err);
453
+ return;
454
+ }
455
+ }
456
+ for (const rel of spec.logout.files ?? []) {
457
+ try {
458
+ fs.rmSync(path.join(os.homedir(), rel), { force: true });
459
+ }
460
+ catch { /* best effort */ }
461
+ }
462
+ for (const [rel, varName] of spec.logout.envStrip ?? []) {
463
+ const envPath = path.join(os.homedir(), rel);
464
+ try {
465
+ const body = fs.readFileSync(envPath, 'utf8');
466
+ const lines = body.split('\n').filter(l => !l.startsWith(`${varName}=`));
467
+ fs.writeFileSync(envPath, lines.join('\n'), { mode: 0o600 });
468
+ }
469
+ catch { /* no env file = nothing to strip */ }
470
+ }
471
+ const probe = await reprobeAuthed(spec.id);
472
+ await (0, heartbeat_1.beat)();
473
+ if (!probe.authed)
474
+ await update(cmd.id, 'completed', { step: 'done' });
475
+ else
476
+ await update(cmd.id, 'failed', {}, 'logout ran but the CLI still reports authenticated');
477
+ }
478
+ /* ── update_runtime ──────────────────────────────────────────────────────
479
+ * Roll THIS node to a version (default: npm latest) and come back up.
480
+ *
481
+ * The command is deliberately left `running` here: this process is about to
482
+ * stop existing, so it cannot honestly report the outcome. index.ts's boot
483
+ * path completes it from the handoff file once the replacement daemon is
484
+ * actually up, with whatever version it actually came up as. */
485
+ async function runUpdateRuntime(cmd) {
486
+ const target = (cmd.input?.version ?? '').trim() || 'latest';
487
+ if (!restartHook) {
488
+ await update(cmd.id, 'failed', {}, 'this daemon is too old to restart itself');
489
+ return;
490
+ }
491
+ const mode = (0, self_update_1.detectLaunchMode)(process.argv[1] ?? '');
492
+ if (mode === 'source' && target !== 'latest') {
493
+ // A git checkout's version comes from the working tree, not npm — we'd
494
+ // restart and report the same old version, looking like a silent no-op.
495
+ await update(cmd.id, 'failed', { mode }, 'this node runs from a source checkout — pull and rebuild it there; a remote version bump cannot apply');
496
+ return;
497
+ }
498
+ await update(cmd.id, 'running', { step: 'draining', mode, target_version: target, from_version: VERSION });
499
+ try {
500
+ await restartHook({ commandId: cmd.id, version: target });
501
+ }
502
+ catch (err) {
503
+ // The hook owns the point of no return (it writes the handoff only once
504
+ // the replacement is spawned), so a throw here means we never left.
505
+ await update(cmd.id, 'failed', { mode }, `restart failed: ${err.message}`);
506
+ }
507
+ }
508
+ /* ── dispatcher ──────────────────────────────────────────────────────── */
509
+ async function execute(cmd) {
510
+ // Not a harness command — dispatch before the harness lookup, which would
511
+ // otherwise reject it for having no harness.
512
+ if (cmd.kind === 'update_runtime') {
513
+ await runUpdateRuntime(cmd);
514
+ return;
515
+ }
516
+ const spec = (0, harness_registry_1.harness)(cmd.harness ?? '');
517
+ if (!spec) {
518
+ await update(cmd.id, 'failed', {}, `unknown harness: ${cmd.harness}`);
519
+ return;
520
+ }
521
+ if (cmd.kind === 'install_harness') {
522
+ await runInstall(cmd, spec);
523
+ return;
524
+ }
525
+ if (cmd.kind === 'logout_harness') {
526
+ await runLogout(cmd, spec);
527
+ return;
528
+ }
529
+ const extra = cmd.input?.method
530
+ ? (spec.extraLogins ?? []).find(m => m.method === cmd.input?.method)
531
+ : undefined;
532
+ if (extra) {
533
+ if (extra.strategy === 'pty-url-code') {
534
+ await runLoginPty(cmd, spec, extra);
535
+ return;
536
+ }
537
+ if (extra.strategy === 'api-key') {
538
+ await runLoginApiKey(cmd, spec, extra.method, extra.instructions);
539
+ return;
540
+ }
541
+ }
542
+ switch (spec.login.strategy) {
543
+ case 'pty-url-code':
544
+ await runLoginPty(cmd, spec);
545
+ return;
546
+ case 'api-key':
547
+ await runLoginApiKey(cmd, spec);
548
+ return;
549
+ default:
550
+ await update(cmd.id, 'failed', { instructions: spec.login.instructions }, `${spec.label} can only be logged in on the device.`);
551
+ }
552
+ }
553
+ /** Called from index.ts when a heartbeat reports pending_commands > 0.
554
+ * Drains the queue serially; re-entrant calls while draining are no-ops
555
+ * (the drain loop re-picks until empty). */
556
+ function wake() {
557
+ if (running)
558
+ return;
559
+ running = true;
560
+ void (async () => {
561
+ try {
562
+ for (;;) {
563
+ const t = token();
564
+ if (!t)
565
+ return;
566
+ let rows;
567
+ try {
568
+ rows = await (0, supabase_client_1.rpc)('runtime_commands_pick', { p_token: t });
569
+ }
570
+ catch (err) {
571
+ console.error('[commands] pick failed:', err.message);
572
+ return;
573
+ }
574
+ if (!rows || rows.length === 0)
575
+ return;
576
+ for (const row of rows) {
577
+ console.log(`[commands] ${row.kind} ${row.harness} (${row.id})`);
578
+ try {
579
+ await execute(row);
580
+ }
581
+ catch (err) {
582
+ console.error('[commands] execute crashed:', err.message);
583
+ await update(row.id, 'failed', {}, err.message);
584
+ }
585
+ }
586
+ }
587
+ }
588
+ finally {
589
+ running = false;
590
+ }
591
+ })();
592
+ }
@@ -0,0 +1,7 @@
1
+ export declare const SUPABASE_URL = "https://syhzpqqvrplaqdipcymw.supabase.co";
2
+ export declare const SUPABASE_ANON_KEY: string;
3
+ export declare const VAULT_PAIR_URL = "https://vault.add.ai/add/entity";
4
+ export declare const HEARTBEAT_INTERVAL_MS = 30000;
5
+ export declare const HANG_WATCHDOG_IDLE_MS: number;
6
+ export declare const PAIR_POLL_INTERVAL_MS = 2000;
7
+ export declare const PAIR_TIMEOUT_MS: number;
package/dist/config.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ // Compile-time runtime configuration. The anon key is intended to be
3
+ // public (PostgREST anon role only sees what RLS permits), so it's fine
4
+ // to ship in the npm package.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PAIR_TIMEOUT_MS = exports.PAIR_POLL_INTERVAL_MS = exports.HANG_WATCHDOG_IDLE_MS = exports.HEARTBEAT_INTERVAL_MS = exports.VAULT_PAIR_URL = exports.SUPABASE_ANON_KEY = exports.SUPABASE_URL = void 0;
7
+ exports.SUPABASE_URL = 'https://syhzpqqvrplaqdipcymw.supabase.co';
8
+ // Legacy anon JWT for the +Ai Project. Used by the daemon for every RPC
9
+ // call; per-runtime authentication happens inside each RPC via the
10
+ // daemon_token argument (or pair_code for unpaired calls).
11
+ exports.SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' +
12
+ 'eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InN5aHpwcXF2cnBsYXFkaXBjeW13Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDQ4ODAwNDgsImV4cCI6MjA2MDQ1NjA0OH0.' +
13
+ 'X6Rp3JrHra-SdDn8YC0aK3uIsbNZlRJrIUhW9Kiwmac';
14
+ // Where the user goes to pair this machine. The short alias
15
+ // /add/entity[/:code] lands on the Vault "New Item" dialog with the
16
+ // Entity provider pre-selected and the code pre-filled. Vault's
17
+ // LegacyRedirect prepends the user's default workspace slug.
18
+ exports.VAULT_PAIR_URL = 'https://vault.add.ai/add/entity';
19
+ // Heartbeat interval. Picked so the desktop can mark a runtime offline
20
+ // after ~2x missed heartbeats (~60s) without too much chatty traffic.
21
+ exports.HEARTBEAT_INTERVAL_MS = 30_000;
22
+ // Per-run hang watchdog: if a spawned agent produces NO output for this long
23
+ // we kill it (the run then fails 'agent_hang', which is transient → retried).
24
+ // Deliberately generous: a legit slow tool call (e.g. grok kicking off an
25
+ // image/video flow, which emits no intermediate events) must complete inside
26
+ // this window or a false kill would waste the run. 8 min clears typical flows
27
+ // while still firing well before the 1h node-timeout ceiling and the 10-min
28
+ // server watchdog's hard fail. Override with HANG_WATCHDOG_IDLE_MS (0 = off).
29
+ exports.HANG_WATCHDOG_IDLE_MS = (() => {
30
+ const raw = process.env.HANG_WATCHDOG_IDLE_MS;
31
+ if (raw == null || raw === '')
32
+ return 8 * 60 * 1000;
33
+ const n = Number(raw);
34
+ return Number.isFinite(n) && n >= 0 ? n : 8 * 60 * 1000;
35
+ })();
36
+ // How often we poll runtime_pair_finalize while waiting for the user.
37
+ exports.PAIR_POLL_INTERVAL_MS = 2_000;
38
+ exports.PAIR_TIMEOUT_MS = 15 * 60 * 1000; // matches DB expiry
@@ -0,0 +1,5 @@
1
+ export interface ControlServer {
2
+ port: number;
3
+ close(): Promise<void>;
4
+ }
5
+ export declare function startControlServer(): Promise<ControlServer>;