indusagi-coding-agent 0.2.13 → 0.2.14

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 (56) hide show
  1. package/README.md +19 -0
  2. package/dist/entry.js +1762 -958
  3. package/dist/guardrails.js +145 -35
  4. package/dist/index.js +1810 -1006
  5. package/dist/review-agent.js +344 -60
  6. package/dist/review-server.js +346 -62
  7. package/package.json +12 -3
  8. package/src/boot/auth-vault.ts +4 -5
  9. package/src/boot/runners/addon-wiring.ts +111 -3
  10. package/src/boot/runners/benchmark-integrity.ts +107 -0
  11. package/src/boot/runners/repl-runner.ts +2 -1
  12. package/src/boot/runners/session.ts +18 -8
  13. package/src/boot/server-token.ts +20 -11
  14. package/src/briefing/context-docs.ts +144 -12
  15. package/src/channels/oneshot.ts +6 -0
  16. package/src/conductor/conductor.ts +67 -12
  17. package/src/conductor/contract.ts +4 -3
  18. package/src/conductor/transcript-store/store.ts +4 -8
  19. package/src/console/auth-status.ts +9 -4
  20. package/src/console/components/AgentsView.ts +2 -2
  21. package/src/console/components/BackgroundAgents.ts +15 -2
  22. package/src/console/components/Banner.ts +20 -9
  23. package/src/console/components/Composer.ts +57 -82
  24. package/src/console/components/ScrollViewport.ts +46 -0
  25. package/src/console/components/TerminalConsole.ts +165 -162
  26. package/src/console/components/WorkingIndicator.ts +6 -5
  27. package/src/console/components/banner-sweep.ts +5 -1
  28. package/src/console/components/welcome.ts +3 -3
  29. package/src/console/contract.ts +10 -5
  30. package/src/console/index.ts +7 -7
  31. package/src/console/input/complete.ts +2 -2
  32. package/src/console/input/dir-reader.ts +3 -1
  33. package/src/console/input/graphemes.ts +22 -0
  34. package/src/console/input/keymap.ts +6 -4
  35. package/src/console/mount.ts +45 -3
  36. package/src/console/overlays/auth.ts +28 -8
  37. package/src/console/overlays/boards.ts +13 -40
  38. package/src/console/overlays/host.ts +10 -0
  39. package/src/console/overlays/pickers.ts +35 -30
  40. package/src/console/reducer.ts +12 -5
  41. package/src/console/slash/builtins.ts +4 -3
  42. package/src/console/slash/commands/integrations.ts +13 -14
  43. package/src/console/slash/commands/transcript.ts +3 -8
  44. package/src/console/slash/commands/workbench.ts +2 -2
  45. package/src/console/test-support.ts +48 -0
  46. package/src/integrations/sarvam/config.ts +15 -13
  47. package/src/kit/external-editor.ts +17 -1
  48. package/src/kit/run-shell.ts +31 -0
  49. package/src/launch/invocation/flags.ts +12 -0
  50. package/src/launch/login.ts +23 -10
  51. package/src/launch/oauth.ts +3 -0
  52. package/src/models.ts +78 -2
  53. package/src/security/private-state.ts +122 -0
  54. package/src/window-budget/summarize/condense.ts +4 -1
  55. package/src/workspace/brand.ts +1 -1
  56. package/src/workspace/locator.ts +2 -2
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/review-agent/server.ts
4
- import { randomUUID } from "node:crypto";
4
+ import { randomUUID as randomUUID2 } from "node:crypto";
5
5
  import { createServer } from "node:http";
6
6
  import { realpathSync } from "node:fs";
7
7
  import { fileURLToPath } from "node:url";
@@ -328,6 +328,56 @@ import { Agent } from "indusagi/agent";
328
328
  import { createZeroUsage as createZeroUsage2 } from "indusagi/ai";
329
329
  import * as path2 from "node:path";
330
330
 
331
+ // src/kit/run-shell.ts
332
+ import { spawn } from "node:child_process";
333
+ function runShell(command, cwd, signal) {
334
+ if (signal?.aborted) return Promise.resolve({ output: "Command cancelled.", exitCode: 130 });
335
+ return new Promise((resolve3) => {
336
+ let output = "", truncated = false, settled = false;
337
+ let forceKill;
338
+ const child = spawn(command, { cwd, shell: true, detached: process.platform !== "win32" });
339
+ const capture = (chunk) => {
340
+ output += String(chunk);
341
+ if (output.length > 1024 * 1024) {
342
+ output = output.slice(-1024 * 1024);
343
+ truncated = true;
344
+ }
345
+ };
346
+ const kill = (signalName) => {
347
+ try {
348
+ if (process.platform !== "win32" && child.pid) process.kill(-child.pid, signalName);
349
+ else child.kill(signalName);
350
+ } catch {
351
+ try {
352
+ child.kill(signalName);
353
+ } catch {
354
+ }
355
+ }
356
+ };
357
+ const abort = () => {
358
+ kill("SIGTERM");
359
+ forceKill = setTimeout(() => kill("SIGKILL"), 1e3);
360
+ forceKill.unref();
361
+ };
362
+ const finish = (exitCode) => {
363
+ if (settled) return;
364
+ settled = true;
365
+ signal?.removeEventListener("abort", abort);
366
+ if (forceKill) clearTimeout(forceKill);
367
+ resolve3({ output: (truncated ? "[older output truncated]\n" : "") + output, exitCode: signal?.aborted ? 130 : exitCode });
368
+ };
369
+ child.stdout?.on("data", capture);
370
+ child.stderr?.on("data", capture);
371
+ child.once("error", (error) => {
372
+ capture(error.message);
373
+ finish(1);
374
+ });
375
+ child.once("close", (code) => finish(code ?? 1));
376
+ signal?.addEventListener("abort", abort, { once: true });
377
+ if (signal?.aborted) abort();
378
+ });
379
+ }
380
+
331
381
  // src/conductor/catalog/catalog.ts
332
382
  import { z } from "zod";
333
383
  import { getModels, getProviders, modelRegistry } from "indusagi/ai";
@@ -359,7 +409,74 @@ var GPT_6_ASTRA_BASE = {
359
409
  }
360
410
  }
361
411
  };
412
+ var GPT_6_SOL_BASE = {
413
+ id: "gpt-6-sol",
414
+ name: "GPT-6 Sol",
415
+ api: "openai-responses",
416
+ reasoning: true,
417
+ reasoningEfforts: ["high"],
418
+ input: ["text", "image"],
419
+ cost: {
420
+ input: 10,
421
+ cacheRead: 1,
422
+ cacheWrite: 12.5,
423
+ output: 50
424
+ },
425
+ contextWindow: 105e4,
426
+ maxTokens: 128e3,
427
+ longContextPricing: {
428
+ threshold: 272e3,
429
+ multipliers: {
430
+ input: 2,
431
+ cacheRead: 2,
432
+ cacheWrite: 2,
433
+ output: 1.5
434
+ }
435
+ }
436
+ };
437
+ var GPT_6_LUNA_BASE = {
438
+ id: "gpt-6-luna",
439
+ name: "GPT-6 Luna",
440
+ api: "openai-responses",
441
+ reasoning: true,
442
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
443
+ input: ["text", "image"],
444
+ cost: {
445
+ input: 0.1,
446
+ cacheRead: 0.01,
447
+ cacheWrite: 0.125,
448
+ output: 0.5
449
+ },
450
+ contextWindow: 105e4,
451
+ maxTokens: 128e3,
452
+ longContextPricing: {
453
+ threshold: 272e3,
454
+ multipliers: { input: 2, cacheRead: 2, cacheWrite: 2, output: 1.5 }
455
+ }
456
+ };
362
457
  var CODING_AGENT_MODELS = [
458
+ {
459
+ ...GPT_6_SOL_BASE,
460
+ provider: "openai",
461
+ baseUrl: "https://api.openai.com/v1"
462
+ },
463
+ {
464
+ ...GPT_6_SOL_BASE,
465
+ api: "openai-codex-responses",
466
+ provider: "openai-codex",
467
+ baseUrl: "https://chatgpt.com/backend-api"
468
+ },
469
+ {
470
+ ...GPT_6_LUNA_BASE,
471
+ provider: "openai",
472
+ baseUrl: "https://api.openai.com/v1"
473
+ },
474
+ {
475
+ ...GPT_6_LUNA_BASE,
476
+ api: "openai-codex-responses",
477
+ provider: "openai-codex",
478
+ baseUrl: "https://chatgpt.com/backend-api"
479
+ },
363
480
  {
364
481
  ...GPT_6_ASTRA_BASE,
365
482
  provider: "openai",
@@ -395,8 +512,15 @@ function applyCodingAgentModelOverlay(registry) {
395
512
  return registry;
396
513
  }
397
514
  function isReasoningEffortSupported(modelId, effort) {
398
- if (modelId?.split("/").at(-1) !== "gpt-6-astra") return true;
399
- return GPT_6_ASTRA_BASE.reasoningEfforts.includes(effort);
515
+ const id = modelId?.split("/").at(-1);
516
+ if (id === "gpt-6-astra") {
517
+ return GPT_6_ASTRA_BASE.reasoningEfforts.includes(effort);
518
+ }
519
+ if (id === "gpt-6-sol") return effort === "high";
520
+ if (id === "gpt-6-luna") {
521
+ return GPT_6_LUNA_BASE.reasoningEfforts.includes(effort);
522
+ }
523
+ return true;
400
524
  }
401
525
 
402
526
  // src/conductor/catalog/catalog.ts
@@ -803,6 +927,125 @@ function parseSessionText(sessionId, text) {
803
927
  };
804
928
  }
805
929
 
930
+ // src/security/private-state.ts
931
+ import {
932
+ chmodSync,
933
+ closeSync,
934
+ constants,
935
+ fchmodSync,
936
+ fstatSync,
937
+ lstatSync,
938
+ mkdirSync,
939
+ openSync,
940
+ readFileSync,
941
+ renameSync,
942
+ rmSync,
943
+ writeFileSync
944
+ } from "node:fs";
945
+ import { randomUUID } from "node:crypto";
946
+ import { dirname } from "node:path";
947
+ var PRIVATE_DIRECTORY_MODE = 448;
948
+ var PRIVATE_FILE_MODE = 384;
949
+ var NO_FOLLOW = constants.O_NOFOLLOW ?? 0;
950
+ function isPosix() {
951
+ return process.platform !== "win32";
952
+ }
953
+ function assertOwned(uid, path3) {
954
+ if (!isPosix() || process.getuid === void 0) return;
955
+ if (uid !== process.getuid()) {
956
+ throw new Error(`Refusing sensitive path not owned by the current user: ${path3}`);
957
+ }
958
+ }
959
+ function ensurePrivateDirectorySync(path3) {
960
+ try {
961
+ const current = lstatSync(path3);
962
+ if (current.isSymbolicLink() || !current.isDirectory()) {
963
+ throw new Error(`Refusing non-directory sensitive state path: ${path3}`);
964
+ }
965
+ assertOwned(current.uid, path3);
966
+ } catch (error) {
967
+ if (error.code !== "ENOENT") throw error;
968
+ mkdirSync(path3, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
969
+ }
970
+ if (isPosix()) chmodSync(path3, PRIVATE_DIRECTORY_MODE);
971
+ }
972
+ function validateExistingFile(path3) {
973
+ try {
974
+ const current = lstatSync(path3);
975
+ if (current.isSymbolicLink() || !current.isFile()) {
976
+ throw new Error(`Refusing non-regular sensitive file: ${path3}`);
977
+ }
978
+ assertOwned(current.uid, path3);
979
+ } catch (error) {
980
+ if (error.code === "ENOENT") return;
981
+ throw error;
982
+ }
983
+ }
984
+ function readPrivateFileSync(path3) {
985
+ ensurePrivateDirectorySync(dirname(path3));
986
+ try {
987
+ validateExistingFile(path3);
988
+ const fd = openSync(path3, constants.O_RDONLY | NO_FOLLOW);
989
+ try {
990
+ const current = fstatSync(fd);
991
+ if (!current.isFile()) throw new Error(`Refusing non-regular sensitive file: ${path3}`);
992
+ assertOwned(current.uid, path3);
993
+ if (isPosix()) fchmodSync(fd, PRIVATE_FILE_MODE);
994
+ return readFileSync(fd, "utf8");
995
+ } finally {
996
+ closeSync(fd);
997
+ }
998
+ } catch (error) {
999
+ if (error.code === "ENOENT") return null;
1000
+ throw error;
1001
+ }
1002
+ }
1003
+ function atomicWritePrivateFileSync(path3, content) {
1004
+ const parent = dirname(path3);
1005
+ ensurePrivateDirectorySync(parent);
1006
+ validateExistingFile(path3);
1007
+ const temporary = `${path3}.tmp-${process.pid}-${randomUUID()}`;
1008
+ let fd;
1009
+ try {
1010
+ fd = openSync(
1011
+ temporary,
1012
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NO_FOLLOW,
1013
+ PRIVATE_FILE_MODE
1014
+ );
1015
+ writeFileSync(fd, content, "utf8");
1016
+ if (isPosix()) fchmodSync(fd, PRIVATE_FILE_MODE);
1017
+ closeSync(fd);
1018
+ fd = void 0;
1019
+ renameSync(temporary, path3);
1020
+ if (isPosix()) chmodSync(path3, PRIVATE_FILE_MODE);
1021
+ } catch (error) {
1022
+ if (fd !== void 0) closeSync(fd);
1023
+ try {
1024
+ rmSync(temporary, { force: true });
1025
+ } catch {
1026
+ }
1027
+ throw error;
1028
+ }
1029
+ }
1030
+ function appendPrivateFileSync(path3, content) {
1031
+ ensurePrivateDirectorySync(dirname(path3));
1032
+ validateExistingFile(path3);
1033
+ const fd = openSync(
1034
+ path3,
1035
+ constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | NO_FOLLOW,
1036
+ PRIVATE_FILE_MODE
1037
+ );
1038
+ try {
1039
+ const current = fstatSync(fd);
1040
+ if (!current.isFile()) throw new Error(`Refusing non-regular sensitive file: ${path3}`);
1041
+ assertOwned(current.uid, path3);
1042
+ if (isPosix()) fchmodSync(fd, PRIVATE_FILE_MODE);
1043
+ writeFileSync(fd, content, "utf8");
1044
+ } finally {
1045
+ closeSync(fd);
1046
+ }
1047
+ }
1048
+
806
1049
  // src/conductor/transcript-store/store.ts
807
1050
  function memoryBackend() {
808
1051
  const files = /* @__PURE__ */ new Map();
@@ -830,24 +1073,15 @@ function fsBackend(dir) {
830
1073
  return {
831
1074
  locate: (sessionId) => `${dir}/${sessionId}.ndjson`,
832
1075
  read: async (sessionId) => {
833
- const fs = await import("node:fs/promises");
834
- try {
835
- return await fs.readFile(await fileOf(sessionId), "utf8");
836
- } catch {
837
- return null;
838
- }
1076
+ return readPrivateFileSync(await fileOf(sessionId));
839
1077
  },
840
1078
  append: async (sessionId, line) => {
841
- const fs = await import("node:fs/promises");
842
- await fs.mkdir(dir, { recursive: true });
843
- await fs.appendFile(await fileOf(sessionId), `${line}
844
- `, "utf8");
1079
+ appendPrivateFileSync(await fileOf(sessionId), `${line}
1080
+ `);
845
1081
  },
846
1082
  rewrite: async (sessionId, text) => {
847
- const fs = await import("node:fs/promises");
848
- await fs.mkdir(dir, { recursive: true });
849
- await fs.writeFile(await fileOf(sessionId), text.length === 0 ? "" : `${text}
850
- `, "utf8");
1083
+ atomicWritePrivateFileSync(await fileOf(sessionId), text.length === 0 ? "" : `${text}
1084
+ `);
851
1085
  }
852
1086
  };
853
1087
  }
@@ -1365,7 +1599,7 @@ async function loadSpawn() {
1365
1599
  return void 0;
1366
1600
  }
1367
1601
  }
1368
- function captureOutput(spawn2, bin, args, cwd, signal) {
1602
+ function captureOutput(spawn3, bin, args, cwd, signal) {
1369
1603
  return new Promise((resolve3) => {
1370
1604
  let settled = false;
1371
1605
  const finish = (value) => {
@@ -1376,7 +1610,7 @@ function captureOutput(spawn2, bin, args, cwd, signal) {
1376
1610
  };
1377
1611
  let child;
1378
1612
  try {
1379
- child = spawn2(bin, args, { cwd, shell: false });
1613
+ child = spawn3(bin, args, { cwd, shell: false });
1380
1614
  } catch {
1381
1615
  finish();
1382
1616
  return;
@@ -1405,9 +1639,9 @@ function captureOutput(spawn2, bin, args, cwd, signal) {
1405
1639
  });
1406
1640
  }
1407
1641
  async function runTscDiagnostics(cwd, signal) {
1408
- const spawn2 = await loadSpawn();
1409
- if (!spawn2 || signal?.aborted) return [];
1410
- const output = await captureOutput(spawn2, path.join(cwd, "node_modules", ".bin", "tsc"), ["--noEmit", "--pretty", "false"], cwd, signal);
1642
+ const spawn3 = await loadSpawn();
1643
+ if (!spawn3 || signal?.aborted) return [];
1644
+ const output = await captureOutput(spawn3, path.join(cwd, "node_modules", ".bin", "tsc"), ["--noEmit", "--pretty", "false"], cwd, signal);
1411
1645
  return output === void 0 ? [] : parseTscOutput(output, cwd);
1412
1646
  }
1413
1647
  function parseTscOutput(output, cwd) {
@@ -1427,9 +1661,9 @@ function parseTscOutput(output, cwd) {
1427
1661
  }
1428
1662
  async function runEslintDiagnostics(files, cwd, signal) {
1429
1663
  if (files.length === 0) return [];
1430
- const spawn2 = await loadSpawn();
1431
- if (!spawn2 || signal?.aborted) return [];
1432
- const output = await captureOutput(spawn2, path.join(cwd, "node_modules", ".bin", "eslint"), ["--format", "json", ...files], cwd, signal);
1664
+ const spawn3 = await loadSpawn();
1665
+ if (!spawn3 || signal?.aborted) return [];
1666
+ const output = await captureOutput(spawn3, path.join(cwd, "node_modules", ".bin", "eslint"), ["--format", "json", ...files], cwd, signal);
1433
1667
  return output === void 0 ? [] : parseEslintOutput(output, cwd);
1434
1668
  }
1435
1669
  function parseEslintOutput(output, cwd) {
@@ -1694,6 +1928,8 @@ var SessionConductorImpl = class {
1694
1928
  #matcher;
1695
1929
  #state;
1696
1930
  #busy = false;
1931
+ #activeWork;
1932
+ #shellAbort;
1697
1933
  #draining = false;
1698
1934
  #queue = [];
1699
1935
  #boundModel;
@@ -1753,6 +1989,7 @@ var SessionConductorImpl = class {
1753
1989
  abort() {
1754
1990
  this.clearQueue();
1755
1991
  this.#condenseAbort?.abort();
1992
+ this.#shellAbort?.abort();
1756
1993
  this.agent.abort();
1757
1994
  const fault = conductorFault("aborted", "turn aborted by caller");
1758
1995
  this.#dispatch({ type: "fault", fault });
@@ -1765,13 +2002,22 @@ var SessionConductorImpl = class {
1765
2002
  return this.#state;
1766
2003
  }
1767
2004
  this.#busy = true;
1768
- try {
1769
- await this.#runTurn(input);
1770
- await this.#drainQueue();
1771
- return this.#state;
1772
- } finally {
1773
- this.#busy = false;
1774
- }
2005
+ const work = (async () => {
2006
+ try {
2007
+ await this.#runTurn(input);
2008
+ await this.#drainQueue();
2009
+ return this.#state;
2010
+ } finally {
2011
+ this.#busy = false;
2012
+ }
2013
+ })();
2014
+ this.#activeWork = work;
2015
+ return work;
2016
+ }
2017
+ async #stopWork() {
2018
+ if (!this.#busy) return;
2019
+ this.abort();
2020
+ await this.#activeWork;
1775
2021
  }
1776
2022
  async #drainQueue() {
1777
2023
  if (this.#draining) return;
@@ -1860,8 +2106,16 @@ var SessionConductorImpl = class {
1860
2106
  }
1861
2107
  this.#applyModel(card);
1862
2108
  }
1863
- cycleModel(id) {
1864
- this.selectModel(id);
2109
+ cycleModel(id, direction = 1) {
2110
+ if (id.trim()) {
2111
+ this.selectModel(id);
2112
+ return;
2113
+ }
2114
+ const cards = this.availableModels();
2115
+ if (!cards.length) return;
2116
+ const at = cards.findIndex((card) => card.id === this.#state.modelId);
2117
+ const index = at < 0 ? 0 : (at + direction + cards.length) % cards.length;
2118
+ this.selectModel(cards[index].id);
1865
2119
  }
1866
2120
  #applyModel(card) {
1867
2121
  const model = withGatewayBaseUrl(card.model, this.#gatewayBaseUrls);
@@ -1876,28 +2130,36 @@ var SessionConductorImpl = class {
1876
2130
  registerTools(tools) {
1877
2131
  this.agent.setTools([...this.#deckTools, ...tools]);
1878
2132
  }
1879
- async condense() {
2133
+ async condense(instructions) {
1880
2134
  if (this.#busy) return "busy";
1881
2135
  this.#busy = true;
1882
2136
  this.#condenseAbort = new AbortController();
1883
- try {
1884
- const result = await this.#runCondense(true, this.#condenseAbort.signal);
1885
- await this.#drainQueue();
1886
- return result;
1887
- } finally {
1888
- this.#busy = false;
1889
- this.#condenseAbort = void 0;
1890
- }
2137
+ const work = (async () => {
2138
+ try {
2139
+ const result = await this.#runCondense(true, this.#condenseAbort.signal, instructions);
2140
+ await this.#drainQueue();
2141
+ return result;
2142
+ } finally {
2143
+ this.#busy = false;
2144
+ this.#condenseAbort = void 0;
2145
+ }
2146
+ })();
2147
+ this.#activeWork = work;
2148
+ return work;
1891
2149
  }
1892
2150
  cancelCondense() {
1893
2151
  this.#condenseAbort?.abort();
1894
2152
  }
1895
- async #runCondense(force, signal) {
2153
+ async #runCondense(force, signal, instructions) {
1896
2154
  const before = [...this.agent.state.messages];
1897
2155
  this.#dispatch({ type: "phase", phase: "condensing" });
1898
2156
  try {
1899
- const after = await (this.deps.condense ?? noopCondense)(before, { force, model: this.#boundModel, contextTokens: this.#state.contextTokens, signal });
1900
- if (signal?.aborted) return "cancelled";
2157
+ const after = await (this.deps.condense ?? noopCondense)(before, { force, model: this.#boundModel, contextTokens: this.#state.contextTokens, signal, instructions });
2158
+ if (signal?.aborted) {
2159
+ this.#dispatch({ type: "settled" });
2160
+ this.#hub.emit({ kind: "idle" });
2161
+ return "cancelled";
2162
+ }
1901
2163
  if (after.length >= before.length) {
1902
2164
  this.#dispatch({ type: "settled" });
1903
2165
  return "nothing";
@@ -1924,10 +2186,12 @@ var SessionConductorImpl = class {
1924
2186
  }
1925
2187
  async #move(id, message) {
1926
2188
  try {
2189
+ await this.#stopWork();
1927
2190
  await this.store.branchAt(id);
1928
2191
  this.agent.replaceMessages(this.store.messages());
1929
2192
  this.#dispatch({ type: "head", head: this.store.head });
1930
2193
  this.#dispatch({ type: "settled" });
2194
+ this.#hub.emit({ kind: "reset" });
1931
2195
  this.#hub.emit({ kind: "idle" });
1932
2196
  } catch (error) {
1933
2197
  const fault = conductorFault("persistence", message, error);
@@ -1942,18 +2206,33 @@ var SessionConductorImpl = class {
1942
2206
  return { canRestore: this.options.checkpoint?.hasSnapshot(id) ?? false };
1943
2207
  }
1944
2208
  async executeBash(command, opts = {}) {
1945
- const { spawn: spawn2 } = await import("node:child_process");
1946
- const outcome = await new Promise((resolve3) => {
1947
- const child = spawn2(command, { cwd: this.#workspace, shell: true });
1948
- let output = "";
1949
- child.stdout?.on("data", (chunk) => output += String(chunk));
1950
- child.stderr?.on("data", (chunk) => output += String(chunk));
1951
- child.on("error", (error) => resolve3({ output: output + errorText(error), exitCode: 1 }));
1952
- child.on("close", (code) => resolve3({ output, exitCode: code ?? 1 }));
1953
- });
1954
- if (!opts.excludeFromContext) await this.store.append({ role: "user", content: `$ ${command}
1955
- ${outcome.output}`.trimEnd(), timestamp: Date.now() }, "note");
1956
- return outcome;
2209
+ if (this.#busy) throw new Error("A turn is running \u2014 stop it before running a shell command.");
2210
+ this.#busy = true;
2211
+ this.#shellAbort = new AbortController();
2212
+ const signal = opts.signal ? AbortSignal.any([opts.signal, this.#shellAbort.signal]) : this.#shellAbort.signal;
2213
+ const work = (async () => {
2214
+ try {
2215
+ this.#dispatch({ type: "phase", phase: "tooling" });
2216
+ const outcome = await runShell(command, this.#workspace, signal);
2217
+ if (!opts.excludeFromContext) {
2218
+ const message = { role: "user", content: `$ ${command}
2219
+ ${outcome.output}`.trimEnd(), timestamp: Date.now() };
2220
+ const entry = await this.store.append(message, "note");
2221
+ this.agent.replaceMessages([...this.agent.state.messages, message]);
2222
+ this.#dispatch({ type: "head", head: this.store.head });
2223
+ this.#hub.emit({ kind: "persisted", entryId: entry.id });
2224
+ }
2225
+ this.#dispatch({ type: "settled" });
2226
+ this.#hub.emit({ kind: "idle" });
2227
+ await this.#drainQueue();
2228
+ return outcome;
2229
+ } finally {
2230
+ this.#busy = false;
2231
+ this.#shellAbort = void 0;
2232
+ }
2233
+ })();
2234
+ this.#activeWork = work;
2235
+ return work;
1957
2236
  }
1958
2237
  stats() {
1959
2238
  const messages = this.agent.state.messages;
@@ -2023,6 +2302,7 @@ ${outcome.output}`.trimEnd(), timestamp: Date.now() }, "note");
2023
2302
  this.#sessionName = name;
2024
2303
  }
2025
2304
  async resume(id) {
2305
+ await this.#stopWork();
2026
2306
  if (!await this.store.load(id)) {
2027
2307
  const fault = conductorFault("persistence", `no session "${id}" to resume`);
2028
2308
  this.#dispatch({ type: "fault", fault });
@@ -2031,19 +2311,23 @@ ${outcome.output}`.trimEnd(), timestamp: Date.now() }, "note");
2031
2311
  }
2032
2312
  this.agent.replaceMessages(this.store.messages());
2033
2313
  this.#state = { ...this.#state, head: this.store.head, usage: this.store.usage ?? createZeroUsage2(), phase: "idle" };
2314
+ this.#hub.emit({ kind: "reset" });
2034
2315
  this.#hub.emit({ kind: "idle" });
2035
2316
  }
2036
2317
  async newSession() {
2037
- this.agent.abort();
2318
+ await this.#stopWork();
2038
2319
  this.agent.replaceMessages([]);
2039
2320
  this.store.startNewSession(newSessionId());
2040
2321
  this.#queue.length = 0;
2041
2322
  this.#sessionName = void 0;
2042
2323
  this.#state = { phase: "idle", head: this.store.head, usage: createZeroUsage2(), contextTokens: 0, modelId: this.#state.modelId };
2324
+ this.#hub.emit({ kind: "reset" });
2043
2325
  this.#hub.emit({ kind: "idle" });
2044
2326
  }
2045
2327
  #dispatch(action) {
2328
+ const phase = this.#state.phase;
2046
2329
  this.#state = reduceState(this.#state, action);
2330
+ if (phase !== this.#state.phase) this.#hub.emit({ kind: "phase", phase: this.#state.phase });
2047
2331
  }
2048
2332
  };
2049
2333
 
@@ -2146,7 +2430,7 @@ var todoCard = {
2146
2430
  };
2147
2431
 
2148
2432
  // src/capability-deck/cards/bg-process-card.ts
2149
- import { spawn } from "node:child_process";
2433
+ import { spawn as spawn2 } from "node:child_process";
2150
2434
  import { Type as Type2 } from "@sinclair/typebox";
2151
2435
  var MAX_BUFFERED_LINES = 2e3;
2152
2436
  var DaemonTable = class {
@@ -2158,7 +2442,7 @@ var DaemonTable = class {
2158
2442
  }
2159
2443
  start(command) {
2160
2444
  const id = `bg-${this.nextSeq++}`;
2161
- const child = spawn(command, {
2445
+ const child = spawn2(command, {
2162
2446
  cwd: this.cwd,
2163
2447
  shell: true,
2164
2448
  stdio: ["ignore", "pipe", "pipe"],
@@ -4207,7 +4491,7 @@ function createReviewAgentRuntime(config) {
4207
4491
  store,
4208
4492
  tokenProvider,
4209
4493
  reviewer,
4210
- workerId: `${process.env.HOSTNAME ?? "local"}-${process.pid}-${randomUUID().slice(0, 8)}`
4494
+ workerId: `${process.env.HOSTNAME ?? "local"}-${process.pid}-${randomUUID2().slice(0, 8)}`
4211
4495
  });
4212
4496
  return { store, service };
4213
4497
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indusagi-coding-agent",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Indusagi coding agent — a terminal-first AI coding agent, built from scratch on the indusagi framework.",
5
5
  "author": "Varun Israni",
6
6
  "license": "AGPL-3.0",
@@ -55,6 +55,9 @@
55
55
  "build": "node build.mjs",
56
56
  "test": "vitest --run",
57
57
  "test:watch": "vitest",
58
+ "test:tui": "vitest run src/console src/conductor/tui-controls.test.ts src/kit/external-editor.test.ts src/launch/login-cancellation.test.ts",
59
+ "test:tui:loop": "node scripts/test-tui-loop.mjs",
60
+ "test:tui:pty": "python3 scripts/test-tui-pty.py",
58
61
  "prepublishOnly": "npm test && npm run build",
59
62
  "start": "node dist/entry.js",
60
63
  "start:help": "node dist/entry.js --help",
@@ -64,12 +67,13 @@
64
67
  "@sinclair/typebox": "^0.34.49",
65
68
  "chalk": "^5.6.2",
66
69
  "highlight.js": "^11.11.1",
67
- "indusagi": "0.13.11",
70
+ "indusagi": "0.13.12",
68
71
  "ink": "^5.2.1",
69
72
  "jiti": "^2.7.0",
70
73
  "marked": "^18.0.4",
71
74
  "react": "^18.3.1",
72
75
  "strip-ansi": "^7.2.0",
76
+ "string-width": "^7.2.0",
73
77
  "ulid": "^3.0.2",
74
78
  "zod": "^4.4.3"
75
79
  },
@@ -78,6 +82,11 @@
78
82
  "@types/react": "^18.3.12",
79
83
  "esbuild": "^0.28.0",
80
84
  "typescript": "^5.7.2",
81
- "vitest": "^2.1.8"
85
+ "vitest": "^4.1.11"
86
+ },
87
+ "overrides": {
88
+ "fast-uri": "3.1.6",
89
+ "hono": "4.13.5",
90
+ "qs": "6.16.0"
82
91
  }
83
92
  }
@@ -20,13 +20,12 @@
20
20
  * launch wiring needs. When the concrete multi-account vault arrives it replaces
21
21
  * this adapter at the one call site in {@link "./boot"}.
22
22
  */
23
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
24
- import { dirname } from "node:path";
25
23
  import {
26
24
  ensureFreshOAuthCredentials,
27
25
  getOAuthProvider,
28
26
  } from "indusagi/ai";
29
27
  import type { AuthVault } from "../launch.js";
28
+ import { atomicWritePrivateFileSync, readPrivateFileSync } from "../security/private-state.js";
30
29
 
31
30
  interface OAuthRecord {
32
31
  access: string;
@@ -68,8 +67,9 @@ function normaliseRecord(value: unknown): NormalisedRecord | undefined {
68
67
  }
69
68
 
70
69
  function readAuthFile(filePath: string): Record<string, Record<string, NormalisedRecord>> {
70
+ const text = readPrivateFileSync(filePath);
71
+ if (text === null) return {};
71
72
  try {
72
- const text = readFileSync(filePath, "utf8");
73
73
  const parsed = JSON.parse(text);
74
74
  if (parsed === null || typeof parsed !== "object") return {};
75
75
  const file: Record<string, Record<string, NormalisedRecord>> = {};
@@ -92,8 +92,7 @@ function writeAuthFile(
92
92
  filePath: string,
93
93
  data: Record<string, Record<string, NormalisedRecord>>,
94
94
  ): void {
95
- mkdirSync(dirname(filePath), { recursive: true });
96
- writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, { mode: 384 });
95
+ atomicWritePrivateFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
97
96
  }
98
97
 
99
98
  function toCredentials(record: OAuthStoredRecord): OAuthRecord {