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.
- package/README.md +19 -0
- package/dist/entry.js +1762 -958
- package/dist/guardrails.js +145 -35
- package/dist/index.js +1810 -1006
- package/dist/review-agent.js +344 -60
- package/dist/review-server.js +346 -62
- package/package.json +12 -3
- package/src/boot/auth-vault.ts +4 -5
- package/src/boot/runners/addon-wiring.ts +111 -3
- package/src/boot/runners/benchmark-integrity.ts +107 -0
- package/src/boot/runners/repl-runner.ts +2 -1
- package/src/boot/runners/session.ts +18 -8
- package/src/boot/server-token.ts +20 -11
- package/src/briefing/context-docs.ts +144 -12
- package/src/channels/oneshot.ts +6 -0
- package/src/conductor/conductor.ts +67 -12
- package/src/conductor/contract.ts +4 -3
- package/src/conductor/transcript-store/store.ts +4 -8
- package/src/console/auth-status.ts +9 -4
- package/src/console/components/AgentsView.ts +2 -2
- package/src/console/components/BackgroundAgents.ts +15 -2
- package/src/console/components/Banner.ts +20 -9
- package/src/console/components/Composer.ts +57 -82
- package/src/console/components/ScrollViewport.ts +46 -0
- package/src/console/components/TerminalConsole.ts +165 -162
- package/src/console/components/WorkingIndicator.ts +6 -5
- package/src/console/components/banner-sweep.ts +5 -1
- package/src/console/components/welcome.ts +3 -3
- package/src/console/contract.ts +10 -5
- package/src/console/index.ts +7 -7
- package/src/console/input/complete.ts +2 -2
- package/src/console/input/dir-reader.ts +3 -1
- package/src/console/input/graphemes.ts +22 -0
- package/src/console/input/keymap.ts +6 -4
- package/src/console/mount.ts +45 -3
- package/src/console/overlays/auth.ts +28 -8
- package/src/console/overlays/boards.ts +13 -40
- package/src/console/overlays/host.ts +10 -0
- package/src/console/overlays/pickers.ts +35 -30
- package/src/console/reducer.ts +12 -5
- package/src/console/slash/builtins.ts +4 -3
- package/src/console/slash/commands/integrations.ts +13 -14
- package/src/console/slash/commands/transcript.ts +3 -8
- package/src/console/slash/commands/workbench.ts +2 -2
- package/src/console/test-support.ts +48 -0
- package/src/integrations/sarvam/config.ts +15 -13
- package/src/kit/external-editor.ts +17 -1
- package/src/kit/run-shell.ts +31 -0
- package/src/launch/invocation/flags.ts +12 -0
- package/src/launch/login.ts +23 -10
- package/src/launch/oauth.ts +3 -0
- package/src/models.ts +78 -2
- package/src/security/private-state.ts +122 -0
- package/src/window-budget/summarize/condense.ts +4 -1
- package/src/workspace/brand.ts +1 -1
- package/src/workspace/locator.ts +2 -2
package/dist/review-server.js
CHANGED
|
@@ -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
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
842
|
-
|
|
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
|
-
|
|
848
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
1409
|
-
if (!
|
|
1410
|
-
const output = await captureOutput(
|
|
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
|
|
1431
|
-
if (!
|
|
1432
|
-
const output = await captureOutput(
|
|
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
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
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)
|
|
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
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
${outcome.output}`.trimEnd(), timestamp: Date.now() }
|
|
1956
|
-
|
|
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
|
|
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 =
|
|
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}-${
|
|
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.
|
|
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.
|
|
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": "^
|
|
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
|
}
|
package/src/boot/auth-vault.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|