adaptiqai 0.1.1 → 0.2.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 (3) hide show
  1. package/README.md +45 -18
  2. package/lib/index.js +988 -377
  3. package/package.json +2 -3
package/lib/index.js CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  // src/entry.ts
4
4
  import { homedir } from "node:os";
5
- import { join as join15 } from "node:path";
5
+ import { join as join16 } from "node:path";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { parseArgs } from "node:util";
8
8
 
9
9
  // src/apps/claude-code.ts
10
- import { access, chmod as chmod2, constants as constants2, mkdir as mkdir2, readdir } from "node:fs/promises";
11
- import { existsSync as existsSync2 } from "node:fs";
12
- import { dirname as dirname2, join as join5 } from "node:path";
10
+ import { access, chmod as chmod2, constants as constants2, mkdir as mkdir3, readdir } from "node:fs/promises";
11
+ import { existsSync as existsSync3 } from "node:fs";
12
+ import { dirname as dirname2, join as join6 } from "node:path";
13
13
 
14
14
  // src/context.ts
15
15
  import { hostname } from "node:os";
@@ -57,7 +57,7 @@ function checkedAddress(raw, variable) {
57
57
  }
58
58
 
59
59
  // src/files.ts
60
- import { chmod, lstat, mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
60
+ import { chmod, lstat, mkdir, open, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
61
61
  import { basename, dirname, join as join2 } from "node:path";
62
62
 
63
63
  // src/output.ts
@@ -99,6 +99,25 @@ var Output = class {
99
99
  };
100
100
 
101
101
  // src/files.ts
102
+ var KEY_SHAPE = /ak_(?:live|test)_[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{8,}/u;
103
+ var PIECE = 1 << 20;
104
+ async function fileHoldsKey(path) {
105
+ const handle = await open(path, "r").catch(() => void 0);
106
+ if (handle === void 0) return false;
107
+ try {
108
+ const piece = Buffer.alloc(PIECE);
109
+ let carried = "";
110
+ for (; ; ) {
111
+ const { bytesRead } = await handle.read(piece, 0, PIECE, null);
112
+ if (bytesRead === 0) return false;
113
+ const text = carried + piece.toString("latin1", 0, bytesRead);
114
+ if (KEY_SHAPE.test(text)) return true;
115
+ carried = text.slice(-256);
116
+ }
117
+ } finally {
118
+ await handle.close();
119
+ }
120
+ }
102
121
  async function readText(path) {
103
122
  try {
104
123
  return await readFile(path, "utf8");
@@ -123,8 +142,8 @@ async function readJson(path) {
123
142
  }
124
143
  var Backups = class {
125
144
  /** @param dir - this run's folder, made when the first copy is. */
126
- constructor(dir3) {
127
- this.dir = dir3;
145
+ constructor(dir2) {
146
+ this.dir = dir2;
128
147
  }
129
148
  saved = [];
130
149
  seen = /* @__PURE__ */ new Map();
@@ -219,6 +238,40 @@ async function removeFile(path, backups) {
219
238
  return true;
220
239
  }
221
240
 
241
+ // src/node.ts
242
+ var ADAPTIQ_NODE_BIN = "$HOME/.adaptiq/node/current/bin";
243
+ var HandedOver = class extends Error {
244
+ constructor(exitCode) {
245
+ super("handed over");
246
+ this.exitCode = exitCode;
247
+ }
248
+ };
249
+ function atLeast(version2, minimum) {
250
+ const have = version2.replace(/^v/u, "").split(".").map(Number);
251
+ const need = minimum.split(".").map(Number);
252
+ for (let index = 0; index < 3; index++) {
253
+ const a = have[index] ?? 0;
254
+ const b = need[index] ?? 0;
255
+ if (a !== b) return a > b;
256
+ }
257
+ return true;
258
+ }
259
+ async function needNode(context2, minimum, what, args) {
260
+ if (atLeast(context2.node, minimum)) return;
261
+ const said = `${what} needs Node.js ${minimum} or newer, and this runs on ${context2.node}.`;
262
+ const script = `${context2.origin}/v1/install.sh`;
263
+ const command2 = `curl -fsSL '${script}' | ADAPTIQ_NODE_MIN=${minimum} ADAPTIQ_INSTALL_NODE=1 sh -s -- ${args.join(" ")}`;
264
+ const windows2 = context2.platform === "win32";
265
+ if (windows2 || !await context2.confirm(`${said} Install a newer Node.js for AdaptiqAI, in your home folder? It needs no admin password.`)) {
266
+ throw new Problem("node_too_old", `${said} Install it from nodejs.org${windows2 ? "" : `, or have AdaptiqAI install one: ${command2}`}`, {
267
+ node: context2.node,
268
+ needs: minimum,
269
+ ...windows2 ? {} : { install: command2 }
270
+ });
271
+ }
272
+ throw new HandedOver(await context2.handOver(command2));
273
+ }
274
+
222
275
  // src/run.ts
223
276
  import { spawn } from "node:child_process";
224
277
  import { accessSync, constants, statSync } from "node:fs";
@@ -228,10 +281,10 @@ var FLUSH_MS = 150;
228
281
  function which(name2, env, platform = process.platform) {
229
282
  const windows2 = platform === "win32";
230
283
  const extensions = windows2 ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
231
- for (const dir3 of (env.PATH ?? env.Path ?? "").split(windows2 ? ";" : delimiter)) {
232
- if (dir3 === "") continue;
284
+ for (const dir2 of (env.PATH ?? env.Path ?? "").split(windows2 ? ";" : delimiter)) {
285
+ if (dir2 === "") continue;
233
286
  for (const extension of ["", ...extensions]) {
234
- const found = join3(dir3, name2 + extension);
287
+ const found = join3(dir2, name2 + extension);
235
288
  try {
236
289
  accessSync(found, windows2 ? constants.F_OK : constants.X_OK);
237
290
  if (statSync(found).isFile()) return found;
@@ -246,12 +299,39 @@ function launch(command2, args, platform) {
246
299
  const quoted = [command2, ...args].map((word) => `"${word.replace(/"/gu, '""')}"`).join(" ");
247
300
  return ["cmd.exe", ["/d", "/s", "/c", `"${quoted}"`], { windowsVerbatimArguments: true }];
248
301
  }
302
+ function ownEnv(env, platform) {
303
+ const own = Object.fromEntries(Object.entries(env).filter(([name2]) => !/^npm_/iu.test(name2) && name2 !== "INIT_CWD" && !["ADAPTIQ_CONNECT", "ADAPTIQ_HELLO", "ADAPTIQ_KEY"].includes(name2)));
304
+ const path = Object.keys(own).find((name2) => name2.toUpperCase() === "PATH");
305
+ if (env.npm_lifecycle_event === void 0 || path === void 0) return own;
306
+ const separator = platform === "win32" ? ";" : delimiter;
307
+ const dirs = (own[path] ?? "").split(separator);
308
+ const first = dirs.findIndex((dir2) => !/(?:node_modules[\\/]\.bin|node-gyp-bin)[\\/]?$/u.test(dir2));
309
+ return { ...own, [path]: dirs.slice(first < 0 ? dirs.length : first).join(separator) };
310
+ }
311
+ function handOver(base, platform = process.platform) {
312
+ return async (command2) => {
313
+ if (holdsKey(command2)) throw new Problem("key_on_command_line", "Refused to put a key on a command line.");
314
+ const [file3, args] = platform === "win32" ? ["cmd.exe", ["/d", "/s", "/c", `"${command2}"`]] : ["/bin/sh", ["-c", command2]];
315
+ const keep = () => {
316
+ };
317
+ process.on("SIGINT", keep);
318
+ try {
319
+ return await new Promise((resolve) => {
320
+ const child = spawn(file3, args, { env: ownEnv(base, platform), stdio: "inherit", windowsVerbatimArguments: platform === "win32" });
321
+ child.on("error", () => resolve(1));
322
+ child.on("exit", (code) => resolve(code ?? 1));
323
+ });
324
+ } finally {
325
+ process.off("SIGINT", keep);
326
+ }
327
+ };
328
+ }
249
329
  function runner(base, platform = process.platform) {
250
330
  return async (command2, args, options = {}) => {
251
331
  if (args.some(holdsKey) || holdsKey(command2)) throw new Problem("key_on_command_line", "Refused to put a key on a command line.");
252
- const [file4, words, extra] = launch(command2, args, platform);
332
+ const [file3, words, extra] = launch(command2, args, platform);
253
333
  return await new Promise((resolve, reject) => {
254
- const child = spawn(file4, [...words], {
334
+ const child = spawn(file3, [...words], {
255
335
  env: { ...base, ...options.env },
256
336
  stdio: [options.input === void 0 ? "ignore" : "pipe", "pipe", "pipe"],
257
337
  ...options.cwd === void 0 ? {} : { cwd: options.cwd },
@@ -296,6 +376,7 @@ import { mkdtemp, rm as rm2 } from "node:fs/promises";
296
376
  import { tmpdir } from "node:os";
297
377
  import { join as join4 } from "node:path";
298
378
  var QUESTION = "AdaptiqAI connection check: reply with the single word connected.";
379
+ var shellWord = (text) => `'${text.replace(/'/gu, "'\\''")}'`;
299
380
  var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
300
381
  function macApp(context2, name2) {
301
382
  if (context2.platform !== "darwin") return void 0;
@@ -318,6 +399,14 @@ async function emptyFolder(work) {
318
399
  await rm2(folder2, { recursive: true, force: true });
319
400
  }
320
401
  }
402
+ async function forgetGrant(context2, folder2) {
403
+ const grant = join4(folder2, "grant.json");
404
+ try {
405
+ if (JSON.parse(await readText(grant) ?? "{}").apiBase === context2.origin && await removeFile(grant)) return grant;
406
+ } catch {
407
+ }
408
+ return void 0;
409
+ }
321
410
  async function running(context2, name2) {
322
411
  if (context2.platform === "win32") return false;
323
412
  const seen = await context2.run("pgrep", ["-x", name2], { timeoutMs: 5e3 }).catch(() => void 0);
@@ -325,25 +414,57 @@ async function running(context2, name2) {
325
414
  }
326
415
  async function quitMacApp(context2, bundleId) {
327
416
  const script = `application id "${bundleId}" is running`;
328
- const open2 = async () => (await context2.run("osascript", ["-e", script], { timeoutMs: 5e3 }).catch(() => void 0))?.stdout.trim() === "true";
329
- if (!await open2()) return true;
417
+ const open3 = async () => (await context2.run("osascript", ["-e", script], { timeoutMs: 5e3 }).catch(() => void 0))?.stdout.trim() === "true";
418
+ if (!await open3()) return true;
330
419
  await context2.run("osascript", ["-e", `quit application id "${bundleId}"`], { timeoutMs: 35e3 }).catch(() => void 0);
331
- for (let tries = 0; tries < 60 && await open2(); tries++) await context2.sleep(500);
332
- return !await open2();
420
+ for (let tries = 0; tries < 60 && await open3(); tries++) await context2.sleep(500);
421
+ return !await open3();
333
422
  }
334
423
  var openMacApp = async (context2, path) => {
335
424
  await context2.run("open", [path], { timeoutMs: 15e3 }).catch(() => void 0);
336
425
  };
337
- var modelWindow = (model2) => ({
338
- ...model2.input.includes("image") ? { input: ["text", "image"] } : {},
339
- contextWindow: CONVERSATION_TOKENS,
340
- ...model2.output > 0 ? { maxTokens: model2.output } : {}
341
- });
426
+
427
+ // src/apps/mcp.ts
428
+ import { existsSync as existsSync2 } from "node:fs";
429
+ import { mkdir as mkdir2 } from "node:fs/promises";
430
+ import { join as join5 } from "node:path";
431
+ var PACKAGE = "@adaptiqai/mcp";
432
+ var mcpDir = (context2) => join5(configDir(context2.env, context2.home), "mcp");
433
+ var mcpServer = (context2) => join5(mcpDir(context2), "node_modules", ".bin", context2.platform === "win32" ? "adaptiqai-mcp.cmd" : "adaptiqai-mcp");
434
+ var mcpPlugin = (context2) => join5(mcpDir(context2), "node_modules", "@adaptiqai", "mcp", "claude-code");
435
+ var mcpHasHook = (context2) => existsSync2(join5(mcpPlugin(context2), "hooks", "hooks.json"));
436
+ var mcpEnv = (context2) => context2.origin === PRODUCTION_API ? void 0 : { ADAPTIQAI_API_BASE: context2.origin, ADAPTIQAI_CONSOLE_URL: context2.console };
437
+ var MCP_UPDATE = `U="$D/mcp/.checked"; if [ -x "$X" ] && command -v npm >/dev/null 2>&1 && [ -z "$(find "$U" -mtime -1 2>/dev/null)" ]; then touch "$U"; N="$D/mcp.next.$$"; O="$D/mcp.old.$$"; rm -rf "$N"; if B=$(node -e 'process.stdout.write(new Date(Date.now()-172800000).toISOString())' 2>/dev/null) && npm install --prefix "$N" --no-audit --no-fund --fetch-retries=0 --fetch-timeout=10000 --before="$B" ${PACKAGE}@latest </dev/null >/dev/null 2>&1 && [ -x "$N/node_modules/.bin/adaptiqai-mcp" ] && node --check "$N/node_modules/@adaptiqai/mcp/lib/index.js" >/dev/null 2>&1; then touch "$N/.checked"; mv "$D/mcp" "$O" && { mv "$N" "$D/mcp" || mv "$O" "$D/mcp"; }; fi; rm -rf "$N" "$O"; fi`;
438
+ async function installMcp(context2, app) {
439
+ const had = existsSync2(mcpServer(context2));
440
+ const npm = command(context2, "npm");
441
+ if (npm === void 0) {
442
+ if (had) return;
443
+ throw new Problem("tools_not_installed", `AdaptiqAI's knowledge tools for ${app} need npm, which comes with Node.js. Install Node.js from nodejs.org, then run this again.`, { app });
444
+ }
445
+ context2.say.note(`Installing AdaptiqAI's knowledge tools for ${app}...`);
446
+ await mkdir2(mcpDir(context2), { recursive: true });
447
+ const done = await context2.run(npm, ["install", "--prefix", mcpDir(context2), "--no-audit", "--no-fund", PACKAGE], { timeoutMs: 3e5 });
448
+ if (done.code === 0 && existsSync2(mcpServer(context2)) || had) return;
449
+ const said = `${done.stderr}
450
+ ${done.stdout}`.trim().split("\n").pop() ?? "";
451
+ throw new Problem("tools_not_installed", `AdaptiqAI's knowledge tools for ${app} could not be installed, so ${app} was left as it was. Check the connection, then run this again.${said === "" ? "" : ` npm said: ${said}`}`, { app });
452
+ }
342
453
 
343
454
  // src/apps/claude-code.ts
344
455
  var LAUNCHER = "adaptiq-claude";
345
- var ALIASES = ["ANTHROPIC_DEFAULT_FABLE_MODEL", "ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "CLAUDE_CODE_SUBAGENT_MODEL"];
346
- var MOST_OUTPUT = 128e3;
456
+ var ALIASES = ["ANTHROPIC_DEFAULT_OPUS_MODEL", "ANTHROPIC_DEFAULT_SONNET_MODEL", "ANTHROPIC_DEFAULT_HAIKU_MODEL", "CLAUDE_CODE_SUBAGENT_MODEL"];
457
+ var MOST_OUTPUT = 16e3;
458
+ var KEEP_JS = [
459
+ 'const fs=require("fs"),e=process.env,file=e.AQ_USER,',
460
+ 'read=(f)=>{try{return JSON.parse(fs.readFileSync(f,"utf8"))}catch(x){return x.code==="ENOENT"?{}:null}},',
461
+ "ours=((read(e.AQ_MODELS)||{}).options||[]).map((o)=>o.model),s=read(file);",
462
+ 'if(!s||typeof s!=="object"||Array.isArray(s))process.exit(0);',
463
+ 'if(!ours.includes(s.model)){if(process.argv[2]==="start")fs.writeFileSync(e.AQ_WAS,JSON.stringify({model:s.model??null}));process.exit(0)}',
464
+ "const was=(read(e.AQ_WAS)||{}).model;if(was==null||ours.includes(was))delete s.model;else s.model=was;",
465
+ 'const tmp=file+".adaptiq";fs.writeFileSync(tmp,JSON.stringify(s,null,2)+"\\n",{mode:fs.statSync(file).mode&0o777});',
466
+ "fs.renameSync(tmp,file)"
467
+ ].join("\n");
347
468
  var SETTINGS = [
348
469
  // AdaptiqAI keeps the whole conversation in native memory, so Claude Code does not compact it by itself; /compact still does.
349
470
  ["DISABLE_AUTO_COMPACT", "1"],
@@ -353,20 +474,24 @@ var SETTINGS = [
353
474
  ["CLAUDE_CODE_SUBAGENT_MODEL_FORCE", "1"],
354
475
  ["CLAUDE_CODE_AUTO_MODE_SERVER", "0"],
355
476
  // Each request says whether it is the chat's, an agent's or a side request, so the gateway keeps one session per chat.
356
- ["CLAUDE_CODE_GATEWAY_HINT_HEADERS", "1"]
477
+ ["CLAUDE_CODE_GATEWAY_HINT_HEADERS", "1"],
478
+ // Behind a gateway Claude Code hides MCP tools behind its tool search, so the model would see AdaptiqAI's knowledge tools by name only.
479
+ ["ENABLE_TOOL_SEARCH", "false"]
357
480
  ];
481
+ var ALLOW = ["mcp__adaptiqai__adaptiq_knowledge", "mcp__adaptiqai__adaptiq_memory"];
358
482
  var folder = (context2) => configDir(context2.env, context2.home);
359
- var keyFile = (context2) => join5(folder(context2), "claude-code.key");
360
- var bin = (context2) => join5(context2.home, ".local", "bin");
483
+ var keyFile = (context2) => join6(folder(context2), "claude-code.key");
484
+ var bin = (context2) => join6(context2.home, ".local", "bin");
361
485
  var name = (context2) => context2.platform === "win32" ? `${LAUNCHER}.cmd` : LAUNCHER;
362
486
  async function launcherDir(context2) {
363
487
  const claude = which("claude", context2.env, context2.platform);
364
488
  if (claude === void 0) return bin(context2);
365
489
  return await access(dirname2(claude), constants2.W_OK).then(() => dirname2(claude), () => bin(context2));
366
490
  }
367
- var installed = (context2) => which(LAUNCHER, context2.env, context2.platform) ?? [join5(bin(context2), name(context2))].find(existsSync2);
491
+ var installed = (context2) => which(LAUNCHER, context2.env, context2.platform) ?? [join6(bin(context2), name(context2))].find(existsSync3);
368
492
  function posix(context2, model2) {
369
493
  const output = Math.min(model2.output, MOST_OUTPUT);
494
+ const env = mcpEnv(context2);
370
495
  return [
371
496
  "#!/bin/sh",
372
497
  "# Claude Code on AdaptiqAI, from `npx -y adaptiqai connect claude-code`. Running it again writes this again.",
@@ -376,27 +501,61 @@ function posix(context2, model2) {
376
501
  `M=$(printf %s "\${ADAPTIQ_MODEL:-${model2.id}}" | tr -cd 'A-Za-z0-9._:/-')`,
377
502
  `S='{"env":{"ANTHROPIC_BASE_URL":"${context2.origin}"'`,
378
503
  `for A in ${ALIASES.join(" ")}; do S="$S,\\"$A\\":\\"$M\\""; done`,
504
+ // The window and the answer length are the launcher's model's: another one in ADAPTIQ_MODEL has its own.
505
+ `[ "$M" = '${model2.id}' ] && S="$S"',"CLAUDE_CODE_MAX_CONTEXT_TOKENS":"${CONVERSATION_TOKENS}"'`,
379
506
  ...output > 0 ? [`[ "$M" = '${model2.id}' ] && S="$S"',"CLAUDE_CODE_MAX_OUTPUT_TOKENS":"${output}"'`] : [],
380
- `S="$S"',"CLAUDE_CODE_MAX_CONTEXT_TOKENS":"${CONVERSATION_TOKENS}"${SETTINGS.map(([key, value]) => `,"${key}":"${value}"`).join("")}'`,
507
+ // The plugin's hook reads Claude Code's own environment for an AdaptiqAI other than production.
508
+ `S="$S"'${[...SETTINGS, ...Object.entries(env ?? {})].map(([key, value]) => `,"${key}":"${value}"`).join("")}'`,
509
+ // A Node.js AdaptiqAI installed comes last, for the MCP server, the guard below and a Claude Code npm put there.
510
+ `PATH="$PATH:${ADAPTIQ_NODE_BIN}"`,
511
+ // Once a day /model takes the models the workspace has now, in place of Anthropic's lineup, so a model added later
512
+ // shows up by itself. The first start waits up to 5 seconds. The key goes to curl on its input, never in its
513
+ // arguments. Only one line of ASCII shaped as the list is kept: a failure, or any other answer, keeps the list there was.
514
+ 'L="$D/claude-code-models.json"; V="$L.checked"',
515
+ `aq_list() { case "$1" in '{"options":[{"model":'*'}],"replaceBuiltInOptions":true}') ;; *) return 1 ;; esac; ! printf %s "$1" | LC_ALL=C grep -q '[^ -~]'; }`,
516
+ `aq_models() { printf 'Authorization: Bearer %s\\n' "$K" | curl -fsS --max-time "$1" -H @- '${context2.origin}/v1/cli-auth/claude-code/models' -o "$L.new" 2>/dev/null && aq_list "$(cat "$L.new")" && mv -f "$L.new" "$L"; rm -f "$L.new"; }`,
517
+ 'if command -v curl >/dev/null 2>&1 && [ -z "$(find "$V" -mtime -1 2>/dev/null)" ]; then touch "$V"; if [ -s "$L" ]; then ( aq_models 20 ) </dev/null >/dev/null 2>&1 & else aq_models 5; fi; fi',
518
+ 'Q=$(cat "$L" 2>/dev/null); aq_list "$Q" || Q=',
519
+ // A pick in /model is saved as the person's own default, where plain claude would then ask Anthropic for an
520
+ // AdaptiqAI model. So the list goes in only with Node.js to undo that, as the session ends and at the next start.
521
+ "command -v node >/dev/null 2>&1 || Q=",
522
+ 'export AQ_KEEP="$D/claude-code-keep.js" AQ_MODELS="$L" AQ_WAS="$D/claude-code-own-model.json" AQ_USER="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"',
523
+ `[ -z "$Q" ] || { cat >"$AQ_KEEP" <<'KEEP'`,
524
+ KEEP_JS,
525
+ "KEEP",
526
+ 'node "$AQ_KEEP" start; }',
527
+ `[ -n "$Q" ] && Q=",\\"modelPicker\\":$Q"',"hooks":{"SessionEnd":[{"hooks":[{"type":"command","command":"node \\"$AQ_KEEP\\" end"}]}]}'`,
381
528
  `F="$D/claude-code-$(printf %s "$M" | tr '/:' '__').json"`,
382
- `( umask 077; printf '%s,"ANTHROPIC_AUTH_TOKEN":"%s","ANTHROPIC_API_KEY":""}}' "$S" "$K" >"$F" && chmod 600 "$F" ) || exit 1`,
529
+ `( umask 077; printf '%s,"ANTHROPIC_AUTH_TOKEN":"%s","ANTHROPIC_API_KEY":""},"permissions":{"allow":${JSON.stringify(ALLOW)}}%s}' "$S" "$K" "$Q" >"$F" && chmod 600 "$F" ) || exit 1`,
383
530
  // Claude Code's own local install is often only an alias, which a script does not see.
384
531
  'command -v claude >/dev/null 2>&1 || PATH="$HOME/.claude/local:$HOME/.local/bin:$PATH"',
385
532
  "unset ANTHROPIC_API_KEY",
386
533
  'export ANTHROPIC_AUTH_TOKEN="$K"',
534
+ // The server takes its key from Claude Code's environment. Its config comes before --settings: --mcp-config takes every word after it up to the next option.
535
+ 'X="$D/mcp/node_modules/.bin/adaptiqai-mcp"; C="$D/claude-code-mcp.json"',
536
+ // Beside Claude Code, never before it: a slow npm delays no start, and the update takes effect at the next.
537
+ `( ${MCP_UPDATE} ) </dev/null >/dev/null 2>&1 &`,
538
+ // The server's Claude Code plugin, where the server installed carries one; --plugin-dir must name one.
539
+ 'P="$D/mcp/node_modules/@adaptiqai/mcp/claude-code"',
540
+ `if [ -x "$X" ] && printf '{"mcpServers":{"adaptiqai":{"command":"%s"${env === void 0 ? "" : `,"env":${JSON.stringify(env)}`}}}}\\n' "$X" >"$C"; then`,
541
+ ' [ -f "$P/.claude-plugin/plugin.json" ] && exec claude --model "$M" --plugin-dir "$P" --mcp-config "$C" --settings "$F" "$@"',
542
+ ' exec claude --model "$M" --mcp-config "$C" --settings "$F" "$@"',
543
+ "fi",
387
544
  'exec claude --model "$M" --settings "$F" "$@"',
388
545
  ""
389
546
  ].join("\n");
390
547
  }
548
+ var mcpConfig = (context2) => join6(folder(context2), "claude-code-mcp.json");
391
549
  function windows(context2, model2) {
392
- const file4 = join5(folder(context2), "claude-code.json");
550
+ const file3 = join6(folder(context2), "claude-code.json");
551
+ const tools2 = existsSync3(mcpServer(context2)) ? ` --mcp-config "${mcpConfig(context2)}"` : "";
393
552
  return [
394
553
  "@echo off",
395
554
  "rem Claude Code on AdaptiqAI, from npx -y adaptiqai connect claude-code. Running it again writes this again.",
396
555
  "setlocal",
397
556
  'set "ANTHROPIC_API_KEY="',
398
557
  `set /p ANTHROPIC_AUTH_TOKEN=<"${keyFile(context2)}"`,
399
- `claude --model "${model2.id}" --settings "${file4}" %*`,
558
+ `claude --model "${model2.id}"${tools2} --settings "${file3}" %*`,
400
559
  ""
401
560
  ].join("\r\n");
402
561
  }
@@ -411,7 +570,8 @@ function settingsJson(context2, model2, key) {
411
570
  ...Object.fromEntries(SETTINGS),
412
571
  ANTHROPIC_AUTH_TOKEN: key,
413
572
  ANTHROPIC_API_KEY: ""
414
- }
573
+ },
574
+ permissions: { allow: ALLOW }
415
575
  }, null, 2)}
416
576
  `;
417
577
  }
@@ -420,10 +580,13 @@ var claudeCode = {
420
580
  aliases: ["claude"],
421
581
  name: "Claude Code",
422
582
  install: "npm install -g @anthropic-ai/claude-code",
423
- does: "Added `adaptiq-claude`, which starts Claude Code on AdaptiqAI. Your own Claude Code stays as it is.",
583
+ does: "Added `adaptiq-claude`, which starts Claude Code on AdaptiqAI with your memory and knowledge. Your own Claude Code stays as it is.",
424
584
  // The console knows a setup that lasts by this name.
425
585
  signsInAs: async () => LAUNCHER,
426
- found: async (context2) => command(context2, "claude", [join5(context2.home, ".claude", "local", "claude")]) !== void 0,
586
+ found: async (context2) => command(context2, "claude", [join6(context2.home, ".claude", "local", "claude")]) !== void 0,
587
+ async prepare(context2) {
588
+ await installMcp(context2, "Claude Code");
589
+ },
427
590
  async current(context2) {
428
591
  const path = installed(context2);
429
592
  const text = path === void 0 ? void 0 : await readText(path);
@@ -439,16 +602,19 @@ var claudeCode = {
439
602
  },
440
603
  async apply(context2, facts) {
441
604
  const [model2] = facts.models;
442
- await mkdir2(folder(context2), { recursive: true, mode: 448 });
605
+ await mkdir3(folder(context2), { recursive: true, mode: 448 });
443
606
  await chmod2(folder(context2), 448).catch(() => {
444
607
  });
445
608
  await writeWhole(keyFile(context2), `${facts.key}
446
609
  `, null, { secret: true });
447
- const dir3 = await launcherDir(context2);
448
- await mkdir2(dir3, { recursive: true });
449
- const launcher = join5(dir3, name(context2));
610
+ const dir2 = await launcherDir(context2);
611
+ await mkdir3(dir2, { recursive: true });
612
+ const launcher = join6(dir2, name(context2));
450
613
  if (context2.platform === "win32") {
451
- await writeWhole(join5(folder(context2), "claude-code.json"), settingsJson(context2, model2, facts.key), null, { secret: true });
614
+ await writeWhole(join6(folder(context2), "claude-code.json"), settingsJson(context2, model2, facts.key), null, { secret: true });
615
+ const env = mcpEnv(context2);
616
+ if (existsSync3(mcpServer(context2))) await writeWhole(mcpConfig(context2), `${JSON.stringify({ mcpServers: { adaptiqai: { command: mcpServer(context2), ...env === void 0 ? {} : { env } } } })}
617
+ `, null);
452
618
  await writeWhole(launcher, windows(context2, model2), context2.backups);
453
619
  } else {
454
620
  await writeWhole(launcher, posix(context2, model2), context2.backups, { mode: 493 });
@@ -460,217 +626,95 @@ var claudeCode = {
460
626
  if (path !== void 0) await removeFile(path);
461
627
  await removeFile(keyFile(context2));
462
628
  const left = await readdir(folder(context2)).catch(() => []);
463
- for (const file4 of left.filter((entry2) => /^claude-code.*\.json$/u.test(entry2))) await removeFile(join5(folder(context2), file4));
629
+ for (const file3 of left.filter((entry2) => /^claude-code.*\.json$/u.test(entry2))) await removeFile(join6(folder(context2), file3));
630
+ },
631
+ async clean(context2) {
632
+ const settings = (await readdir(folder(context2)).catch(() => [])).filter((entry2) => /^claude-code.*\.json$/u.test(entry2)).map((entry2) => join6(folder(context2), entry2));
633
+ const removed = [];
634
+ if (await removeFile(keyFile(context2))) removed.push(keyFile(context2));
635
+ for (const file3 of settings) if (await fileHoldsKey(file3) && await removeFile(file3)) removed.push(file3);
636
+ return { removed, left: [] };
464
637
  },
465
638
  async start(context2) {
466
639
  if (which(LAUNCHER, context2.env, context2.platform) !== void 0) return LAUNCHER;
467
- return installed(context2) ?? join5(await launcherDir(context2), name(context2));
468
- }
469
- };
470
-
471
- // src/apps/cline.ts
472
- import { createHash, randomBytes } from "node:crypto";
473
- import { existsSync as existsSync3 } from "node:fs";
474
- import { mkdir as mkdir4, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
475
- import { join as join7 } from "node:path";
476
-
477
- // src/record.ts
478
- import { join as join6 } from "node:path";
479
- import { mkdir as mkdir3 } from "node:fs/promises";
480
- var file = (context2, app) => join6(cliDir(context2), `${app.id}.json`);
481
- async function remember(context2, app, values) {
482
- if (Object.keys(await readJson(file(context2, app))).length > 0) return;
483
- await mkdir3(cliDir(context2), { recursive: true, mode: 448 });
484
- await writeWhole(file(context2, app), `${JSON.stringify({ had: true, ...values }, null, 2)}
485
- `, null, { secret: true });
486
- }
487
- async function recall(context2, app) {
488
- return await readJson(file(context2, app));
489
- }
490
- var forget = async (context2, app) => {
491
- await removeFile(file(context2, app));
492
- };
493
-
494
- // src/apps/cline.ts
495
- var ID = "adaptiq";
496
- var dir = (context2) => join7(context2.env.CLINE_DATA_DIR ?? join7(context2.env.CLINE_DIR ?? join7(context2.home, ".cline"), "data"), "settings");
497
- var file2 = (context2, name2) => join7(dir(context2), name2);
498
- var HUB = "^[^ ]+ --cline-hub-daemon( |$)";
499
- function clineModel(model2) {
500
- const { input, ...window } = modelWindow(model2);
501
- return {
502
- id: model2.id,
503
- capabilities: ["streaming", "tools", ...model2.reasoning === null ? [] : ["reasoning"], ...input === void 0 ? [] : ["images"]],
504
- ...model2.reasoning === null ? {} : { supportsReasoning: true },
505
- ...window
506
- };
507
- }
508
- async function apply(context2, facts) {
509
- const base = apiBase(context2);
510
- const entries = facts.models.map(clineModel);
511
- const chosen = facts.models[0].id;
512
- const { key } = facts;
513
- const providers = record(await readJson(file2(context2, "providers.json")));
514
- const settings = await readJson(file2(context2, "global-settings.json"));
515
- await remember(context2, cline, { lastUsedProvider: providers.lastUsedProvider ?? null, compactionEnabled: settings.compactionEnabled ?? null });
516
- await updateJson(file2(context2, "models.json"), (value) => {
517
- const all = record(value.providers);
518
- const had = record(record(all[ID]).models);
519
- value.version = 1;
520
- value.providers = {
521
- ...all,
522
- [ID]: {
523
- provider: {
524
- name: "AdaptiqAI",
525
- baseUrl: base,
526
- defaultModelId: chosen,
527
- capabilities: ["streaming", "tools", ...facts.models.every((model2) => model2.reasoning !== null) ? ["reasoning"] : []],
528
- modelsSourceUrl: `${base}/models`
529
- },
530
- models: Object.fromEntries(entries.map((entry2) => {
531
- const id = String(entry2.id);
532
- const own = record(had[id]);
533
- return [id, { ...own, ...entry2, name: own.name ?? id }];
534
- })),
535
- discoveredModelIds: entries.map((entry2) => entry2.id)
536
- }
537
- };
538
- }, context2.backups, { secret: true });
539
- await updateJson(file2(context2, "providers.json"), (value) => {
540
- const all = record(value.providers);
541
- const was = record(all[ID]);
542
- const wanted = { provider: ID, apiKey: key, baseUrl: base, model: chosen };
543
- const same = sameJson(was.settings) === sameJson(wanted);
544
- value.version = 1;
545
- value.modes = value.modes ?? {};
546
- value.providers = { ...all, [ID]: { settings: wanted, updatedAt: same ? was.updatedAt : new Date(context2.now()).toISOString(), tokenSource: "manual" } };
547
- value.lastUsedProvider = ID;
548
- }, context2.backups, { secret: true });
549
- await updateJson(file2(context2, "global-settings.json"), (value) => {
550
- value.compactionEnabled = false;
551
- }, context2.backups, { secret: true });
552
- }
553
- var HEX = (text) => Buffer.from(text, "utf16le").toString("hex");
554
- async function appState(context2, facts) {
555
- const root = join7(context2.home, "Library", "WebKit", "bot.cline.app", "WebsiteData", "Default");
556
- const saltFile = join7(root, "salt");
557
- await mkdir4(root, { recursive: true });
558
- if (!existsSync3(saltFile)) await writeFile2(saltFile, randomBytes(8));
559
- const folder2 = createHash("sha256").update(Buffer.concat([Buffer.from("tauri://localhost"), await readFile2(saltFile)])).digest("base64url");
560
- const home3 = join7(root, folder2, folder2);
561
- await mkdir4(join7(home3, "LocalStorage"), { recursive: true });
562
- const part = (text) => {
563
- const bytes = Buffer.from(text);
564
- const size = Buffer.alloc(4);
565
- size.writeUInt32LE(bytes.length);
566
- return Buffer.concat([size, Buffer.from([1]), bytes]);
567
- };
568
- const one = Buffer.concat([part("tauri"), part("localhost"), Buffer.from([0])]);
569
- const origin = join7(home3, "origin");
570
- if (!existsSync3(origin)) await writeFile2(origin, Buffer.concat([one, one]));
571
- const store = join7(home3, "LocalStorage", "localstorage.sqlite3");
572
- const sql = async (text) => {
573
- const done = await context2.run("sqlite3", [store, text], { timeoutMs: 1e4 });
574
- if (done.code !== 0) throw new Error("sqlite3 failed");
575
- return done.stdout.trim();
576
- };
577
- const blob = (value) => `X'${HEX(JSON.stringify(value))}'`;
578
- const read = async (key) => {
579
- try {
580
- return record(JSON.parse(Buffer.from(await sql(`SELECT hex(value) FROM ItemTable WHERE key='${key}'`), "hex").toString("utf16le")));
581
- } catch {
582
- return {};
583
- }
584
- };
585
- const SELECTION = "cline.code.model-selection.v1";
586
- const EFFORT = "cline.code.reasoning-selection.v1";
587
- await sql("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)");
588
- const selection = await read(SELECTION);
589
- const chosen = { lastProvider: ID, lastModelByProvider: { ...record(selection.lastModelByProvider), [ID]: facts.models[0].id } };
590
- if (sameJson(selection) !== sameJson(chosen)) {
591
- await sql(`INSERT INTO ItemTable VALUES('${SELECTION}',${blob(chosen)});INSERT OR IGNORE INTO ItemTable VALUES('cline.code.onboarding.v1',${blob({ completedAt: new Date(context2.now()).toISOString() })})`);
592
- }
593
- const level = facts.models[0].reasoning?.defaultLevel ?? "";
594
- const picked = await read(EFFORT);
595
- if (["low", "medium", "high", "xhigh"].includes(level) && picked[ID] === void 0) {
596
- await sql(`INSERT INTO ItemTable VALUES('${EFFORT}',${blob({ ...picked, [ID]: level })})`);
597
- }
598
- }
599
- async function restart(context2) {
600
- if (context2.platform === "darwin") await context2.run("osascript", ["-e", 'quit app "Cline"'], { timeoutMs: 35e3 }).catch(() => void 0);
601
- else await context2.run("pkill", ["-x", "cline-app"], { timeoutMs: 5e3 }).catch(() => void 0);
602
- await context2.run("pkill", ["-f", "--", HUB], { timeoutMs: 5e3 }).catch(() => void 0);
603
- for (let tries = 0; tries < 10 && await busy(context2); tries++) await context2.sleep(500);
604
- await context2.run("pkill", ["-x", "cline-app"], { timeoutMs: 5e3 }).catch(() => void 0);
605
- return !await busy(context2);
606
- }
607
- async function busy(context2) {
608
- if (context2.platform === "win32") return false;
609
- if (await running(context2, "cline-app")) return true;
610
- return (await context2.run("pgrep", ["-f", "--", HUB], { timeoutMs: 5e3 }).catch(() => void 0))?.code === 0;
611
- }
612
- var cline = {
613
- id: "cline",
614
- name: "Cline",
615
- install: "npm install -g cline",
616
- does: "Added AdaptiqAI to Cline and turned off its automatic compaction, for every model. Your other providers stay.",
617
- signsInAs: async () => "Cline",
618
- found: async (context2) => command(context2, "cline") !== void 0 || command(context2, "cline-app") !== void 0 || macApp(context2, "Cline") !== void 0 || existsSync3(dir(context2)),
619
- async prepare(context2) {
620
- await Promise.all(["providers.json", "models.json", "global-settings.json"].map((name2) => readJson(file2(context2, name2))));
621
- },
622
- async current(context2) {
623
- const settings = record(record(record(await readJson(file2(context2, "providers.json"))).providers)[ID]).settings;
624
- const { baseUrl, model: model2, apiKey } = record(settings);
625
- return typeof baseUrl === "string" ? { base: baseUrl, ...typeof model2 === "string" ? { model: model2 } : {}, ...typeof apiKey === "string" ? { key: apiKey } : {} } : {};
640
+ return installed(context2) ?? join6(await launcherDir(context2), name(context2));
626
641
  },
627
- async apply(context2, facts) {
628
- await apply(context2, facts);
629
- const notes = [];
630
- if (await busy(context2) && !(await context2.confirm("Cline is running, and reads its settings when it starts. Quit it now?") && await restart(context2))) {
631
- notes.push("Cline is running. Quit it and open it again to see AdaptiqAI.");
632
- }
633
- if (macApp(context2, "Cline") !== void 0 && !await busy(context2)) await appState(context2, facts).catch(() => {
634
- });
635
- return notes;
636
- },
637
- async remove(context2) {
638
- const had = await recall(context2, cline);
639
- await updateJson(file2(context2, "providers.json"), (value) => {
640
- const all = record(value.providers);
641
- delete all[ID];
642
- value.providers = all;
643
- if (value.lastUsedProvider === ID) {
644
- if (typeof had.lastUsedProvider === "string") value.lastUsedProvider = had.lastUsedProvider;
645
- else delete value.lastUsedProvider;
646
- }
647
- }, context2.backups, { secret: true });
648
- await updateJson(file2(context2, "models.json"), (value) => {
649
- const all = record(value.providers);
650
- delete all[ID];
651
- value.providers = all;
652
- }, context2.backups, { secret: true });
653
- if ("compactionEnabled" in had) {
654
- await updateJson(file2(context2, "global-settings.json"), (value) => {
655
- if (had.compactionEnabled === null) delete value.compactionEnabled;
656
- else value.compactionEnabled = had.compactionEnabled;
657
- }, context2.backups, { secret: true });
658
- }
659
- await forget(context2, cline);
660
- },
661
- start: async (context2) => macApp(context2, "Cline") === void 0 ? "cline" : "open -a Cline"
642
+ greet: async (context2, model2, hello2) => context2.platform === "win32" ? void 0 : `${shellWord(await claudeCode.start(context2, model2))} ${shellWord(hello2)}`
662
643
  };
663
644
 
664
645
  // src/apps/codex.ts
665
- import { chmod as chmod3, mkdir as mkdir5 } from "node:fs/promises";
666
- import { join as join8 } from "node:path";
646
+ import { existsSync as existsSync4 } from "node:fs";
647
+ import { chmod as chmod3, mkdir as mkdir4 } from "node:fs/promises";
648
+ import { join as join7 } from "node:path";
667
649
  var PROVIDER = "adaptiqai-connect";
668
- var home = (context2) => context2.env.CODEX_HOME ?? join8(context2.home, ".codex");
669
- var profile = (context2) => join8(home(context2), "adaptiq.config.toml");
670
- var catalog = (context2) => join8(home(context2), "adaptiq.models.json");
671
- var keyFile2 = (context2) => join8(configDir(context2.env, context2.home), "codex.key");
650
+ var home = (context2) => context2.env.CODEX_HOME ?? join7(context2.home, ".codex");
651
+ var profile = (context2) => join7(home(context2), "adaptiq.config.toml");
652
+ var catalog = (context2) => join7(home(context2), "adaptiq.models.json");
653
+ var keyFile2 = (context2) => join7(configDir(context2.env, context2.home), "codex.key");
654
+ var wrapper = (context2) => join7(configDir(context2.env, context2.home), context2.platform === "win32" ? "codex-mcp.cmd" : "codex-mcp");
672
655
  var codexCommand = (context2) => command(context2, "codex") ?? "codex";
673
656
  var toml = (text) => JSON.stringify(text);
657
+ var REFRESH_JS = [
658
+ 'const fs=require("fs"),[file,body]=process.argv.slice(1),',
659
+ 'now=JSON.parse(fs.readFileSync(file,"utf8")).models,got=JSON.parse(body).models;',
660
+ 'if(Array.isArray(now)&&now.length&&Array.isArray(got)&&got.length&&got.every((m)=>m&&typeof m.slug=="string"&&m.slug&&typeof m.display_name=="string"',
661
+ "&&Number.isInteger(m.priority)&&Array.isArray(m.supported_reasoning_levels)&&Array.isArray(m.input_modalities))){",
662
+ "const{slug,display_name,priority,supported_reasoning_levels,default_reasoning_level,context_window,",
663
+ 'max_context_window,effective_context_window_percent,input_modalities,...rest}=now[0],tmp=file+".adaptiq";',
664
+ "fs.writeFileSync(tmp,JSON.stringify({models:got.map((m)=>({...rest,...m}))}));fs.renameSync(tmp,file)}"
665
+ ].join("\n");
666
+ var catalogUpdate = (context2) => [
667
+ 'V="$D/codex-models.checked"; C="${CODEX_HOME:-$HOME/.codex}/adaptiq.models.json";',
668
+ ' if [ -f "$C" ] && command -v curl >/dev/null 2>&1 && command -v node >/dev/null 2>&1 && [ -z "$(find "$V" -mtime -1 2>/dev/null)" ]; then touch "$V";\n',
669
+ `T=$(printf 'Authorization: Bearer %s\\n' "$(tr -cd 'A-Za-z0-9._-' <"$D/codex.key")" | curl -fsS --max-time 20 -H @- '${context2.origin}/v1/cli-auth/codex/models') && node -e '
670
+ `,
671
+ REFRESH_JS,
672
+ `' "$C" "$T"; fi`
673
+ ].join("");
674
+ function wrapperText(context2) {
675
+ if (context2.platform === "win32") {
676
+ return [
677
+ "@echo off",
678
+ "rem AdaptiqAI's MCP server for Codex, from npx -y adaptiqai connect codex. Running it again writes this again.",
679
+ 'set /p ADAPTIQAI_API_KEY=<"%~dp0codex.key"',
680
+ '"%~dp0mcp\\node_modules\\.bin\\adaptiqai-mcp.cmd" %*',
681
+ ""
682
+ ].join("\r\n");
683
+ }
684
+ return [
685
+ "#!/bin/sh",
686
+ "# AdaptiqAI's MCP server for Codex, from `npx -y adaptiqai connect codex`. Running it again writes this again.",
687
+ 'D=$(dirname "$0"); X="$D/mcp/node_modules/.bin/adaptiqai-mcp"',
688
+ // A Node.js AdaptiqAI installed comes last: the server and npm run on it where there is no other.
689
+ `PATH="$PATH:${ADAPTIQ_NODE_BIN}"`,
690
+ `( ${MCP_UPDATE} ) </dev/null >/dev/null 2>&1 &`,
691
+ `( ${catalogUpdate(context2)} ) </dev/null >/dev/null 2>&1 &`,
692
+ 'ADAPTIQAI_API_KEY=$(cat "$D/codex.key") exec "$X" "$@"',
693
+ ""
694
+ ].join("\n");
695
+ }
696
+ function tools(context2) {
697
+ if (!existsSync4(mcpServer(context2))) return [];
698
+ const env = mcpEnv(context2);
699
+ const windows2 = context2.platform === "win32";
700
+ const prefix = env === void 0 ? "" : Object.entries(env).map(([name2, value]) => `${name2}='${value}' `).join("");
701
+ return [
702
+ "[mcp_servers.adaptiqai]",
703
+ ...windows2 ? ['command = "cmd"', `args = ["/d", "/c", ${toml(wrapper(context2))}]`] : [`command = ${toml(wrapper(context2))}`],
704
+ 'env_vars = ["CODEX_HOME"]',
705
+ ...env === void 0 ? [] : [`env = { ${Object.entries(env).map(([name2, value]) => `${name2} = ${toml(value)}`).join(", ")} }`],
706
+ "",
707
+ ...["adaptiq_knowledge", "adaptiq_memory"].flatMap((tool) => [`[mcp_servers.adaptiqai.tools.${tool}]`, 'approval_mode = "approve"', ""]),
708
+ ...windows2 || !mcpHasHook(context2) ? [] : [
709
+ "[[hooks.UserPromptSubmit]]",
710
+ "[[hooks.UserPromptSubmit.hooks]]",
711
+ 'type = "command"',
712
+ `command = ${toml(`${prefix}ADAPTIQAI_API_KEY="$(cat '${keyFile2(context2)}')" exec '${mcpServer(context2)}' hook codex`)}`,
713
+ "timeout = 30",
714
+ ""
715
+ ]
716
+ ];
717
+ }
674
718
  function config(context2, facts) {
675
719
  const [model2] = facts.models;
676
720
  const path = keyFile2(context2);
@@ -694,7 +738,8 @@ function config(context2, facts) {
694
738
  `[model_providers.${PROVIDER}.auth]`,
695
739
  `command = ${toml(read[0])}`,
696
740
  `args = [${read[1].map(toml).join(", ")}]`,
697
- ""
741
+ "",
742
+ ...tools(context2)
698
743
  ].join("\n");
699
744
  }
700
745
  function catalogEntry(model2, priority, instructions2) {
@@ -736,11 +781,12 @@ var codex = {
736
781
  id: "codex",
737
782
  name: "Codex",
738
783
  install: "npm install -g @openai/codex",
739
- does: "Added the `adaptiq` profile, which starts Codex on AdaptiqAI. Your own Codex stays as it is.",
784
+ does: "Added the `adaptiq` profile, which starts Codex on AdaptiqAI with your memory and knowledge. Your own Codex stays as it is.",
740
785
  signsInAs: async () => "Codex",
741
786
  found: async (context2) => command(context2, "codex") !== void 0,
742
787
  async prepare(context2) {
743
788
  await instructions(context2);
789
+ await installMcp(context2, "Codex");
744
790
  },
745
791
  async current(context2) {
746
792
  const text = await readText(profile(context2));
@@ -753,14 +799,14 @@ var codex = {
753
799
  async apply(context2, facts) {
754
800
  const prompt = await instructions(context2);
755
801
  const folder2 = configDir(context2.env, context2.home);
756
- await mkdir5(folder2, { recursive: true, mode: 448 });
802
+ await mkdir4(folder2, { recursive: true, mode: 448 });
757
803
  await chmod3(folder2, 448).catch(() => {
758
804
  });
759
805
  await writeWhole(keyFile2(context2), `${facts.key}
760
806
  `, null, { secret: true });
761
- await mkdir5(home(context2), { recursive: true });
762
- await writeWhole(catalog(context2), `${JSON.stringify({ models: facts.models.map((model2, priority) => catalogEntry(model2, priority, prompt)) })}
763
- `, null);
807
+ await writeWhole(wrapper(context2), wrapperText(context2), null, { mode: 493 });
808
+ await mkdir4(home(context2), { recursive: true });
809
+ await writeWhole(catalog(context2), JSON.stringify({ models: facts.models.map((model2, priority) => catalogEntry(model2, priority, prompt)) }), null);
764
810
  await writeWhole(profile(context2), config(context2, facts), context2.backups, { secret: true });
765
811
  return [];
766
812
  },
@@ -768,15 +814,41 @@ var codex = {
768
814
  await removeFile(profile(context2));
769
815
  await removeFile(catalog(context2));
770
816
  await removeFile(keyFile2(context2));
817
+ await removeFile(wrapper(context2));
771
818
  },
772
- start: async () => "codex --profile adaptiq"
819
+ // With Codex taken off this computer, or its profile, there was nothing to remove, and the key stayed in its file.
820
+ clean: async (context2) => ({ removed: await removeFile(keyFile2(context2)) ? [keyFile2(context2)] : [], left: [] }),
821
+ start: async () => "codex --profile adaptiq",
822
+ greet: async (context2, _model, hello2) => context2.platform === "win32" ? void 0 : `codex --profile adaptiq ${shellWord(hello2)}`
773
823
  };
774
824
 
775
825
  // src/apps/deepseek.ts
776
- import { chmod as chmod4, mkdir as mkdir6, mkdtemp as mkdtemp2, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
777
- import { existsSync as existsSync4 } from "node:fs";
826
+ import { createHash } from "node:crypto";
827
+ import { access as access2, chmod as chmod4, constants as constants3, mkdir as mkdir6, mkdtemp as mkdtemp2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
828
+ import { createWriteStream, existsSync as existsSync5 } from "node:fs";
829
+ import { Readable } from "node:stream";
830
+ import { pipeline } from "node:stream/promises";
778
831
  import { tmpdir as tmpdir2 } from "node:os";
779
832
  import { delimiter as delimiter2, join as join9 } from "node:path";
833
+
834
+ // src/record.ts
835
+ import { join as join8 } from "node:path";
836
+ import { mkdir as mkdir5 } from "node:fs/promises";
837
+ var file = (context2, app) => join8(cliDir(context2), `${app.id}.json`);
838
+ async function remember(context2, app, values) {
839
+ if (Object.keys(await readJson(file(context2, app))).length > 0) return;
840
+ await mkdir5(cliDir(context2), { recursive: true, mode: 448 });
841
+ await writeWhole(file(context2, app), `${JSON.stringify({ had: true, ...values }, null, 2)}
842
+ `, null, { secret: true });
843
+ }
844
+ async function recall(context2, app) {
845
+ return await readJson(file(context2, app));
846
+ }
847
+ var forget = async (context2, app) => {
848
+ await removeFile(file(context2, app));
849
+ };
850
+
851
+ // src/apps/deepseek.ts
780
852
  var PLUGIN = "@adaptiqai/deepseek-harness";
781
853
  var ITS = "AdaptiqAI's plugin for DeepSeek Harness";
782
854
  var BUNDLE = "com.deepseek.dsh";
@@ -788,6 +860,56 @@ var credentials = (context2) => join9(home2(context2), ".credentials.yaml");
788
860
  var appApp = (context2) => macApp(context2, "DeepSeek Harness");
789
861
  var appBinary = (app) => join9(app, "Contents", "MacOS", "DeepSeek Harness");
790
862
  var NPX = ["npx", "-y", "@deepseek-ai/dsh"];
863
+ var TEAM = "NAN929V4UM";
864
+ function feedField(feed, key) {
865
+ const lines = feed.split("\n");
866
+ const at = lines.findIndex((line) => line.startsWith(`${key}:`));
867
+ if (at < 0) return void 0;
868
+ const value = lines[at]?.slice(key.length + 1).trim() ?? "";
869
+ return (/^[>|]/u.test(value) ? lines[at + 1]?.trim() : value)?.split(/\s/u)[0] || void 0;
870
+ }
871
+ async function installMacApp(context2) {
872
+ const arm = (await context2.run("sysctl", ["-n", "hw.optional.arm64"], { timeoutMs: 5e3 }).catch(() => void 0))?.stdout.trim() === "1";
873
+ const feedUrl = `https://download.deepseek.com/dsh-desk/feeds/mac-${arm ? "arm64" : "x64"}/nightly-mac.yml`;
874
+ const temp = await mkdtemp2(join9(tmpdir2(), "adaptiq-dsh-"));
875
+ try {
876
+ const feed = await context2.fetch(feedUrl);
877
+ if (!feed.ok) return false;
878
+ const text = await feed.text();
879
+ const path = feedField(text, "path");
880
+ const sum = feedField(text, "sha512");
881
+ if (path === void 0 || sum === void 0) return false;
882
+ const url = new URL(path, feedUrl);
883
+ if (url.protocol !== "https:") return false;
884
+ context2.say.note("Downloading the DeepSeek Harness app...");
885
+ const zip = await context2.fetch(url.href);
886
+ if (!zip.ok || zip.body === null) return false;
887
+ const file3 = join9(temp, "app.zip");
888
+ const hash = createHash("sha512");
889
+ await pipeline(Readable.fromWeb(zip.body), async function* (chunks) {
890
+ for await (const chunk of chunks) {
891
+ hash.update(chunk);
892
+ yield chunk;
893
+ }
894
+ }, createWriteStream(file3));
895
+ if (hash.digest("base64") !== sum) return false;
896
+ const app = join9(temp, "DeepSeek Harness.app");
897
+ for (const [command2, args] of [
898
+ ["ditto", ["-x", "-k", file3, temp]],
899
+ ["spctl", ["-a", "-t", "exec", app]],
900
+ ["codesign", ["-v", `-R=anchor apple generic and certificate leaf[subject.OU] = ${TEAM}`, app]]
901
+ ]) {
902
+ if ((await context2.run(command2, [...args], { timeoutMs: 3e5 })).code !== 0) return false;
903
+ }
904
+ const folder2 = await access2("/Applications", constants3.W_OK).then(() => "/Applications", () => join9(context2.home, "Applications"));
905
+ if ((await context2.run("mkdir", ["-p", folder2], { timeoutMs: 1e4 })).code !== 0) return false;
906
+ return (await context2.run("mv", [app, `${folder2}/`], { timeoutMs: 12e4 })).code === 0;
907
+ } catch {
908
+ return false;
909
+ } finally {
910
+ await rm3(temp, { recursive: true, force: true });
911
+ }
912
+ }
791
913
  var EDIT_APP = String.raw`
792
914
  const fs=require("fs"),path=require("path"),os=require("os");
793
915
  const [app,base,provider,root,site]=process.argv.slice(1),given=JSON.parse(provider),ids=given.models.map((m)=>m.id);
@@ -852,6 +974,21 @@ else patch.deleteIn([p])}}
852
974
  save(pf,patch)}
853
975
  }catch(e){quit(String(e&&e.message||e))}
854
976
  `;
977
+ var CLEAN_APP = String.raw`
978
+ const fs=require("fs"),path=require("path"),os=require("os");
979
+ const [app,root]=process.argv.slice(1);
980
+ const {parseDocument,isMap}=require(path.join(app,"Contents","Resources","app.asar","dsh","node_modules","yaml"));
981
+ const home=process.env.DSH_HOME||path.join(os.homedir(),".dsh"),quit=(text)=>{console.error(text);process.exit(1)};
982
+ try{
983
+ const cf=path.join(home,".credentials.yaml");let text;
984
+ try{text=fs.readFileSync(cf,"utf8")}catch(e){if(e.code==="ENOENT")process.exit(0);throw e}
985
+ const keys=parseDocument(text,{customTags:[{tag:"tag:yaml.org,2002:js",resolve:(v)=>v}]});if(keys.errors.length)quit(cf+" is not valid YAML.");
986
+ const refs=keys.get("refs");if(isMap(refs)){refs.delete("${APP_KEY}");if(!refs.items.length)keys.delete("refs")}
987
+ if(keys.getIn(["records","adaptiq/workspace","payload","apiBase"])===root)keys.deleteIn(["records","adaptiq/workspace"]);
988
+ if(String(keys)===text)process.exit(0);
989
+ const tmp=cf+".adaptiq";fs.writeFileSync(tmp,String(keys),{mode:0o600});fs.chmodSync(tmp,0o600);fs.renameSync(tmp,cf);console.log("changed")
990
+ }catch(e){quit(String(e&&e.message||e))}
991
+ `;
855
992
  function dshModel(model2) {
856
993
  const { reasoning } = model2;
857
994
  return {
@@ -904,9 +1041,9 @@ async function withPnpm(context2, work) {
904
1041
  if (which("pnpm", context2.env, context2.platform) !== void 0) return await work({});
905
1042
  const bin2 = await mkdtemp2(join9(tmpdir2(), "adaptiq-pnpm-"));
906
1043
  try {
907
- if (context2.platform === "win32") await writeFile3(join9(bin2, "pnpm.cmd"), "@npx -y pnpm@11 %*\r\n");
1044
+ if (context2.platform === "win32") await writeFile2(join9(bin2, "pnpm.cmd"), "@npx -y pnpm@11 %*\r\n");
908
1045
  else {
909
- await writeFile3(join9(bin2, "pnpm"), '#!/bin/sh\nexec npx -y pnpm@11 "$@"\n');
1046
+ await writeFile2(join9(bin2, "pnpm"), '#!/bin/sh\nexec npx -y pnpm@11 "$@"\n');
910
1047
  await chmod4(join9(bin2, "pnpm"), 493);
911
1048
  }
912
1049
  return await work({ PATH: `${bin2}${delimiter2}${context2.env.PATH ?? ""}` });
@@ -915,38 +1052,39 @@ async function withPnpm(context2, work) {
915
1052
  }
916
1053
  }
917
1054
  async function makeAppProfile(context2, app) {
918
- if (existsSync4(join9(profileDir(context2, "desktop"), "package.json"))) return;
1055
+ if (existsSync5(join9(profileDir(context2, "desktop"), "package.json"))) return;
919
1056
  const script = `(async()=>{const path=require("path"),url=require("url");const [app,prof]=process.argv.slice(1);
920
- const boot=await import(url.pathToFileURL(path.join(app,"Contents","Resources","app.asar","dsh","node_modules","@deepseek-ai","dsh-app-boot","lib","index.js")).href);
921
- boot.initProfile(prof,boot.PROFILE_TEMPLATES.web.bundles)})().catch((e)=>{console.error(String(e&&e.message||e));process.exit(1)})`;
1057
+ const lib=(pkg)=>import(url.pathToFileURL(path.join(app,"Contents","Resources","app.asar","dsh","node_modules","@deepseek-ai",pkg,"lib","index.js")).href);
1058
+ const boot=await lib("dsh-app-boot"),templates=boot.PROFILE_TEMPLATES||(await lib("dsh-official-bundles")).PROFILE_TEMPLATES;
1059
+ boot.initProfile(prof,templates.web.bundles)})().catch((e)=>{console.error(String(e&&e.message||e));process.exit(1)})`;
922
1060
  const done = await context2.run(appBinary(app), ["-e", script, app, profileDir(context2, "desktop")], { env: { ELECTRON_RUN_AS_NODE: "1" }, timeoutMs: 6e4 });
923
1061
  if (done.code !== 0) throw new Problem("app_refused", `DeepSeek Harness could not make its profile: ${done.stderr.trim().split("\n").pop() ?? ""}`);
924
1062
  }
925
1063
  async function applyApp(context2, facts, app, notes) {
926
- const wasOpen = await open(context2);
1064
+ const wasOpen = await open2(context2);
927
1065
  const pluginThere = await installedVersion(context2, "desktop") !== "";
928
1066
  const quit2 = wasOpen && await context2.confirm("DeepSeek Harness is open, and AdaptiqAI's plugin goes in while it is closed. Quit it now? It opens again after.");
929
1067
  if (quit2 && !await quitMacApp(context2, BUNDLE)) throw new Problem("app_open", "DeepSeek Harness did not quit, so it was left as it was. Quit it, then run this again.");
930
1068
  if (wasOpen && !quit2 && !pluginThere) throw new Problem("app_open", "DeepSeek Harness is open, and AdaptiqAI's plugin goes in while it is closed. Quit it, then run this again.");
931
1069
  try {
932
- if ((!wasOpen || quit2) && (!pluginThere || facts.deviceCode !== void 0)) {
1070
+ if (!wasOpen || quit2) {
933
1071
  await makeAppProfile(context2, app);
934
1072
  await ensurePlugin(context2, [join9(app, "Contents", "Resources", "runtime", "cli", "bin", "dsh")], "desktop", {}, notes);
935
1073
  }
936
1074
  await rememberDefault(context2, "desktop");
937
1075
  const files = [credentials(context2), join9(profileDir(context2, "desktop"), "cordis.patch.yml")];
938
- for (const file4 of files) await context2.backups.keep(file4);
1076
+ for (const file3 of files) await context2.backups.keep(file3);
939
1077
  const edited = await context2.run(appBinary(app), ["-e", EDIT_APP, app, apiBase(context2), JSON.stringify(dshProvider(facts.models)), context2.origin, context2.console], {
940
1078
  env: { ELECTRON_RUN_AS_NODE: "1", K: facts.key },
941
1079
  timeoutMs: 6e4
942
1080
  });
943
1081
  if (edited.code !== 0) throw new Problem("app_refused", edited.stderr.trim().split("\n").pop() ?? "DeepSeek Harness's settings could not be changed.");
944
- for (const file4 of files) await context2.backups.dropIfSame(file4);
1082
+ for (const file3 of files) await context2.backups.dropIfSame(file3);
945
1083
  } finally {
946
1084
  if (quit2) await openMacApp(context2, app);
947
1085
  }
948
1086
  }
949
- var open = async (context2) => {
1087
+ var open2 = async (context2) => {
950
1088
  const said = await context2.run("osascript", ["-e", `application id "${BUNDLE}" is running`], { timeoutMs: 5e3 }).catch(() => void 0);
951
1089
  return said?.stdout.trim() === "true";
952
1090
  };
@@ -960,7 +1098,7 @@ async function rememberDefault(context2, profile2) {
960
1098
  async function applyWeb(context2, facts, notes) {
961
1099
  const profile2 = profileDir(context2, "adaptiq");
962
1100
  await withPnpm(context2, async (env2) => {
963
- if (!existsSync4(profile2)) {
1101
+ if (!existsSync5(profile2)) {
964
1102
  const made = await context2.run(NPX[0], [...NPX.slice(1), "--profile", "adaptiq", "--from-default-profile", "web", "--dump-config"], { env: env2, timeoutMs: 6e5, cwd: context2.home });
965
1103
  if (made.code !== 0) throw new Problem("app_refused", `DeepSeek Harness could not be fetched: ${made.stderr.trim().split("\n").pop() ?? ""}`);
966
1104
  }
@@ -987,11 +1125,16 @@ var deepseek = {
987
1125
  aliases: ["deepseek-harness", "dsh"],
988
1126
  name: "DeepSeek Harness",
989
1127
  install: "Get the app from deepseek.com.",
1128
+ desktop: "https://www.deepseek.com/harness/",
1129
+ installMacApp,
990
1130
  does: "Installed AdaptiqAI's plugin in DeepSeek Harness, with your memory and knowledge. Your other providers stay.",
991
1131
  fetched: true,
992
1132
  // The app signs in under a name of its own, so the console knows it again.
993
1133
  signsInAs: async (context2) => appApp(context2) === void 0 ? "DeepSeek Harness" : "DeepSeek Harness app",
994
- found: async (context2) => appApp(context2) !== void 0 || existsSync4(home2(context2)),
1134
+ found: async (context2) => appApp(context2) !== void 0 || existsSync5(home2(context2)),
1135
+ async prepare(context2) {
1136
+ if (appApp(context2) === void 0) await needNode(context2, "24.9.0", "DeepSeek Harness in the browser", ["connect", "deepseek"]);
1137
+ },
995
1138
  async current(context2) {
996
1139
  const app = appApp(context2) !== void 0;
997
1140
  const patch = await readText(join9(profileDir(context2, app ? "desktop" : "adaptiq"), "cordis.patch.yml")) ?? "";
@@ -1026,11 +1169,35 @@ var deepseek = {
1026
1169
  }
1027
1170
  await forget(context2, deepseek);
1028
1171
  },
1172
+ async clean(context2) {
1173
+ const removed = [];
1174
+ const app = appApp(context2);
1175
+ if (app !== void 0) {
1176
+ await context2.backups.keep(credentials(context2));
1177
+ const done = await context2.run(appBinary(app), ["-e", CLEAN_APP, app, context2.origin], { env: { ELECTRON_RUN_AS_NODE: "1" }, timeoutMs: 6e4 });
1178
+ if (done.code !== 0) throw new Problem("app_refused", done.stderr.trim().split("\n").pop() ?? "DeepSeek Harness's credentials could not be changed.");
1179
+ if (done.stdout.includes("changed")) removed.push(credentials(context2));
1180
+ await context2.backups.dropIfSame(credentials(context2));
1181
+ }
1182
+ const env = join9(home2(context2), ".env");
1183
+ const text = await readText(env);
1184
+ if (text?.split("\n").some((line) => line.startsWith(`${WEB_KEY}=`)) === true) {
1185
+ await writeWhole(env, `${text.split("\n").filter((line) => !line.startsWith(`${WEB_KEY}=`) && line !== "").join("\n")}
1186
+ `, context2.backups, { secret: true });
1187
+ removed.push(env);
1188
+ }
1189
+ if (!await fileHoldsKey(credentials(context2))) return { removed, left: [] };
1190
+ return {
1191
+ removed,
1192
+ left: [app === void 0 ? `DeepSeek Harness's credentials still hold an AdaptiqAI key (${credentials(context2)}), from a sign-in of AdaptiqAI's plugin. Start it (npx -y @deepseek-ai/dsh adaptiq) and type /adaptiq signout.` : `DeepSeek Harness's credentials still hold an AdaptiqAI key (${credentials(context2)}) that no setup of this AdaptiqAI wrote.`]
1193
+ };
1194
+ },
1029
1195
  start: async (context2) => appApp(context2) === void 0 ? "npx -y @deepseek-ai/dsh adaptiq" : 'open -a "DeepSeek Harness"'
1030
1196
  };
1031
1197
 
1032
1198
  // src/apps/hermes.ts
1033
- import { join as join10 } from "node:path";
1199
+ import { readdir as readdir2 } from "node:fs/promises";
1200
+ import { dirname as dirname3, isAbsolute, join as join10 } from "node:path";
1034
1201
  var PLUGIN2 = "AdaptiqAI/hermes-plugin";
1035
1202
  var ENTRY = "adaptiqai-connect";
1036
1203
  var KEY_ENV = "ADAPTIQ_API_KEY";
@@ -1050,9 +1217,29 @@ async function envFile(context2) {
1050
1217
  }
1051
1218
  var envLine = new RegExp(`^\\s*(?:export\\s+)?${KEY_ENV}\\s*=`, "u");
1052
1219
  var withKey = (text, key) => {
1053
- const lines = text.split("\n").filter((line) => !envLine.test(line) && line !== "");
1220
+ const lines = text.replace(/\n$/u, "").split("\n").filter((line) => !envLine.test(line));
1221
+ if (lines.length === 1 && lines[0] === "") lines.pop();
1054
1222
  return [...lines, ...key === void 0 ? [] : [`${KEY_ENV}=${key}`], ""].join("\n");
1055
1223
  };
1224
+ async function gatewayRuns(file3) {
1225
+ const pid = Number(/\d+/u.exec(await readText(join10(dirname3(file3), "gateway.pid")) ?? "")?.[0]);
1226
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1227
+ try {
1228
+ process.kill(pid, 0);
1229
+ return true;
1230
+ } catch (error) {
1231
+ return error.code === "EPERM";
1232
+ }
1233
+ }
1234
+ async function hermesHome(context2) {
1235
+ const file3 = await envFile(context2).catch(() => void 0);
1236
+ return file3 === void 0 ? context2.env.HERMES_HOME ?? join10(context2.home, ".hermes") : dirname3(file3);
1237
+ }
1238
+ var SNAPSHOT = /^hermes-snap-[A-Za-z0-9]+\.sh(?:\.tmp\.[A-Za-z0-9]+)?$/u;
1239
+ var snapshotFolders = (context2, home3) => [.../* @__PURE__ */ new Set([
1240
+ ...["TERMINAL_TEMP_DIR", "TMPDIR", "TMP", "TEMP"].map((name2) => context2.env[name2] ?? "").filter((path) => isAbsolute(path)),
1241
+ join10(home3, "cache", "terminal")
1242
+ ])];
1056
1243
  var modelBlock = async (context2) => {
1057
1244
  const said = await context2.run(hermes(context2), ["config", "get", "--json", "model"], { timeoutMs: 6e4 });
1058
1245
  if (said.code !== 0) return {};
@@ -1067,11 +1254,21 @@ var hermesApp = {
1067
1254
  id: "hermes",
1068
1255
  name: "Hermes Agent",
1069
1256
  install: "curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash",
1257
+ desktop: "https://hermes-agent.nousresearch.com/#downloads",
1070
1258
  does: "Added AdaptiqAI to Hermes as an endpoint of its own, and made it the main model. Your other providers stay.",
1071
1259
  signsInAs: async () => "Hermes Agent",
1072
1260
  found: async (context2) => command(context2, "hermes", [join10(context2.home, ".local", "bin", "hermes")]) !== void 0,
1073
1261
  async prepare(context2) {
1074
1262
  await envFile(context2);
1263
+ const installed2 = await context2.run(hermes(context2), ["plugins", "list"], { timeoutMs: 6e4 });
1264
+ const got = /adaptiq/iu.test(installed2.stdout) ? await context2.run(hermes(context2), ["plugins", "update", "adaptiq"], { timeoutMs: 18e4 }) : await context2.run(hermes(context2), ["plugins", "install", PLUGIN2, "--enable"], { timeoutMs: 18e4 });
1265
+ if (await quietly(context2, "plugins", "enable", "adaptiq") !== 0) {
1266
+ if (/could not read Username|Repository not found|Authentication failed/u.test(`${got.stdout}
1267
+ ${got.stderr}`)) {
1268
+ throw new Problem("tools_not_installed", "GitHub did not hand out AdaptiqAI's plugin for Hermes, so Hermes was left as it was. Running this again will not help until AdaptiqAI has fixed it: tell us under Help & support.");
1269
+ }
1270
+ throw new Problem("tools_not_installed", `AdaptiqAI's plugin for Hermes could not be installed, so Hermes was left as it was. Check the connection, then run this again, or install it yourself: hermes plugins install ${PLUGIN2} --enable`);
1271
+ }
1075
1272
  },
1076
1273
  async current(context2) {
1077
1274
  const model2 = await modelBlock(context2);
@@ -1084,15 +1281,11 @@ var hermesApp = {
1084
1281
  const notes = [];
1085
1282
  const was = await modelBlock(context2);
1086
1283
  await remember(context2, hermesApp, { model: was.provider === ENTRY ? {} : Object.fromEntries(MODEL_FIELDS.map((field) => [field, was[field] ?? null])) });
1087
- const installed2 = await context2.run(hermes(context2), ["plugins", "list"], { timeoutMs: 6e4 });
1088
- if (!/adaptiq/iu.test(installed2.stdout)) {
1089
- const added = await context2.run(hermes(context2), ["plugins", "install", PLUGIN2], { timeoutMs: 18e4 });
1090
- if (added.code !== 0) notes.push("AdaptiqAI's plugin for Hermes could not be installed, so conversations have no memory of their own yet. Run: hermes plugins install AdaptiqAI/hermes-plugin");
1091
- }
1284
+ await quietly(context2, "config", "set", "tools.tool_search.enabled", "off");
1092
1285
  const base = apiBase(context2);
1093
1286
  const [model2] = facts.models;
1094
- const file4 = await envFile(context2);
1095
- await writeWhole(file4, withKey(await readText(file4) ?? "", facts.key), context2.backups, { secret: true });
1287
+ const file3 = await envFile(context2);
1288
+ await writeWhole(file3, withKey(await readText(file3) ?? "", facts.key), context2.backups, { secret: true });
1096
1289
  await set(context2, `providers.${ENTRY}`, JSON.stringify({
1097
1290
  name: "AdaptiqAI",
1098
1291
  base_url: base,
@@ -1109,22 +1302,47 @@ var hermesApp = {
1109
1302
  await set(context2, "model.api_mode", "chat_completions");
1110
1303
  await set(context2, "model.key_env", KEY_ENV);
1111
1304
  await set(context2, "model.default", model2.id);
1112
- for (const entry2 of facts.models) await quietly(context2, "config", "set", `compression.model_thresholds.${entry2.id.replace(/\./gu, "\\.")}`, "1.0");
1305
+ for (const entry2 of facts.models) await quietly(context2, "config", "set", `compression.model_thresholds.${entry2.id.replace(/\./gu, "\\.")}`, "0.99");
1306
+ if (await gatewayRuns(file3)) {
1307
+ const restarted = await context2.confirm("Restart Hermes' gateway now, so its chats get AdaptiqAI's plugin?") && await quietly(context2, "gateway", "restart", "--detach") === 0;
1308
+ if (!restarted) notes.push("Hermes' chats get AdaptiqAI's plugin once its gateway restarts: hermes gateway restart");
1309
+ }
1113
1310
  if (await hermesOpen(context2)) notes.push("Hermes is open. Quit it and open it again to read the new key, then start a new chat.");
1114
1311
  return notes;
1115
1312
  },
1116
1313
  async remove(context2) {
1117
1314
  const had = record((await recall(context2, hermesApp)).model);
1118
1315
  await quietly(context2, "config", "unset", `providers.${ENTRY}`);
1119
- const file4 = await envFile(context2);
1120
- const text = await readText(file4);
1121
- if (text !== void 0) await writeWhole(file4, withKey(text, void 0), context2.backups, { secret: true });
1316
+ const file3 = await envFile(context2);
1317
+ const text = await readText(file3);
1318
+ if (text !== void 0) await writeWhole(file3, withKey(text, void 0), context2.backups, { secret: true });
1122
1319
  for (const field of MODEL_FIELDS) {
1123
1320
  const value = had[field];
1124
1321
  if (typeof value === "string" && value !== "") await set(context2, `model.${field}`, value);
1125
1322
  else await quietly(context2, "config", "unset", `model.${field}`);
1126
1323
  }
1127
1324
  await forget(context2, hermesApp);
1325
+ const notes = [];
1326
+ if (await gatewayRuns(file3)) {
1327
+ const restarted = await context2.confirm("Restart Hermes' gateway now, so it lets go of the AdaptiqAI key?") && await quietly(context2, "gateway", "restart", "--detach") === 0;
1328
+ if (!restarted) notes.push("Hermes' gateway keeps the AdaptiqAI key until it restarts: hermes gateway restart");
1329
+ }
1330
+ if (await hermesOpen(context2)) notes.push("Hermes is open, and keeps the AdaptiqAI key until it closes. Quit it, and open it again.");
1331
+ return notes;
1332
+ },
1333
+ async clean(context2) {
1334
+ const home3 = await hermesHome(context2);
1335
+ const removed = [];
1336
+ const env = join10(home3, ".env");
1337
+ const text = await readText(env);
1338
+ if (text?.split("\n").some((line) => envLine.test(line)) === true && await writeWhole(env, withKey(text, void 0), context2.backups, { secret: true })) removed.push(env);
1339
+ for (const folder2 of snapshotFolders(context2, home3)) {
1340
+ for (const name2 of (await readdir2(folder2).catch(() => [])).filter((entry2) => SNAPSHOT.test(entry2))) {
1341
+ const path = join10(folder2, name2);
1342
+ if (await fileHoldsKey(path) && await removeFile(path)) removed.push(path);
1343
+ }
1344
+ }
1345
+ return { removed, left: [] };
1128
1346
  },
1129
1347
  start: async (context2) => macApp(context2, "Hermes") === void 0 ? "hermes" : "open -b com.nousresearch.hermes",
1130
1348
  async ask(context2) {
@@ -1138,7 +1356,7 @@ async function hermesOpen(context2) {
1138
1356
  }
1139
1357
 
1140
1358
  // src/apps/opencode.ts
1141
- import { existsSync as existsSync5, readdirSync, readFileSync } from "node:fs";
1359
+ import { existsSync as existsSync6, readdirSync, readFileSync } from "node:fs";
1142
1360
  import { join as join11 } from "node:path";
1143
1361
  var KEY_ENV2 = "ADAPTIQ_API_KEY";
1144
1362
  var dataDir = (context2) => context2.env.XDG_DATA_HOME ?? join11(context2.home, ".local", "share");
@@ -1147,10 +1365,19 @@ async function signIns(context2, folder2) {
1147
1365
  return Object.fromEntries(Object.entries(all).filter(([, entry2]) => record(entry2).key === KEY_ENV2).map(([url, entry2]) => [url, record(entry2)]));
1148
1366
  }
1149
1367
  async function oneSignIn(context2, binary, folder2) {
1150
- for (const url of Object.keys(await signIns(context2, folder2))) {
1151
- if (url !== apiBase(context2)) await context2.run(binary, ["auth", "logout", url], { timeoutMs: 3e4 });
1368
+ const all = await readJson(join11(dataDir(context2), folder2, "auth.json")).catch(() => ({}));
1369
+ for (const [url, entry2] of Object.entries(all)) {
1370
+ if (url !== apiBase(context2) && (url === "adaptiqai" || record(entry2).key === KEY_ENV2)) await context2.run(binary, ["auth", "logout", url], { timeoutMs: 3e4 });
1152
1371
  }
1153
1372
  }
1373
+ async function providersLogin(context2, binary, folder2, name2, env) {
1374
+ const file3 = join11(dataDir(context2), folder2, "auth.json");
1375
+ const before = await readText(file3);
1376
+ const done = await context2.run(binary, ["providers", "login", apiBase(context2)], { env });
1377
+ if (done.code !== 0) throw failed(name2, done.stderr);
1378
+ if (await readText(file3) === before) throw failed(name2, `${done.stdout}
1379
+ ${done.stderr}`);
1380
+ }
1154
1381
  var signInEnv = (context2, app, facts) => ({
1155
1382
  ADAPTIQ_CONNECT: facts.deviceCode,
1156
1383
  ADAPTIQ_APP: app,
@@ -1161,12 +1388,30 @@ var fromSignIn = (context2, entries) => {
1161
1388
  if (entry2 === void 0) return {};
1162
1389
  return { base: apiBase(context2), ...typeof entry2.token === "string" ? { key: entry2.token } : {} };
1163
1390
  };
1164
- var failed = (name2, stderr) => new Problem("app_sign_in_failed", `${name2} could not sign in: ${stderr.trim().split("\n").pop() ?? "it gave no reason."}`);
1391
+ async function cleanSignIns(context2, binary, folder2) {
1392
+ const removed = [];
1393
+ const grant = await forgetGrant(context2, join11(dataDir(context2), folder2, "adaptiqai"));
1394
+ if (grant !== void 0) removed.push(grant);
1395
+ const database = join11(dataDir(context2), folder2, `${folder2}.db`);
1396
+ const unreachable = binary === void 0 && (await fileHoldsKey(database) || await fileHoldsKey(`${database}-wal`)) ? [`${database} still holds an AdaptiqAI key, and ${folder2 === "kilo" ? "Kilo" : "OpenCode"} is not here to log it out. Install it again, then run this again.`] : [];
1397
+ const auth = join11(dataDir(context2), folder2, "auth.json");
1398
+ const ours = async () => Object.keys(await readJson(auth).catch(() => ({}))).filter((url) => url === "adaptiqai" || url === apiBase(context2));
1399
+ const had = await ours();
1400
+ if (had.length === 0) return { removed, left: unreachable };
1401
+ if (binary === void 0) await updateJson(auth, (value) => {
1402
+ for (const url of had) delete value[url];
1403
+ }, context2.backups, { secret: true });
1404
+ else for (const url of had) await context2.run(binary, ["auth", "logout", url], { timeoutMs: 3e4 }).catch(() => void 0);
1405
+ const left = await ours();
1406
+ if (left.length < had.length) removed.push(auth);
1407
+ return { removed, left: [...left.length === 0 ? [] : [`${auth} still holds an AdaptiqAI key. Run: ${folder2} auth logout ${left[0]}`], ...unreachable] };
1408
+ }
1409
+ var failed = (name2, said) => new Problem("app_sign_in_failed", `${name2} could not sign in: ${said.split("\n").map((line) => line.trim()).filter((line) => line !== "").pop() ?? "it gave no reason."}`);
1165
1410
  var isVersion2 = (said) => /(?:^|[\sv])(?:[2-9]|[1-9]\d+)\./u.test(said.trim());
1166
1411
  var carriedCli = (context2) => {
1167
1412
  const app = macApp(context2, "OpenCode");
1168
1413
  const cli = app === void 0 ? void 0 : join11(app, "Contents", "Resources", "opencode-cli");
1169
- return cli !== void 0 && existsSync5(cli) ? cli : void 0;
1414
+ return cli !== void 0 && existsSync6(cli) ? cli : void 0;
1170
1415
  };
1171
1416
  async function opencodeCli(context2) {
1172
1417
  const carried = carriedCli(context2);
@@ -1175,10 +1420,39 @@ async function opencodeCli(context2) {
1175
1420
  const said = await context2.run(path, ["--version"], { timeoutMs: 15e3 });
1176
1421
  return { path, two: isVersion2(said.stdout) };
1177
1422
  }
1423
+ async function v2Credentials(context2, path) {
1424
+ const listed = await context2.run(path, ["auth", "list", "--format=json", "--standalone"], { timeoutMs: 3e4 }).catch(() => void 0);
1425
+ const found = /* @__PURE__ */ new Map();
1426
+ try {
1427
+ for (const entry2 of JSON.parse(listed?.stdout ?? "")) {
1428
+ const { id, connections } = record(entry2);
1429
+ const ids = (Array.isArray(connections) ? connections : []).map(record).filter((row) => row.type === "credential" && typeof row.id === "string").map((row) => row.id);
1430
+ if (typeof id === "string" && ids.length > 0) found.set(id, ids);
1431
+ }
1432
+ } catch {
1433
+ }
1434
+ return found;
1435
+ }
1436
+ async function v2Logout(context2, path, integrations) {
1437
+ const had = await v2Credentials(context2, path);
1438
+ const ours = integrations.filter((integration) => had.has(integration));
1439
+ if (ours.length === 0) return { removed: [], left: [] };
1440
+ for (const integration of ours) {
1441
+ for (const id of had.get(integration) ?? []) await context2.run(path, ["auth", "logout", integration, id, "--standalone"], { timeoutMs: 3e4 });
1442
+ }
1443
+ await context2.run(path, ["service", "restart"], { timeoutMs: 3e4 }).catch(() => void 0);
1444
+ const still = await v2Credentials(context2, path);
1445
+ return {
1446
+ removed: ours.filter((integration) => !still.has(integration)).map((integration) => `OpenCode: ${integration}`),
1447
+ left: ours.filter((integration) => still.has(integration)).map((integration) => `OpenCode still holds an AdaptiqAI key. Run: opencode auth logout ${integration} ${still.get(integration)?.[0] ?? ""}`)
1448
+ };
1449
+ }
1178
1450
  var opencode = {
1179
1451
  id: "opencode",
1180
1452
  name: "OpenCode",
1181
1453
  install: "curl -fsSL https://opencode.ai/v2/install | bash",
1454
+ desktop: "https://opencode.ai/desktop",
1455
+ collectsKey: true,
1182
1456
  does: "Signed OpenCode in to AdaptiqAI, which turns off its automatic compaction, for every model. It reads its models from AdaptiqAI when it starts. Your other providers stay.",
1183
1457
  signsInAs: async () => "OpenCode",
1184
1458
  found: async (context2) => carriedCli(context2) !== void 0 || command(context2, "opencode", [join11(context2.home, ".opencode", "bin", "opencode")]) !== void 0,
@@ -1190,6 +1464,8 @@ var opencode = {
1190
1464
  async current(context2) {
1191
1465
  const { path, two } = await opencodeCli(context2);
1192
1466
  if (!two) return fromSignIn(context2, await signIns(context2, "opencode"));
1467
+ const signedIn = await v2Credentials(context2, path);
1468
+ if (signedIn.size > 0) return signedIn.has(apiBase(context2)) ? { base: apiBase(context2) } : {};
1193
1469
  const listed = await context2.run(path, ["auth", "list"], { timeoutMs: 2e4 });
1194
1470
  return listed.stdout.includes(new URL(context2.origin).host) ? { base: apiBase(context2) } : {};
1195
1471
  },
@@ -1198,25 +1474,35 @@ var opencode = {
1198
1474
  const { path, two } = await opencodeCli(context2);
1199
1475
  const env = signInEnv(context2, "OpenCode", facts);
1200
1476
  if (two) {
1201
- const done2 = await context2.run(path, ["auth", "login", apiBase(context2), "--standalone", "--method", "login"], { env });
1202
- if (done2.code !== 0) throw failed("OpenCode", done2.stderr);
1477
+ const done = await context2.run(path, ["auth", "login", apiBase(context2), "--standalone", "--method", "login"], { env });
1478
+ if (done.code !== 0) throw failed("OpenCode", done.stderr);
1203
1479
  await context2.run(path, ["service", "restart"], { timeoutMs: 3e4 }).catch(() => void 0);
1204
1480
  return [];
1205
1481
  }
1206
- const done = await context2.run(path, ["providers", "login", apiBase(context2)], { env });
1207
- if (done.code !== 0) throw failed("OpenCode", done.stderr);
1482
+ await providersLogin(context2, path, "opencode", "OpenCode", env);
1208
1483
  await oneSignIn(context2, path, "opencode");
1209
1484
  return await running(context2, "OpenCode") ? ["OpenCode is open. Quit it and open it again to see AdaptiqAI."] : [];
1210
1485
  },
1211
1486
  async remove(context2) {
1212
- const { path } = await opencodeCli(context2);
1213
- await context2.run(path, ["auth", "logout", apiBase(context2)], { timeoutMs: 3e4 });
1487
+ const { path, two } = await opencodeCli(context2);
1488
+ if (two) await v2Logout(context2, path, [apiBase(context2)]);
1489
+ else await context2.run(path, ["auth", "logout", apiBase(context2)], { timeoutMs: 3e4 });
1490
+ },
1491
+ async clean(context2) {
1492
+ if (!await opencode.found(context2)) return await cleanSignIns(context2, void 0, "opencode");
1493
+ const { path, two } = await opencodeCli(context2);
1494
+ if (!two) return await cleanSignIns(context2, path, "opencode");
1495
+ const grant = await forgetGrant(context2, join11(dataDir(context2), "opencode", "adaptiqai"));
1496
+ const done = await v2Logout(context2, path, [apiBase(context2), "adaptiqai"]);
1497
+ return { removed: [...grant === void 0 ? [] : [grant], ...done.removed], left: done.left };
1214
1498
  },
1215
1499
  async start(context2, model2) {
1216
1500
  const { two } = await opencodeCli(context2);
1217
1501
  if (carriedCli(context2) !== void 0) return "open -b ai.opencode.desktop";
1218
1502
  return two ? "opencode" : `opencode -m adaptiq/${model2}`;
1219
1503
  },
1504
+ // The app opens where it was left, and takes no first message; the terminal UI does.
1505
+ greet: async (context2, model2, hello2) => carriedCli(context2) !== void 0 || context2.platform === "win32" ? void 0 : `${await opencode.start(context2, model2)} --prompt ${shellWord(hello2)}`,
1220
1506
  async ask(context2, model2) {
1221
1507
  const { path } = await opencodeCli(context2);
1222
1508
  return { command: path, args: ["run", "-m", `adaptiq/${model2}`, QUESTION] };
@@ -1241,7 +1527,7 @@ function editorKilo(context2) {
1241
1527
  const found = entries.filter((entry2) => entry2.startsWith("kilocode.kilo-code-") && gone[entry2] === void 0).sort(newestFirst)[0];
1242
1528
  if (found === void 0) continue;
1243
1529
  const path = join11(root, found, "bin", context2.platform === "win32" ? "kilo.exe" : "kilo");
1244
- if (!existsSync5(path)) continue;
1530
+ if (!existsSync6(path)) continue;
1245
1531
  if (context2.platform === "darwin" && macApp(context2, editor) === void 0) continue;
1246
1532
  return { path, editor };
1247
1533
  }
@@ -1253,6 +1539,7 @@ var kilo = {
1253
1539
  aliases: ["kilo-code", "kilocode"],
1254
1540
  name: "Kilo Code",
1255
1541
  install: "npm install -g @kilocode/cli",
1542
+ collectsKey: true,
1256
1543
  does: "Signed Kilo Code in to AdaptiqAI, which turns off its automatic compaction, for every model. It reads its models from AdaptiqAI when it starts. Your other providers stay.",
1257
1544
  signsInAs: async () => "Kilo Code",
1258
1545
  found: async (context2) => editorKilo(context2) !== void 0 || command(context2, "kilo") !== void 0,
@@ -1261,23 +1548,92 @@ var kilo = {
1261
1548
  async apply(context2, facts) {
1262
1549
  if (facts.deviceCode === void 0) return [];
1263
1550
  const path = kiloCli(context2);
1264
- const done = await context2.run(path, ["providers", "login", apiBase(context2)], { env: signInEnv(context2, "Kilo Code", facts) });
1265
- if (done.code !== 0) throw failed("Kilo Code", done.stderr);
1551
+ await providersLogin(context2, path, "kilo", "Kilo Code", signInEnv(context2, "Kilo Code", facts));
1266
1552
  await oneSignIn(context2, path, "kilo");
1267
- const editor = editorKilo(context2)?.editor;
1268
- return editor === void 0 ? [] : [`In ${editor}, reload the window to see AdaptiqAI in Kilo Code, then start a new task and choose AdaptiqAI in Kilo's model menu.`];
1553
+ const found = editorKilo(context2);
1554
+ if (found === void 0) return [];
1555
+ const [, , scheme] = EDITORS.find(([, editor]) => editor === found.editor);
1556
+ const mac = context2.platform === "darwin";
1557
+ const open3 = mac ? (await context2.run("ps", ["-axo", "comm="], { timeoutMs: 5e3 }).catch(() => void 0))?.stdout.includes(`/${found.editor}.app/Contents/MacOS/`) === true : context2.platform === "win32" || (await context2.run("pgrep", ["-x", scheme.replace(/^vs/u, "")], { timeoutMs: 5e3 }).catch(() => void 0))?.code === 0;
1558
+ if (context2.person && context2.platform !== "win32") await context2.run(mac ? "open" : "xdg-open", [`${scheme}://kilocode.kilo-code/kilocode/model?agent=code`], { timeoutMs: 15e3 }).catch(() => void 0);
1559
+ return [
1560
+ ...open3 ? [`Reload the ${found.editor} window to see AdaptiqAI in Kilo Code: ${mac ? "Cmd" : "Ctrl"}+Shift+P, then Developer: Reload Window.`] : [],
1561
+ `Start a new task, and choose AdaptiqAI \u203A ${facts.models[0].id} in Kilo's model menu.`
1562
+ ];
1269
1563
  },
1270
1564
  async remove(context2) {
1271
1565
  await context2.run(kiloCli(context2), ["auth", "logout", apiBase(context2)], { timeoutMs: 3e4 });
1272
1566
  },
1567
+ clean: async (context2) => await cleanSignIns(context2, await kilo.found(context2) ? kiloCli(context2) : void 0, "kilo"),
1273
1568
  start: async (context2, model2) => editorKilo(context2) === void 0 ? `kilo -m adaptiq/${model2}` : `open Kilo Code in ${editorKilo(context2)?.editor}`,
1569
+ startsByCommand: (context2) => editorKilo(context2) === void 0,
1570
+ greet: async (context2, model2, hello2) => editorKilo(context2) !== void 0 || context2.platform === "win32" ? void 0 : `kilo -m adaptiq/${model2} --prompt ${shellWord(hello2)}`,
1274
1571
  ask: async (context2, model2) => ({ command: kiloCli(context2), args: ["run", "-m", `adaptiq/${model2}`, QUESTION] })
1275
1572
  };
1276
1573
 
1277
1574
  // src/apps/openclaw.ts
1575
+ import { readdir as readdir3, readFile as readFile2 } from "node:fs/promises";
1278
1576
  import { join as join12 } from "node:path";
1279
1577
  var PLUGIN3 = "@adaptiqai/openclaw";
1578
+ var PLUGIN_FLAGS = ["--force", "--accept-capabilities"];
1280
1579
  var oc = (context2) => command(context2, "openclaw", [join12(context2.home, ".openclaw", "bin", "openclaw")]) ?? "openclaw";
1580
+ var state = (context2) => context2.env.OPENCLAW_STATE_DIR ?? join12(context2.home, ".openclaw");
1581
+ var envFile2 = (context2) => join12(state(context2), ".env");
1582
+ var OURS = /^\s*(export\s+)?ADAPTIQAI_(API_KEY|API_BASE|CONSOLE_URL)\s*=/u;
1583
+ async function writeEnv(context2, key) {
1584
+ const before = await readText(envFile2(context2));
1585
+ if (before === void 0 && key === void 0) return false;
1586
+ const kept = (before ?? "").replace(/\n$/u, "").split("\n").filter((line) => !OURS.test(line));
1587
+ if (kept.length === 1 && kept[0] === "") kept.pop();
1588
+ const added = key === void 0 ? [] : [`ADAPTIQAI_API_KEY=${key}`, ...Object.entries(mcpEnv(context2) ?? {}).map(([name2, value]) => `${name2}=${value}`)];
1589
+ return await writeWhole(envFile2(context2), [...kept, ...added, ""].join("\n"), null, { secret: true });
1590
+ }
1591
+ async function logout(context2, profile2) {
1592
+ if ((await context2.run(oc(context2), ["models", "auth", "logout", profile2, "--yes"], { timeoutMs: 3e4 })).code === 0) return;
1593
+ await context2.run(oc(context2), ["gateway", "call", "models.authLogout", "--params", JSON.stringify({ provider: "adaptiq", profileIds: [profile2] })], { timeoutMs: 3e4 });
1594
+ }
1595
+ async function databases(context2) {
1596
+ const agents = await readdir3(join12(state(context2), "agents")).catch(() => []);
1597
+ const withLogs = (path) => [path, `${path}-wal`];
1598
+ return [
1599
+ { files: withLogs(join12(state(context2), "state", "openclaw.sqlite")), compact: ["doctor", "--state-sqlite", "compact", "--json"] },
1600
+ {
1601
+ files: agents.flatMap((agent) => withLogs(join12(state(context2), "agents", agent, "agent", "openclaw-agent.sqlite"))),
1602
+ compact: ["doctor", "--session-sqlite", "compact", "--session-sqlite-all-agents", "--json"]
1603
+ }
1604
+ ];
1605
+ }
1606
+ var holding = async (files) => (await Promise.all(files.map(async (file3) => await fileHoldsKey(file3) ? file3 : ""))).filter((file3) => file3 !== "");
1607
+ var envKey = async (context2) => {
1608
+ const line = (await readText(envFile2(context2)) ?? "").split("\n").filter((entry2) => /^\s*(export\s+)?ADAPTIQAI_API_KEY\s*=/u.test(entry2)).pop();
1609
+ const key = line?.replace(/^[^=]*=/u, "").trim().replace(/^['"]|['"]$/gu, "");
1610
+ return key === void 0 || key === "" ? void 0 : key;
1611
+ };
1612
+ var covers = (stored, wanted) => {
1613
+ if (Array.isArray(wanted)) return Array.isArray(stored) && stored.length === wanted.length && wanted.every((entry2, at) => covers(stored[at], entry2));
1614
+ if (wanted !== null && typeof wanted === "object") {
1615
+ return stored !== null && typeof stored === "object" && Object.entries(wanted).every(([name2, value]) => covers(stored[name2], value));
1616
+ }
1617
+ return stored === wanted;
1618
+ };
1619
+ async function directOnly(dir2) {
1620
+ const entries = await readdir3(dir2, { withFileTypes: true, recursive: true }).catch(() => []);
1621
+ for (const entry2 of entries) {
1622
+ if (!entry2.isFile()) continue;
1623
+ const text = await readFile2(join12(entry2.parentPath, entry2.name), "utf8").catch(() => "");
1624
+ if (text.includes("direct-only")) return true;
1625
+ }
1626
+ return false;
1627
+ }
1628
+ var getJson = async (context2, key) => {
1629
+ const said = await context2.run(oc(context2), ["config", "get", key, "--json"], { timeoutMs: 3e4 });
1630
+ if (said.code !== 0) return void 0;
1631
+ try {
1632
+ return JSON.parse(said.stdout.slice(said.stdout.indexOf("{"), said.stdout.lastIndexOf("}") + 1));
1633
+ } catch {
1634
+ return void 0;
1635
+ }
1636
+ };
1281
1637
  var get = async (context2, key) => {
1282
1638
  const said = await context2.run(oc(context2), ["config", "get", key], { timeoutMs: 3e4 });
1283
1639
  const value = said.stdout.trim().split("\n").pop()?.trim();
@@ -1289,9 +1645,10 @@ var entry = (model2) => ({
1289
1645
  ...model2.input.includes("image") ? { input: ["text", "image"] } : {},
1290
1646
  contextWindow: CONVERSATION_TOKENS,
1291
1647
  ...model2.output > 0 ? { maxTokens: model2.output } : {},
1292
- ...model2.reasoning === null ? {} : {
1293
- reasoning: true,
1294
- compat: {
1648
+ ...model2.reasoning === null ? {} : { reasoning: true },
1649
+ compat: {
1650
+ supportsPromptCacheKey: true,
1651
+ ...model2.reasoning === null ? {} : {
1295
1652
  supportsReasoningEffort: true,
1296
1653
  supportedReasoningEfforts: [...model2.reasoning.offWire === null ? [] : [model2.reasoning.offWire], ...model2.reasoning.levels]
1297
1654
  }
@@ -1302,21 +1659,37 @@ var openclaw = {
1302
1659
  id: "openclaw",
1303
1660
  name: "OpenClaw",
1304
1661
  install: "npm install -g openclaw",
1305
- does: "Added AdaptiqAI to OpenClaw, made it the default model, and turned off its automatic compaction, for every model. Your other providers stay.",
1662
+ // 2026.11.3 refuses to install on an older one (>=24.16.0 <25 || >=26.1.0).
1663
+ installNeedsNode: "24.16.0",
1664
+ does: "Added AdaptiqAI and its plugin to OpenClaw, made it the default model, and turned off its automatic compaction and, where the plugin needs it, tool search, for every model. Your other providers stay.",
1306
1665
  signsInAs: async () => "OpenClaw",
1307
1666
  found: async (context2) => command(context2, "openclaw", [join12(context2.home, ".openclaw", "bin", "openclaw")]) !== void 0,
1308
1667
  async prepare(context2) {
1309
1668
  const done = await context2.run(oc(context2), ["config", "file"], { timeoutMs: 3e4 });
1310
1669
  if (done.code !== 0) throw new Problem("app_refused", `OpenClaw would not start: ${done.stderr.trim().split("\n").pop() ?? ""}`);
1670
+ const inspected = await context2.run(oc(context2), ["plugins", "inspect", "adaptiq"], { timeoutMs: 6e4 });
1671
+ const had = inspected.code === 0 ? /^Version:\s*(\S+)/mu.exec(inspected.stdout)?.[1] ?? "?" : "";
1672
+ const npm = command(context2, "npm");
1673
+ const seen = npm === void 0 ? void 0 : await context2.run(npm, ["view", PLUGIN3, "version"], { timeoutMs: 3e4 }).catch(() => void 0);
1674
+ const latest = seen?.code === 0 ? seen.stdout.trim().split("\n").pop()?.trim() ?? "" : "";
1675
+ if (had !== "" && (!/^\d+\.\d+\.\d+/u.test(latest) || latest === had)) return;
1676
+ const added = await context2.run(oc(context2), ["plugins", "install", PLUGIN3, ...PLUGIN_FLAGS], { timeoutMs: 18e4 });
1677
+ if (added.code !== 0 && had === "") {
1678
+ throw new Problem("tools_not_installed", `AdaptiqAI's plugin for OpenClaw could not be installed, so OpenClaw was left as it was. Check the connection, then run this again. (${added.stderr.trim().split("\n").pop() ?? ""})`);
1679
+ }
1311
1680
  },
1312
1681
  async current(context2) {
1313
1682
  const base = await get(context2, "models.providers.adaptiq.baseUrl");
1314
1683
  if (base === void 0) return {};
1315
1684
  const primary = await get(context2, "agents.defaults.model.primary");
1316
- return { base, ...primary?.startsWith("adaptiq/") === true ? { model: primary.slice("adaptiq/".length) } : {} };
1685
+ const key = await envKey(context2);
1686
+ return {
1687
+ base,
1688
+ ...primary?.startsWith("adaptiq/") === true ? { model: primary.slice("adaptiq/".length) } : {},
1689
+ ...key === void 0 ? {} : { key }
1690
+ };
1317
1691
  },
1318
1692
  async apply(context2, facts) {
1319
- const notes = [];
1320
1693
  const was = await get(context2, "agents.defaults.model.primary");
1321
1694
  const compaction = await get(context2, "agents.defaults.compaction.enabled");
1322
1695
  await remember(context2, openclaw, { primary: was?.startsWith("adaptiq/") === true ? null : was ?? null, compaction: compaction ?? null });
@@ -1324,27 +1697,38 @@ var openclaw = {
1324
1697
  models: { providers: { adaptiq: { baseUrl: apiBase(context2), api: "openai-completions", models: facts.models.map(entry) } } },
1325
1698
  agents: { defaults: { model: { primary: `adaptiq/${facts.models[0].id}` } } }
1326
1699
  };
1327
- const patched = await context2.run(oc(context2), ["config", "patch", "--stdin", "--replace-path", "models.providers.adaptiq.models"], { input: JSON.stringify(patch), timeoutMs: 6e4 });
1328
- if (patched.code !== 0) throw new Problem("app_refused", `OpenClaw refused the setup: ${patched.stderr.trim().split("\n").pop() ?? ""}`);
1329
- if (facts.key !== void 0) {
1700
+ if (was !== patch.agents.defaults.model.primary || !covers(await getJson(context2, "models.providers.adaptiq"), patch.models.providers.adaptiq)) {
1701
+ const patched = await context2.run(oc(context2), ["config", "patch", "--stdin", "--replace-path", "models.providers.adaptiq.models"], { input: JSON.stringify(patch), timeoutMs: 6e4 });
1702
+ if (patched.code !== 0) throw new Problem("app_refused", `OpenClaw refused the setup: ${patched.stderr.trim().split("\n").pop() ?? ""}`);
1703
+ }
1704
+ if (facts.key !== void 0 && facts.key !== await envKey(context2)) {
1330
1705
  const pasted = await context2.run(oc(context2), ["models", "auth", "paste-api-key", "--provider", "adaptiq"], { input: facts.key, timeoutMs: 6e4 });
1331
1706
  if (pasted.code !== 0) throw new Problem("app_refused", `OpenClaw refused the key: ${pasted.stderr.trim().split("\n").pop() ?? ""}`);
1332
1707
  }
1333
1708
  await context2.run(oc(context2), ["config", "set", "agents.defaults.compaction.enabled", "false"], { timeoutMs: 3e4 });
1334
- const listed = await context2.run(oc(context2), ["plugins", "list"], { timeoutMs: 6e4 });
1335
- if (!listed.stdout.includes(PLUGIN3) && !/\badaptiq\b/u.test(listed.stdout)) {
1336
- const added = await context2.run(oc(context2), ["plugins", "install", PLUGIN3], { timeoutMs: 18e4 });
1337
- if (added.code !== 0) notes.push(`AdaptiqAI's plugin for OpenClaw could not be installed, so conversations have no memory of their own yet. Run: openclaw plugins install ${PLUGIN3}`);
1338
- else if (await gatewayUp(context2)) {
1339
- if (await context2.confirm("OpenClaw's Gateway loads AdaptiqAI's plugin when it starts. Restart it now?")) await context2.run(oc(context2), ["gateway", "restart"], { timeoutMs: 6e4 });
1340
- else notes.push("Restart OpenClaw's Gateway to load AdaptiqAI's plugin: openclaw gateway restart");
1709
+ const inspected = await context2.run(oc(context2), ["plugins", "inspect", "adaptiq"], { timeoutMs: 6e4 });
1710
+ const where = /^Install path:\s*(.+)$/mu.exec(inspected.stdout)?.[1]?.trim().replace(/^~\//u, `${context2.home}/`);
1711
+ if (where !== void 0 && await directOnly(join12(where, "lib"))) {
1712
+ if (await get(context2, "tools.toolSearch") !== void 0) await context2.run(oc(context2), ["config", "unset", "tools.toolSearch"], { timeoutMs: 3e4 });
1713
+ } else {
1714
+ await context2.run(oc(context2), ["config", "set", "tools.toolSearch", "false"], { timeoutMs: 3e4 });
1715
+ }
1716
+ if (facts.key !== void 0) await writeEnv(context2, facts.key);
1717
+ await logout(context2, "adaptiq:device");
1718
+ await forgetGrant(context2, join12(state(context2), "adaptiqai"));
1719
+ if (await gatewayUp(context2)) {
1720
+ if (!await context2.confirm("Restart OpenClaw's Gateway now, so it uses AdaptiqAI? Answers in progress stop.")) return ["OpenClaw uses AdaptiqAI once its Gateway restarts: openclaw gateway restart"];
1721
+ const restarted = await context2.run(oc(context2), ["gateway", "restart"], { timeoutMs: 6e4 });
1722
+ if (restarted.code !== 0) {
1723
+ const said = `${restarted.stderr}
1724
+ ${restarted.stdout}`.trim().split("\n").pop() ?? "";
1725
+ return [`OpenClaw's Gateway did not restart${said === "" ? "" : ` (${said})`}. It uses AdaptiqAI once it restarts: openclaw gateway restart`];
1341
1726
  }
1342
1727
  }
1343
- return notes;
1728
+ return [];
1344
1729
  },
1345
1730
  async remove(context2) {
1346
1731
  const had = await recall(context2, openclaw);
1347
- await context2.run(oc(context2), ["models", "auth", "logout", "adaptiq:manual", "--yes"], { timeoutMs: 3e4 });
1348
1732
  await updateJson(join12(context2.env.OPENCLAW_STATE_DIR ?? join12(context2.home, ".openclaw"), "openclaw.json"), (value) => {
1349
1733
  delete record(record(value.models).providers).adaptiq;
1350
1734
  const defaults = record(record(value.agents).defaults);
@@ -1356,9 +1740,34 @@ var openclaw = {
1356
1740
  if (had.compaction === null) delete record(defaults.compaction).enabled;
1357
1741
  else if (had.compaction === "true") record(defaults.compaction).enabled = true;
1358
1742
  }, context2.backups);
1743
+ await writeEnv(context2);
1359
1744
  await forget(context2, openclaw);
1360
1745
  },
1361
- start: async () => "openclaw dashboard",
1746
+ async clean(context2) {
1747
+ const removed = [];
1748
+ if (await writeEnv(context2)) removed.push(envFile2(context2));
1749
+ const grant = await forgetGrant(context2, join12(state(context2), "adaptiqai"));
1750
+ if (grant !== void 0) removed.push(grant);
1751
+ const here = await openclaw.found(context2);
1752
+ if (here) for (const profile2 of ["adaptiq:manual", "adaptiq:device"]) await logout(context2, profile2);
1753
+ const all = await databases(context2);
1754
+ const every = all.flatMap((database) => database.files);
1755
+ const held = await holding(every);
1756
+ let up = here && held.length > 0 && await gatewayUp(context2);
1757
+ if (up && await context2.confirm("OpenClaw's database keeps the old key until its Gateway closes it. Restart its Gateway now? Answers in progress stop.") && (await context2.run(oc(context2), ["gateway", "restart"], { timeoutMs: 6e4 })).code === 0) up = await gatewayUp(context2);
1758
+ for (const { files, compact } of all) {
1759
+ if (here && !up && (await holding(files)).length > 0) await context2.run(oc(context2), compact, { timeoutMs: 6e5 });
1760
+ }
1761
+ const still = await holding(every);
1762
+ removed.push(...held.filter((file3) => !still.includes(file3)));
1763
+ const left = still.length === 0 ? [] : [here ? `OpenClaw's own database still holds the AdaptiqAI key (${still[0]}). Quit OpenClaw and stop its Gateway (openclaw gateway stop), then run this again: npx -y adaptiqai disconnect openclaw` : `OpenClaw's own database still holds the AdaptiqAI key (${still[0]}), and OpenClaw is not here to clear it. If you no longer use OpenClaw, delete its folder, ${state(context2)}.`];
1764
+ return { removed, left };
1765
+ },
1766
+ // The Mac app shows the dashboard in its own window, on the Gateway it runs itself; without it, the browser does.
1767
+ start: async (context2) => {
1768
+ const dashboard = `${/\s/u.test(oc(context2)) ? `'${oc(context2)}'` : oc(context2)} dashboard`;
1769
+ return context2.platform === "darwin" ? `open -b ai.openclaw.mac openclaw://dashboard 2>/dev/null || ${dashboard}` : dashboard;
1770
+ },
1362
1771
  async ask(context2) {
1363
1772
  const local = await gatewayUp(context2) ? [] : ["--local"];
1364
1773
  return { command: oc(context2), args: ["agent", ...local, "--agent", "main", "--json", "--timeout", "120", "--message", QUESTION] };
@@ -1367,17 +1776,17 @@ var openclaw = {
1367
1776
 
1368
1777
  // src/apps/pi.ts
1369
1778
  import { join as join13 } from "node:path";
1370
- var PACKAGE = "npm:@adaptiqai/pi";
1779
+ var PACKAGE2 = "npm:@adaptiqai/pi";
1371
1780
  var PROVIDER2 = "adaptiqai";
1372
1781
  var CONSOLE_PROVIDER = "adaptiq";
1373
- var dir2 = (context2) => context2.env.PI_CODING_AGENT_DIR ?? join13(context2.home, ".pi", "agent");
1374
- var file3 = (context2, name2) => join13(dir2(context2), name2);
1782
+ var dir = (context2) => context2.env.PI_CODING_AGENT_DIR ?? join13(context2.home, ".pi", "agent");
1783
+ var file2 = (context2, name2) => join13(dir(context2), name2);
1375
1784
  var pi = (context2) => command(context2, "pi") ?? "pi";
1376
1785
  async function hasPackage(context2) {
1377
- const { packages } = await readJson(file3(context2, "settings.json"));
1786
+ const { packages } = await readJson(file2(context2, "settings.json"));
1378
1787
  return Array.isArray(packages) && packages.some((entry2) => {
1379
1788
  const source = typeof entry2 === "string" ? entry2 : record(entry2).source;
1380
- return typeof source === "string" && source.startsWith(PACKAGE);
1789
+ return typeof source === "string" && source.startsWith(PACKAGE2);
1381
1790
  });
1382
1791
  }
1383
1792
  var piApp = {
@@ -1388,53 +1797,73 @@ var piApp = {
1388
1797
  signsInAs: async () => "Pi",
1389
1798
  found: async (context2) => command(context2, "pi") !== void 0,
1390
1799
  async prepare(context2) {
1391
- await Promise.all(["auth.json", "models.json", "settings.json"].map((name2) => readJson(file3(context2, name2))));
1800
+ await Promise.all(["auth.json", "models.json", "settings.json"].map((name2) => readJson(file2(context2, name2))));
1801
+ const done = await context2.run(pi(context2), ["install", PACKAGE2], { timeoutMs: 18e4 });
1802
+ if (done.code !== 0 && !await hasPackage(context2)) {
1803
+ throw new Problem("plugin_failed", `Pi could not install AdaptiqAI's package, so it was left as it was. Check the connection, then run this again: ${done.stderr.trim().split("\n").pop() ?? ""}`);
1804
+ }
1392
1805
  },
1393
1806
  async current(context2) {
1394
- const { key } = record((await readJson(file3(context2, "auth.json")))[PROVIDER2]);
1807
+ const { key } = record((await readJson(file2(context2, "auth.json")))[PROVIDER2]);
1395
1808
  return typeof key === "string" ? { base: apiBase(context2), key } : {};
1396
1809
  },
1397
1810
  async apply(context2, facts) {
1398
- if (!await hasPackage(context2)) {
1399
- const done = await context2.run(pi(context2), ["install", PACKAGE], { timeoutMs: 18e4 });
1400
- if (done.code !== 0) throw new Problem("plugin_failed", `Pi could not install AdaptiqAI's package, so it was left as it was: ${done.stderr.trim().split("\n").pop() ?? ""}`);
1401
- }
1402
- const settings = await readJson(file3(context2, "settings.json"));
1811
+ await forgetGrant(context2, file2(context2, "adaptiqai"));
1812
+ const settings = await readJson(file2(context2, "settings.json"));
1403
1813
  await remember(context2, piApp, { compaction: settings.compaction ?? null });
1404
- await updateJson(file3(context2, "auth.json"), (value) => {
1814
+ await updateJson(file2(context2, "auth.json"), (value) => {
1405
1815
  value[PROVIDER2] = { type: "api_key", key: facts.key };
1406
1816
  if (record(value[CONSOLE_PROVIDER]).type === "api_key") delete value[CONSOLE_PROVIDER];
1407
1817
  }, context2.backups, { secret: true });
1408
- await updateJson(file3(context2, "models.json"), (value) => {
1818
+ await updateJson(file2(context2, "models.json"), (value) => {
1409
1819
  const all = record(value.providers);
1410
1820
  if (all[CONSOLE_PROVIDER] === void 0) return;
1411
1821
  delete all[CONSOLE_PROVIDER];
1412
1822
  value.providers = all;
1413
1823
  }, context2.backups, { secret: true });
1414
- await updateJson(file3(context2, "settings.json"), (value) => {
1824
+ await updateJson(file2(context2, "settings.json"), (value) => {
1415
1825
  value.compaction = { ...record(value.compaction), enabled: false };
1826
+ if (value.defaultProvider === void 0 || value.defaultProvider === CONSOLE_PROVIDER) {
1827
+ value.defaultProvider = PROVIDER2;
1828
+ value.defaultModel = facts.models[0].id;
1829
+ }
1416
1830
  }, context2.backups, { secret: true });
1417
1831
  return [];
1418
1832
  },
1419
1833
  async remove(context2) {
1420
1834
  const had = await recall(context2, piApp);
1421
- if (await hasPackage(context2)) await context2.run(pi(context2), ["remove", PACKAGE], { timeoutMs: 6e4 });
1422
- await updateJson(file3(context2, "auth.json"), (value) => {
1835
+ if (await hasPackage(context2)) await context2.run(pi(context2), ["remove", PACKAGE2], { timeoutMs: 6e4 });
1836
+ await updateJson(file2(context2, "auth.json"), (value) => {
1423
1837
  delete value[PROVIDER2];
1424
1838
  }, context2.backups, { secret: true });
1425
- if ("compaction" in had) {
1426
- await updateJson(file3(context2, "settings.json"), (value) => {
1839
+ await updateJson(file2(context2, "settings.json"), (value) => {
1840
+ if ("compaction" in had) {
1427
1841
  if (had.compaction === null) delete value.compaction;
1428
1842
  else value.compaction = had.compaction;
1429
- }, context2.backups, { secret: true });
1430
- }
1843
+ }
1844
+ if (value.defaultProvider === PROVIDER2) {
1845
+ delete value.defaultProvider;
1846
+ delete value.defaultModel;
1847
+ }
1848
+ }, context2.backups, { secret: true });
1431
1849
  await forget(context2, piApp);
1432
1850
  },
1433
- start: async (_context, model2) => `pi --provider ${PROVIDER2} --model ${model2}`
1851
+ async clean(context2) {
1852
+ const removed = [];
1853
+ const auth = file2(context2, "auth.json");
1854
+ if (await updateJson(auth, (value) => {
1855
+ delete value[PROVIDER2];
1856
+ }, context2.backups, { secret: true })) removed.push(auth);
1857
+ const grant = await forgetGrant(context2, file2(context2, "adaptiqai"));
1858
+ if (grant !== void 0) removed.push(grant);
1859
+ return { removed, left: [] };
1860
+ },
1861
+ start: async (_context, model2) => `pi --provider ${PROVIDER2} --model ${model2}`,
1862
+ greet: async (context2, model2, hello2) => context2.platform === "win32" ? void 0 : `pi --provider ${PROVIDER2} --model ${model2} ${shellWord(hello2)}`
1434
1863
  };
1435
1864
 
1436
1865
  // src/apps/index.ts
1437
- var APPS = [cline, deepseek, codex, claudeCode, opencode, kilo, piApp, hermesApp, openclaw];
1866
+ var APPS = [deepseek, codex, claudeCode, opencode, kilo, piApp, hermesApp, openclaw];
1438
1867
  function appNamed(word) {
1439
1868
  const wanted = word.trim().toLowerCase();
1440
1869
  return APPS.find((app) => app.id === wanted || app.aliases?.includes(wanted) === true);
@@ -1442,7 +1871,9 @@ function appNamed(word) {
1442
1871
 
1443
1872
  // src/commands.ts
1444
1873
  import { spawn as spawn2 } from "node:child_process";
1445
- import { dirname as dirname3 } from "node:path";
1874
+ import { existsSync as existsSync7 } from "node:fs";
1875
+ import { access as access3, constants as constants4 } from "node:fs/promises";
1876
+ import { delimiter as delimiter3, dirname as dirname4, join as join15 } from "node:path";
1446
1877
 
1447
1878
  // ../core/lib/errors.js
1448
1879
  var CODES = {
@@ -1466,6 +1897,13 @@ var CODES = {
1466
1897
  };
1467
1898
  var RETRYABLE_CODES = [CODES.empty, CODES.busy, CODES.server, CODES.timeout, CODES.transport, CODES.preparing];
1468
1899
 
1900
+ // ../core/lib/tool-failures.js
1901
+ import { AsyncLocalStorage } from "node:async_hooks";
1902
+ var PASSING = [CODES.server, CODES.timeout, CODES.transport, CODES.busy, CODES.preparing, CODES.empty, CODES.conversationUnavailable];
1903
+ var inCall = new AsyncLocalStorage();
1904
+ var UNKNOWN_TURN_MS = 2 * 6e4;
1905
+ var KEPT_MS = 10 * 6e4;
1906
+
1469
1907
  // ../core/lib/auth.js
1470
1908
  var REFRESH_MARGIN_MS = 2 * 6e4;
1471
1909
  var DEVICE_TIMEOUT_MS = 10 * 6e4;
@@ -1570,13 +2008,13 @@ async function pollDeviceCode(client2, code, extra = {}) {
1570
2008
  return { state: "failed", message: detailMessage(await readJson2(response)) ?? `Sign-in failed (HTTP ${response.status}).` };
1571
2009
  }
1572
2010
  const body = await readJson2(response);
1573
- const access2 = String(body?.access_token ?? "");
2011
+ const access4 = String(body?.access_token ?? "");
1574
2012
  const refresh = String(body?.refresh_token ?? "");
1575
- if (!access2.startsWith("ak_") || refresh !== "" && !refresh.startsWith("akr_"))
2013
+ if (!access4.startsWith("ak_") || refresh !== "" && !refresh.startsWith("akr_"))
1576
2014
  return { state: "failed", message: "AdaptiqAI returned an invalid credential." };
1577
2015
  return {
1578
2016
  state: "approved",
1579
- access: access2,
2017
+ access: access4,
1580
2018
  refresh: refresh === "" ? null : refresh,
1581
2019
  expiresIn: Math.max(60, Number(body?.expires_in ?? 3600)),
1582
2020
  workspaceLabel: typeof body?.workspace_label === "string" && body.workspace_label !== "" ? body.workspace_label : "Your workspace"
@@ -1810,6 +2248,7 @@ var FOLLOW_MS = 3 * 36e5;
1810
2248
 
1811
2249
  // ../core/lib/update.js
1812
2250
  var RELEASE_AGE_MS = 24 * 36e5;
2251
+ var OFFER_AGE_MS = 48 * 36e5;
1813
2252
 
1814
2253
  // ../core/lib/workspace.js
1815
2254
  var UNDO_WINDOW_MS = 15 * 6e4;
@@ -1820,13 +2259,51 @@ var ATTACH_BACKOFF_MAX_MS = 10 * 6e4;
1820
2259
  var ATTACH_DEADLINE_MS = 6 * 36e5;
1821
2260
  var SESSION_BACKOFF_MAX_MS = 5 * 6e4;
1822
2261
 
2262
+ // ../core/lib/commands.js
2263
+ var ACTIONS = [
2264
+ ["", "workspace, models and memory"],
2265
+ ["signin", "connect your AdaptiqAI account in the browser"],
2266
+ ["private", "stop saving new messages of this conversation"],
2267
+ ["remember", "save this conversation to memory again"],
2268
+ ["knowledge", "knowledge used in this conversation, and what else you can use"],
2269
+ ["use <name>", "use a collection or an earlier conversation here"],
2270
+ ["stop <name>", "stop using it here (nothing is deleted)"],
2271
+ ["open", "open this conversation in AdaptiqAI Knowledge"],
2272
+ ["diagnostics", "a report you can send to support (no secrets)"],
2273
+ ["signout", "forget the connection on this computer"]
2274
+ ];
2275
+ var COMMAND_HINT = ["status", ...ACTIONS.slice(1).map(([action]) => action.split(" ")[0])].join(" \xB7 ");
2276
+
2277
+ // ../core/lib/preparing.js
2278
+ var PREPARING_MOST_MS = 15 * 6e4;
2279
+
2280
+ // ../core/lib/reading.js
2281
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
2282
+
2283
+ // ../core/lib/series.js
2284
+ var MONTHS = [
2285
+ "januar februar marts april maj juni juli august september oktober november december",
2286
+ "january february march april may june july august september october november december",
2287
+ "jan feb mar apr maj jun jul aug sep okt nov dec",
2288
+ "jan feb mar apr may jun jul aug sep oct nov dec"
2289
+ ].map((words) => words.split(" "));
2290
+ var TOKEN = new RegExp(`(\\d+|(?<!\\p{L})(?:${[...new Set(MONTHS.flat())].sort((a, b) => b.length - a.length).join("|")})(?!\\p{L}))`, "iu");
2291
+
1823
2292
  // ../core/lib/reading.js
1824
2293
  var READ_PER_REPLY = 13e4;
1825
- var READING_NOTE = `A collection used here is in your memory, but a large one is read in parts: each reply reads what matches the conversation (about ${formatTokens(READ_PER_REPLY)} tokens of it), never all of it at once. To work in it: contents lists its documents; find searches its text for words and gives the exact lines as document:line; open reads one document (around a line, if given) in your next reply. Before you say something is not there, find it; for an overview, take contents first; answer from what you read, citing document:line.`;
2294
+ var READING_NOTE = `A collection used here is in your memory, but a large one is read in parts: each reply reads what matches the conversation (about ${formatTokens(READ_PER_REPLY)} tokens of it), never all of it at once. contents lists its documents; open shows a document's exact lines (around a line, if given; more: true reads on). Before you say something is not there, find it; for an overview, take contents first. Answer from what these actions return, citing document:line as a result shows it, and never name a document or a line no result showed.`;
2295
+ var READS_IDLE_MS = 10 * 6e4;
2296
+ var ORIGINAL_BYTES = 16 * 1024 * 1024;
2297
+ var KNOWN_CHARS = 32 * 1024 * 1024;
2298
+ var calls = new AsyncLocalStorage2();
1826
2299
 
1827
2300
  // ../core/lib/tools.js
1828
2301
  var SOURCE_LIMIT = 6 * 1024 * 1024;
1829
2302
 
2303
+ // ../core/lib/turns.js
2304
+ var OFFER = new RegExp("(?<!\\p{L})(?:knowledge|collection|ncm|viden|samling)|\u77E5\u8BC6|\u96C6\u5408", "iu");
2305
+ var OFFER_DOES = new RegExp("(?<!\\p{L})(?:save|make|create|add|put|keep|store|turn|remember|gem|gemme|lav|lave|opret|oprette|tilf\xF8j|tilf\xF8je|l\xE6g|l\xE6gge)(?!\\p{L})|\u4FDD\u5B58|\u521B\u5EFA|\u505A\u6210|\u653E\u8FDB|\u52A0\u5165|\u5B58", "iu");
2306
+
1830
2307
  // src/gateway.ts
1831
2308
  async function call(context2, path, options = {}) {
1832
2309
  let response;
@@ -1849,6 +2326,7 @@ async function call(context2, path, options = {}) {
1849
2326
  return { status: response.status, body, headers: response.headers };
1850
2327
  }
1851
2328
  var SAFE_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/u;
2329
+ var namedModels = (ids) => models(ids.map((id) => ({ id })));
1852
2330
  var models = (rows) => buildCatalog(rows, { kind: "unknown" }).filter((row) => SAFE_MODEL_ID.test(row.id)).map((row) => ({
1853
2331
  id: row.id,
1854
2332
  name: row.name,
@@ -1932,7 +2410,7 @@ async function testRequest(context2, key, model2) {
1932
2410
  }
1933
2411
 
1934
2412
  // src/signin.ts
1935
- import { randomBytes as randomBytes2, createHash as createHash2 } from "node:crypto";
2413
+ import { randomBytes, createHash as createHash2 } from "node:crypto";
1936
2414
  import { chmod as chmod5, mkdir as mkdir7, rm as rm4 } from "node:fs/promises";
1937
2415
  import { join as join14 } from "node:path";
1938
2416
  var pendingFile = (context2, app) => join14(cliDir(context2), `sign-in-${app.id}.json`);
@@ -1951,12 +2429,12 @@ var forgetSignIn = async (context2, app) => {
1951
2429
  await rm4(pendingFile(context2, app), { force: true });
1952
2430
  };
1953
2431
  async function installationId(context2) {
1954
- const file4 = join14(configDir(context2.env, context2.home), "installation");
1955
- const saved = (await readText(file4))?.replace(/[^a-f0-9]/gu, "").slice(0, 32);
2432
+ const file3 = join14(configDir(context2.env, context2.home), "installation");
2433
+ const saved = (await readText(file3))?.replace(/[^a-f0-9]/gu, "").slice(0, 32);
1956
2434
  if (saved?.length === 32) return saved;
1957
- const made = randomBytes2(16).toString("hex");
2435
+ const made = randomBytes(16).toString("hex");
1958
2436
  await mkdir7(configDir(context2.env, context2.home), { recursive: true, mode: 448 });
1959
- await writeWhole(file4, `${made}
2437
+ await writeWhole(file3, `${made}
1960
2438
  `, null, { secret: true });
1961
2439
  return made;
1962
2440
  }
@@ -1968,7 +2446,7 @@ async function start(context2, app) {
1968
2446
  app: await app.signsInAs(context2),
1969
2447
  device: deviceName(),
1970
2448
  // Nothing needs this again: the key an app stores lasts by itself and is never refreshed.
1971
- binding: createHash2("sha256").update(randomBytes2(32)).digest("hex"),
2449
+ binding: createHash2("sha256").update(randomBytes(32)).digest("hex"),
1972
2450
  tokenLifetime: "long"
1973
2451
  });
1974
2452
  } catch (error) {
@@ -1988,7 +2466,7 @@ function tell(context2, app, pending) {
1988
2466
  url: pending.url,
1989
2467
  code: pending.userCode,
1990
2468
  expires_in: Math.max(0, Math.round((pending.expiresAt - context2.now()) / 1e3)),
1991
- say: "Open this link, sign in or create an account, check that the page shows the same code, and press Connect. Then add a card or funds. Nothing is charged when you add a card."
2469
+ say: "Open this link, sign in or create an account, check that the page shows the same code, and press Connect. Then add funds."
1992
2470
  }, [
1993
2471
  ...elsewhere === void 0 ? [] : ["", `Signing in at ${elsewhere}, set by ADAPTIQAI_API_BASE, not at AdaptiqAI's own address.`],
1994
2472
  "",
@@ -2000,13 +2478,14 @@ function tell(context2, app, pending) {
2000
2478
  ]);
2001
2479
  }
2002
2480
  async function signIn(context2, app, options) {
2003
- let pending = await pendingSignIn(context2, app);
2481
+ const given = options.code === void 0 ? void 0 : { deviceCode: options.code, userCode: "", url: "", expiresAt: context2.now() + 6e5, interval: 2e3, app: app.id, origin: context2.origin };
2482
+ let pending = given ?? await pendingSignIn(context2, app);
2004
2483
  const resumed = pending !== void 0;
2005
2484
  if (pending === void 0) {
2006
2485
  pending = await start(context2, app);
2007
2486
  await options.open(pending.url);
2008
2487
  }
2009
- tell(context2, app, pending);
2488
+ if (given === void 0) tell(context2, app, pending);
2010
2489
  if (!resumed && options.waitMs === 0) return { state: "waiting" };
2011
2490
  const extra = {
2012
2491
  client_app: await app.signsInAs(context2),
@@ -2014,17 +2493,24 @@ async function signIn(context2, app, options) {
2014
2493
  model: options.model,
2015
2494
  installation: await installationId(context2)
2016
2495
  };
2017
- const until = context2.now() + options.waitMs;
2018
- context2.say.note("Waiting for you in the browser...");
2496
+ const until = context2.now() + (given === void 0 ? options.waitMs : Math.max(options.waitMs, 6e4));
2497
+ if (given === void 0) context2.say.note("Waiting for you in the browser...");
2019
2498
  for (; ; ) {
2020
2499
  const answer = await pollDeviceCode(client(context2), pending, extra);
2021
2500
  if (answer.state === "approved") return { state: "approved", key: answer.access, workspace: answer.workspaceLabel, deviceCode: pending.deviceCode };
2022
2501
  if (answer.state === "waiting") {
2023
- if (context2.now() + answer.delayMs > until) return { state: "waiting" };
2502
+ if (context2.now() + answer.delayMs > until) {
2503
+ if (given !== void 0) throw new Problem("sign_in_failed", "AdaptiqAI did not answer for the code from the console. Check the connection, then run the command again.");
2504
+ return { state: "waiting" };
2505
+ }
2024
2506
  await context2.sleep(answer.delayMs);
2025
2507
  continue;
2026
2508
  }
2027
- await forgetSignIn(context2, app);
2509
+ if (given !== void 0 && answer.state === "expired") {
2510
+ context2.say.note("The code from the console has expired, or was used. Signing in in the browser instead.");
2511
+ return await signIn(context2, app, { model: options.model, waitMs: options.waitMs, open: options.open });
2512
+ }
2513
+ if (given === void 0) await forgetSignIn(context2, app);
2028
2514
  if (answer.state === "denied") throw new Problem("declined", answer.message);
2029
2515
  if (answer.state === "expired" && resumed) return await signIn(context2, app, options);
2030
2516
  throw new Problem(answer.state === "expired" ? "sign_in_expired" : "sign_in_failed", answer.message);
@@ -2034,13 +2520,14 @@ async function signIn(context2, app, options) {
2034
2520
  // src/commands.ts
2035
2521
  var EXIT = { connected: 0, failed: 1, usage: 2, waitingForSignIn: 3, waitingForFunds: 4 };
2036
2522
  var FUNDS_POLL_MS = 15e3;
2523
+ var hello = (name2) => `Hi${name2 === void 0 || name2 === "" || name2 === "Your workspace" ? "" : `, I'm ${name2}`}. Briefly confirm that the connection to AdaptiqAI works, and ask what we should work on.`;
2037
2524
  async function openLink(context2, url) {
2038
2525
  const { env, platform } = context2;
2039
2526
  if (env.SSH_CONNECTION !== void 0 || env.CI !== void 0 || !safeLink(url)) return;
2040
2527
  if (platform !== "darwin" && platform !== "win32" && env.DISPLAY === void 0 && env.WAYLAND_DISPLAY === void 0) return;
2041
- const [file4, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", new URL(url).href]] : ["xdg-open", [url]];
2528
+ const [file3, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", new URL(url).href]] : ["xdg-open", [url]];
2042
2529
  await new Promise((resolve) => {
2043
- const child = spawn2(file4, [...args], { stdio: "ignore", detached: true, env });
2530
+ const child = spawn2(file3, [...args], { stdio: "ignore", detached: true, env });
2044
2531
  child.on("error", () => resolve());
2045
2532
  child.on("spawn", () => {
2046
2533
  child.unref();
@@ -2048,6 +2535,75 @@ async function openLink(context2, url) {
2048
2535
  });
2049
2536
  });
2050
2537
  }
2538
+ var APP_POLL_MS = 3e3;
2539
+ function installer(context2, app) {
2540
+ if (app.install.startsWith("npm install -g ")) return command(context2, "npm") === void 0 ? void 0 : app.install;
2541
+ return context2.platform !== "win32" && app.install.startsWith("curl ") ? app.install : void 0;
2542
+ }
2543
+ async function npmUserPrefix(context2) {
2544
+ const npm = command(context2, "npm");
2545
+ if (npm === void 0 || context2.platform === "win32") return void 0;
2546
+ const prefix = (await context2.run(npm, ["prefix", "-g"], { timeoutMs: 3e4 })).stdout.trim().split("\n").pop() ?? "";
2547
+ if (prefix === "") return void 0;
2548
+ const writable = async (path) => await access3(path, constants4.W_OK).then(() => true, () => false);
2549
+ const modules = join15(prefix, "lib", "node_modules");
2550
+ if (await writable(modules) || !existsSync7(modules) && await writable(prefix)) return void 0;
2551
+ return join15(context2.home, ".local");
2552
+ }
2553
+ async function waitForApp(context2, app, page, options) {
2554
+ context2.say.note(`Opening ${page}. Download ${app.name} there and install it; this goes on once it is here.`);
2555
+ if (options.open) await openLink(context2, page);
2556
+ const until = context2.now() + options.waitMs;
2557
+ while (!await app.found(context2)) {
2558
+ if (context2.now() + APP_POLL_MS > until) throw new Problem("app_not_installed", `${app.name} is not installed yet. Install it from ${page}, then run this again.`, { app: app.id, download: page });
2559
+ await context2.sleep(APP_POLL_MS);
2560
+ }
2561
+ context2.say.note(`Found ${app.name}.`);
2562
+ return true;
2563
+ }
2564
+ async function getApp(context2, app, options) {
2565
+ const page = context2.platform === "darwin" ? app.desktop : void 0;
2566
+ if (app.fetched === true && page === void 0) return true;
2567
+ const terminal = app.fetched === true ? "" : installer(context2, app);
2568
+ const ways = [...page === void 0 ? [] : ["app"], ...terminal === void 0 ? [] : ["terminal"]];
2569
+ if (!context2.person || ways.length === 0) return app.fetched === true;
2570
+ if (ways.length === 1 && terminal && app.installNeedsNode !== void 0) await needNode(context2, app.installNeedsNode, `${app.name}'s installer`, ["connect", app.id]);
2571
+ let way;
2572
+ if (ways.length === 2) {
2573
+ const picked = await context2.choose(`${app.name} is not on this computer. How do you want it?`, [
2574
+ app.installMacApp === void 0 ? "Download the Mac app" : "Install the Mac app",
2575
+ app.fetched === true ? "Run it from this terminal, in your browser" : `Install it in this terminal (${app.install})`
2576
+ ]);
2577
+ way = picked === void 0 ? void 0 : ways[picked];
2578
+ } else if (await context2.confirm(ways[0] === "app" ? `${app.name} is not on this computer. Download the Mac app from ${page}?` : `${app.name} is not installed here. Install it now, with ${app.install}?`)) {
2579
+ way = ways[0];
2580
+ }
2581
+ if (way === void 0) return false;
2582
+ if (way === "app" && app.installMacApp !== void 0) {
2583
+ if (await app.installMacApp(context2) && await app.found(context2)) return true;
2584
+ if (app.fetched === true) {
2585
+ context2.say.note(`${app.name}'s app could not be installed here, so it runs in your browser instead.`);
2586
+ return true;
2587
+ }
2588
+ }
2589
+ if (way === "app") return await waitForApp(context2, app, page, options);
2590
+ if (terminal === "") return true;
2591
+ const local = terminal?.startsWith("npm install -g ") === true ? await npmUserPrefix(context2) : void 0;
2592
+ context2.say.note(`Installing ${app.name}...`);
2593
+ const code = await context2.handOver(local === void 0 ? terminal : terminal.replace("npm install -g ", `npm install -g --prefix=${shellWord(local)} `));
2594
+ if (code !== 0) throw new Problem("app_install_failed", `${app.name} could not be installed: its installer ended with ${code}. Install it with ${app.install}, then run this again.`, { app: app.id, install: app.install });
2595
+ if (local !== void 0) {
2596
+ const bin2 = join15(local, "bin");
2597
+ const path = context2.env.PATH ?? "";
2598
+ if (!path.split(delimiter3).includes(bin2)) {
2599
+ context2.env.PATH = `${bin2}${delimiter3}${path}`;
2600
+ context2.say.note(`${app.name} went in ~/.local/bin, as npm's own folder is for root here. Add that folder to your PATH to start it by name.`);
2601
+ }
2602
+ }
2603
+ if (!await app.found(context2)) throw new Problem("app_not_found", `${app.name} is installed, but this terminal does not find it yet. Open a new terminal, then run this again.`, { app: app.id });
2604
+ context2.say.note(`Installed ${app.name}.`);
2605
+ return true;
2606
+ }
2051
2607
  async function stillGood(context2, current) {
2052
2608
  return current.key !== void 0 && await whoami(context2, current.key) !== void 0;
2053
2609
  }
@@ -2081,29 +2637,51 @@ async function test(context2, app, key, model2) {
2081
2637
  }
2082
2638
  async function connect(context2, app, options) {
2083
2639
  const { say } = context2;
2084
- if (app.fetched !== true && !await app.found(context2)) throw new Problem("app_not_installed", `${app.name} is not installed here. ${/^(npm|curl) /u.test(app.install) ? `Install it with: ${app.install}` : app.install}`, { app: app.id, install: app.install });
2085
- await app.prepare?.(context2);
2640
+ try {
2641
+ if (!await app.found(context2) && !await getApp(context2, app, options)) {
2642
+ const page = context2.platform === "darwin" ? app.desktop : void 0;
2643
+ throw new Problem("app_not_installed", `${app.name} is not installed here. ${/^(npm|curl) /u.test(app.install) ? `Install it with: ${app.install}` : app.install}${page === void 0 ? "" : `, or get the Mac app: ${page}`}`, { app: app.id, install: app.install, ...page === void 0 ? {} : { download: page } });
2644
+ }
2645
+ await app.prepare?.(context2);
2646
+ } catch (error) {
2647
+ if (error instanceof HandedOver) return error.exitCode;
2648
+ throw error;
2649
+ }
2086
2650
  const current = await app.current(context2);
2087
2651
  const here = current.base === apiBase(context2);
2088
- const reuse = options.fresh !== true && here && (current.key === void 0 ? app.ask !== void 0 && await pendingSignIn(context2, app) === void 0 : await stillGood(context2, current));
2089
- let key = reuse ? current.key : void 0;
2652
+ let given;
2653
+ if (options.key !== void 0) {
2654
+ if (app.collectsKey === true) throw new Problem("needs_sign_in", `${app.name} signs in by itself, which an owner or admin of the workspace approves. Ask one to connect it, or set it up by hand from Connect to app.`, { app: app.id });
2655
+ given = await whoami(context2, options.key);
2656
+ if (given === void 0) throw new Problem("key_refused", "AdaptiqAI does not take that key. Make a new one in the console, then run this again.", { app: app.id });
2657
+ }
2658
+ const reuse = options.code === void 0 && options.key === void 0 && options.fresh !== true && here && (current.key === void 0 ? app.ask !== void 0 && await pendingSignIn(context2, app) === void 0 : await stillGood(context2, current));
2659
+ let key = reuse ? current.key : options.key;
2090
2660
  let deviceCode;
2091
2661
  let signedInOn;
2092
- if (!reuse) {
2662
+ let workspace;
2663
+ if (!reuse && options.code !== void 0 && app.collectsKey === true) {
2664
+ signedInOn = await modelForSignIn(context2, options.model);
2665
+ deviceCode = options.code;
2666
+ } else if (options.key !== void 0) {
2667
+ signedInOn = await modelForSignIn(context2, options.model);
2668
+ workspace = given?.workspace;
2669
+ } else if (!reuse) {
2093
2670
  signedInOn = await modelForSignIn(context2, options.model);
2094
2671
  const signedIn = await signIn(context2, app, {
2095
2672
  model: signedInOn,
2096
2673
  waitMs: options.waitMs,
2097
2674
  open: async (url) => {
2098
2675
  if (options.open) await openLink(context2, url);
2099
- }
2676
+ },
2677
+ ...options.code === void 0 ? {} : { code: options.code }
2100
2678
  });
2101
2679
  if (signedIn.state === "waiting") {
2102
2680
  say.event("waiting", { for: "sign_in", app: app.id, run_again: true }, "Still waiting. Run this again once you have pressed Connect.");
2103
2681
  return EXIT.waitingForSignIn;
2104
2682
  }
2105
2683
  ;
2106
- ({ key, deviceCode } = signedIn);
2684
+ ({ key, deviceCode, workspace } = signedIn);
2107
2685
  say.event("signed_in", { app: app.id, workspace: signedIn.workspace }, `Signed in to ${signedIn.workspace}.`);
2108
2686
  } else {
2109
2687
  await forgetSignIn(context2, app);
@@ -2114,14 +2692,16 @@ async function connect(context2, app, options) {
2114
2692
  let left = [];
2115
2693
  if (settled !== void 0) model2 = settled;
2116
2694
  else {
2117
- const models2 = chooseModels(await listModels(context2, key), options.model, reuse ? current.model : signedInOn);
2695
+ const listed = await listModels(context2, key);
2696
+ const offered = key === void 0 && options.code !== void 0 && options.model !== void 0 && !listed.some((entry2) => entry2.id === options.model) ? [...namedModels([options.model]), ...listed] : listed;
2697
+ const models2 = chooseModels(offered, options.model, reuse ? current.model : signedInOn);
2118
2698
  const facts = { ...key === void 0 ? {} : { key }, ...deviceCode === void 0 ? {} : { deviceCode }, models: models2 };
2119
2699
  left = await app.apply(context2, facts);
2120
2700
  await forgetSignIn(context2, app);
2121
2701
  model2 = models2[0].id;
2122
2702
  say.event("configured", { app: app.id, model: model2, backups: context2.backups.saved }, [
2123
2703
  `${app.does} It starts on ${models2[0].name}.`,
2124
- ...context2.backups.saved.length > 0 ? [`What changed was copied first, to ${dirname3(context2.backups.saved[0])}.`] : []
2704
+ ...context2.backups.saved.length > 0 ? [`What changed was copied first, to ${dirname4(context2.backups.saved[0])}.`] : []
2125
2705
  ]);
2126
2706
  }
2127
2707
  const until = context2.now() + options.waitMs;
@@ -2139,7 +2719,7 @@ async function connect(context2, app, options) {
2139
2719
  say.event(
2140
2720
  "insufficient_balance",
2141
2721
  { app: app.id, add_funds_url: result.url, run_again: true, message: result.message },
2142
- [`Your balance is empty. Add a card or funds: ${result.url}`, ...context2.now() + FUNDS_POLL_MS > until ? ["Run this again once they are in."] : ["Waiting for funds. You can stop this and run it again at any time."]]
2722
+ [`Your balance is empty. Add funds: ${result.url}`, ...context2.now() + FUNDS_POLL_MS > until ? ["Run this again once they are in."] : ["Waiting for funds. You can stop this and run it again at any time."]]
2143
2723
  );
2144
2724
  told = true;
2145
2725
  }
@@ -2148,6 +2728,9 @@ async function connect(context2, app, options) {
2148
2728
  }
2149
2729
  const start2 = await app.start(context2, model2);
2150
2730
  say.event("connected", { app: app.id, model: model2, start: start2, notes: left }, ["Connected. AdaptiqAI answered.", ...left, `Start it: ${start2}`]);
2731
+ if (app.startsByCommand?.(context2) !== false && await context2.confirm(`Start ${app.name} now?`)) {
2732
+ await context2.handOver(await app.greet?.(context2, model2, options.hello ?? hello(workspace)) ?? start2);
2733
+ }
2151
2734
  return EXIT.connected;
2152
2735
  }
2153
2736
  async function status(context2, only) {
@@ -2180,13 +2763,21 @@ async function status(context2, only) {
2180
2763
  }
2181
2764
  async function disconnect(context2, app) {
2182
2765
  await forgetSignIn(context2, app);
2183
- if (!await app.found(context2) || (await app.current(context2)).base === void 0) {
2184
- context2.say.event("not_connected", { app: app.id }, `${app.name} is not connected to AdaptiqAI here.`);
2766
+ const connected = await app.found(context2) && (await app.current(context2)).base !== void 0;
2767
+ const notes = connected ? await app.remove(context2) ?? [] : [];
2768
+ const { removed, left } = await app.clean?.(context2) ?? { removed: [], left: [] };
2769
+ if (!connected) {
2770
+ context2.say.event("not_connected", { app: app.id, removed, notes: left }, [
2771
+ `${app.name} is not connected to AdaptiqAI here.`,
2772
+ ...removed.length > 0 ? ["It still held an AdaptiqAI key, which is gone now."] : [],
2773
+ ...left
2774
+ ]);
2185
2775
  return 0;
2186
2776
  }
2187
- await app.remove(context2);
2188
- context2.say.event("disconnected", { app: app.id, console_url: context2.console, backups: context2.backups.saved }, [
2777
+ context2.say.event("disconnected", { app: app.id, console_url: context2.console, backups: context2.backups.saved, removed, notes: [...notes, ...left] }, [
2189
2778
  `${app.name} no longer uses AdaptiqAI.`,
2779
+ ...notes,
2780
+ ...left,
2190
2781
  `To end the connection too, disconnect it in the console: ${context2.console}`
2191
2782
  ]);
2192
2783
  return 0;
@@ -2195,6 +2786,7 @@ async function disconnect(context2, app) {
2195
2786
  // src/entry.ts
2196
2787
  var USAGE = `Connect the AI app you use to AdaptiqAI.
2197
2788
 
2789
+ npx -y adaptiqai pick the app, then connect it
2198
2790
  npx -y adaptiqai connect <app> sign in, set the app up, and test it
2199
2791
  npx -y adaptiqai status [app] say where each app stands
2200
2792
  npx -y adaptiqai disconnect <app> take AdaptiqAI out of an app
@@ -2218,17 +2810,14 @@ to press Connect; 4 waiting for funds. After 3 or 4, run the same command again.
2218
2810
  `;
2219
2811
  var TEN_MINUTES_MS = 6e5;
2220
2812
  async function pick(context2) {
2221
- const found = [];
2222
- for (const app of APPS) if (await app.found(context2)) found.push(app);
2223
- if (found.length === 1) return found[0];
2224
- if (found.length === 0) throw new Problem("no_app", `No app that AdaptiqAI connects is installed here. Apps: ${APPS.map((app) => app.id).join(", ")}.`);
2813
+ const here = /* @__PURE__ */ new Set();
2814
+ for (const app of APPS) if (await app.found(context2)) here.add(app.id);
2225
2815
  if (!context2.person) {
2226
- context2.say.event("choose_app", { apps: found.map((app) => app.id) }, "Say which app.");
2816
+ context2.say.event("choose_app", { apps: APPS.map((app) => app.id), installed: APPS.filter((app) => here.has(app.id)).map((app) => app.id) }, "Say which app.");
2227
2817
  return void 0;
2228
2818
  }
2229
- found.forEach((app, index) => context2.say.note(` ${index + 1}. ${app.name}`));
2230
- const answer = await ask("Which app? ");
2231
- return found[Number(answer) - 1] ?? appNamed(answer);
2819
+ const index = await context2.choose("Which app do you want to use with AdaptiqAI?", APPS.map((app) => `${app.name}${here.has(app.id) ? " (on this computer)" : ""}`));
2820
+ return index === void 0 ? void 0 : APPS[index];
2232
2821
  }
2233
2822
  async function ask(question) {
2234
2823
  const reader = createInterface({ input: process.stdin, output: process.stdout });
@@ -2249,14 +2838,24 @@ function context(person, json, app) {
2249
2838
  platform,
2250
2839
  person,
2251
2840
  confirm: async (question) => person && !/^n/iu.test(await ask(`${question} [Y/n] `)),
2841
+ choose: async (question, options) => {
2842
+ if (!person) return void 0;
2843
+ process.stdout.write(`${question}
2844
+ ${options.map((option, index) => ` ${index + 1}. ${option}`).join("\n")}
2845
+ `);
2846
+ const answer = Number(await ask(`Pick 1-${options.length}: `));
2847
+ return Number.isInteger(answer) && answer >= 1 && answer <= options.length ? answer - 1 : void 0;
2848
+ },
2252
2849
  say: new Output(json),
2253
2850
  run: runner(env, platform),
2851
+ handOver: handOver(env, platform),
2852
+ node: process.versions.node,
2254
2853
  fetch: globalThis.fetch.bind(globalThis),
2255
2854
  now: Date.now,
2256
2855
  sleep: async (ms) => {
2257
2856
  await new Promise((resolve) => setTimeout(resolve, ms));
2258
2857
  },
2259
- backups: new Backups(join15(cliDir(paths), "backups", app, stamp))
2858
+ backups: new Backups(join16(cliDir(paths), "backups", app, stamp))
2260
2859
  };
2261
2860
  }
2262
2861
  var parse = (argv) => parseArgs({
@@ -2281,7 +2880,7 @@ async function main(argv) {
2281
2880
  }
2282
2881
  const { values, positionals } = parsed;
2283
2882
  if (values.version) {
2284
- process.stdout.write(`${"0.1.1"}
2883
+ process.stdout.write(`${"0.2.0"}
2285
2884
  `);
2286
2885
  return 0;
2287
2886
  }
@@ -2305,9 +2904,21 @@ async function main(argv) {
2305
2904
  const seconds = values.wait === void 0 ? void 0 : Number(values.wait);
2306
2905
  if (seconds !== void 0 && !(seconds >= 0)) throw new Problem("bad_wait", "--wait takes a number of seconds.", { exit: EXIT.usage });
2307
2906
  const waitMs = values["no-wait"] ? 0 : seconds === void 0 ? person ? TEN_MINUTES_MS : 0 : seconds * 1e3;
2907
+ const code = run.env.ADAPTIQ_CONNECT?.trim();
2908
+ if (code !== void 0 && code !== "" && !/^[A-Za-z0-9_-]{32,128}$/u.test(code)) throw new Problem("bad_code", "ADAPTIQ_CONNECT is not a connect code from the console.", { exit: EXIT.usage });
2308
2909
  const app = named ?? await pick(run);
2309
2910
  if (app === void 0) return EXIT.usage;
2310
- return await connect(run, app, { ...values.model === void 0 ? {} : { model: values.model }, waitMs, open: !values["no-open"] });
2911
+ const key = run.env.ADAPTIQ_KEY?.trim();
2912
+ if (key !== void 0 && key !== "" && !/^ak_[A-Za-z0-9._-]+$/u.test(key)) throw new Problem("bad_key", "ADAPTIQ_KEY is not an AdaptiqAI key.", { exit: EXIT.usage });
2913
+ const hello2 = run.env.ADAPTIQ_HELLO?.replace(new RegExp("\\p{Cc}", "gu"), "").trim().slice(0, 400);
2914
+ return await connect(run, app, {
2915
+ ...values.model === void 0 ? {} : { model: values.model },
2916
+ ...hello2 === void 0 || hello2 === "" ? {} : { hello: hello2 },
2917
+ ...key === void 0 || key === "" ? {} : { key },
2918
+ ...code === void 0 || code === "" ? {} : { code },
2919
+ waitMs,
2920
+ open: !values["no-open"]
2921
+ });
2311
2922
  }
2312
2923
  var quit = (code) => {
2313
2924
  process.stdout.write("", () => process.exit(code));