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
@@ -11,6 +11,8 @@
11
11
  */
12
12
  import { EMPTY_CONSOLE_STATE, NO_MODAL, transitionModal, type ConsoleEvent, type ConsoleState } from "./contract.js";
13
13
 
14
+ import { previousGrapheme, nextGrapheme, moveGrapheme, snapGrapheme } from "./input/graphemes.js";
15
+
14
16
  function clamp(offset: number, max: number) {
15
17
  if (offset < 0) return 0;
16
18
  if (offset > max) return max;
@@ -38,14 +40,15 @@ function insertBuffer(state: ConsoleState, text: string) {
38
40
  function erasePrev(state: ConsoleState) {
39
41
  const at = clamp(state.caret, state.buffer.length);
40
42
  if (at === 0) return state;
41
- const next = state.buffer.slice(0, at - 1) + state.buffer.slice(at);
42
- return withBuffer(state, next, at - 1);
43
+ const previous = previousGrapheme(state.buffer, at);
44
+ const next = state.buffer.slice(0, previous) + state.buffer.slice(at);
45
+ return withBuffer(state, next, previous);
43
46
  }
44
47
 
45
48
  function eraseNext(state: ConsoleState) {
46
49
  const at = clamp(state.caret, state.buffer.length);
47
50
  if (at >= state.buffer.length) return state;
48
- const next = state.buffer.slice(0, at) + state.buffer.slice(at + 1);
51
+ const next = state.buffer.slice(0, at) + state.buffer.slice(nextGrapheme(state.buffer, at));
49
52
  return withBuffer(state, next, at);
50
53
  }
51
54
 
@@ -97,6 +100,10 @@ function patchRow(state: ConsoleState, id: string, text: string) {
97
100
  */
98
101
  export function consoleReducer(state: ConsoleState, event: ConsoleEvent): ConsoleState {
99
102
  switch (event.type) {
103
+ case "view:reset":
104
+ return { ...state, rows: [], blocks: [], buffer: "", caret: 0, historyAt: null, stash: "", modal: NO_MODAL, busy: false };
105
+ case "display:set":
106
+ return { ...state, [event.key]: event.value };
100
107
  // Composer buffer & caret
101
108
  case "buffer:set":
102
109
  return setBuffer(state, event.text);
@@ -119,9 +126,9 @@ export function consoleReducer(state: ConsoleState, event: ConsoleEvent): Consol
119
126
  case "buffer:eraseNext":
120
127
  return eraseNext(state);
121
128
  case "caret:move":
122
- return { ...state, caret: clamp(state.caret + event.delta, state.buffer.length) };
129
+ return { ...state, caret: moveGrapheme(state.buffer, state.caret, event.delta) };
123
130
  case "caret:jump":
124
- return { ...state, caret: clamp(event.offset, state.buffer.length) };
131
+ return { ...state, caret: snapGrapheme(state.buffer, event.offset) };
125
132
  // History ring
126
133
  case "history:push":
127
134
  return pushHistory(state, event.text);
@@ -4,6 +4,7 @@
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { gatherSkillCards, loadMacros } from "../../briefing/index.js";
7
+ import type { SkillCard, Macro } from "../../briefing/index.js";
7
8
  import type { SlashCommand } from "../contract.js";
8
9
  import { FAMILY } from "./commands/shared.js";
9
10
  import { buildDynamicCommands } from "./commands/dynamic.js";
@@ -30,16 +31,16 @@ function dynamicRoots() {
30
31
 
31
32
  function discoverDynamicSources() {
32
33
  const roots = dynamicRoots();
33
- let skills: Array<{ name: string; location: string }> = [];
34
+ let skills: SkillCard[] = [];
34
35
  try {
35
36
  skills = gatherSkillCards(roots.skills).cards.slice();
36
37
  } catch {
37
38
  skills = [];
38
39
  }
39
- const templates: Array<{ name: string; source: string }> = [];
40
+ const templates: Macro[] = [];
40
41
  const claimed = new Set<string>();
41
42
  for (const root of roots.macros) {
42
- let loaded: Array<{ name: string; source: string }> = [];
43
+ let loaded: Macro[] = [];
43
44
  try {
44
45
  loaded = loadMacros(root.dir, { origin: root.origin, label: root.label });
45
46
  } catch {
@@ -3,6 +3,7 @@
3
3
  * clipboard / export / share I/O verbs.
4
4
  */
5
5
  import { spawnSync } from "node:child_process";
6
+ import { getEnvApiKey } from "indusagi/ai";
6
7
  import { readFileSync, writeFileSync } from "node:fs";
7
8
  import { homedir } from "node:os";
8
9
  import { join, resolve } from "node:path";
@@ -10,6 +11,7 @@ import { createMCPAgentToolFactory, loadMCPConfig, MCPClientPool } from "indusag
10
11
  import { createComposioGateway } from "indusagi/connectors-saas";
11
12
  import type { SlashCommand, SlashContext } from "../../contract.js";
12
13
  import { FAMILY, HANDLED, familyRunner, info, warn, splitVerb } from "./shared.js";
14
+ import type { TranscriptEntry } from "../../../transcript-export/publish.js";
13
15
  import { publishTranscript } from "../../../transcript-export/index.js";
14
16
  import { APP_NOVEL_CARDS, memoryCard } from "../../../capability-deck/index.js";
15
17
  import { getServerToken, hasValidServerToken, INDUS_SERVER_URL } from "../../../boot/server-token.js";
@@ -48,7 +50,7 @@ function writeClipboard(text: string): boolean {
48
50
  return false;
49
51
  }
50
52
 
51
- function lastAssistantText(messages: any[]): string {
53
+ function lastAssistantText(messages: readonly any[]): string {
52
54
  for (let i = messages.length - 1; i >= 0; i--) {
53
55
  const message = messages[i];
54
56
  if (message.role !== "assistant") continue;
@@ -66,11 +68,12 @@ function lastAssistantText(messages: any[]): string {
66
68
  return "";
67
69
  }
68
70
 
69
- function toPublishRole(role: string): string {
70
- return PUBLISH_ROLES.has(role) ? role : "note";
71
+ function toPublishRole(role: string): TranscriptEntry["role"] {
72
+ if (role === "toolResult") return "tool";
73
+ return PUBLISH_ROLES.has(role) ? role as TranscriptEntry["role"] : "note";
71
74
  }
72
75
 
73
- function toPublishEntries(messages: any[]) {
76
+ function toPublishEntries(messages: readonly any[]) {
74
77
  return messages.map((message) => {
75
78
  const m = message;
76
79
  return {
@@ -541,16 +544,12 @@ function runLogin(ctx: SlashContext) {
541
544
  return HANDLED;
542
545
  }
543
546
 
544
- function hasLocalProviderKey(provider: string) {
545
- if (provider.length === 0) return false;
547
+ async function hasLocalProviderKey(provider: string, ctx: SlashContext) {
548
+ if (!provider) return false;
546
549
  try {
547
- const file = join(homedir(), ".indusagi", "agent", "auth.json");
548
- const vault = JSON.parse(readFileSync(file, "utf8"));
549
- const entry = vault[provider];
550
- return entry !== null && typeof entry === "object" && Object.keys(entry).length > 0;
551
- } catch {
552
- return false;
553
- }
550
+ if (getEnvApiKey(provider)) return true;
551
+ return (await ctx.services?.vault.listAccounts(provider) ?? []).length > 0;
552
+ } catch { return false; }
554
553
  }
555
554
 
556
555
  function runStatus(ctx: SlashContext) {
@@ -564,7 +563,7 @@ function runStatus(ctx: SlashContext) {
564
563
  signedIn = false;
565
564
  }
566
565
  const eligible = isGatewayEligibleProvider(provider);
567
- const localKey = hasLocalProviderKey(provider);
566
+ const localKey = await hasLocalProviderKey(provider, ctx);
568
567
  const viaServer = signedIn && eligible && !localKey;
569
568
  if (viaServer) {
570
569
  ctx.setStatus(info(`● server tier — ${modelId} routing via ${INDUS_SERVER_URL} (signed in)`));
@@ -69,12 +69,7 @@ const compactCommand: SlashCommand = {
69
69
  ctx.setStatus({ kind: "busy", text: busyText });
70
70
  void (async () => {
71
71
  try {
72
- if (instructions.length > 0) {
73
- await ctx.conductor.executeBash(
74
- `printf '%s' ${JSON.stringify(`Compaction guidance: ${instructions}`)}`
75
- );
76
- }
77
- switch (await ctx.conductor.condense()) {
72
+ switch (await ctx.conductor.condense(instructions || undefined)) {
78
73
  case "condensed":
79
74
  ctx.setStatus(
80
75
  info(
@@ -123,7 +118,7 @@ const sessionCommand: SlashCommand = {
123
118
  const name = ctx.conductor.sessionName();
124
119
  const block = {
125
120
  id: `session-stats-${Date.now().toString(36)}`,
126
- kind: "changelog",
121
+ kind: "changelog" as const,
127
122
  title: name ? `Session: ${name}` : "Session statistics",
128
123
  markdown: statsMarkdown(stats, name),
129
124
  timestamp: Date.now()
@@ -155,7 +150,7 @@ const costCommand: SlashCommand = {
155
150
  ${rows.map(([k, v]) => `| ${k} | ${v} |`).join("\n")}`;
156
151
  const block = {
157
152
  id: `session-cost-${Date.now().toString(36)}`,
158
- kind: "changelog",
153
+ kind: "changelog" as const,
159
154
  title: "Session cost",
160
155
  markdown,
161
156
  timestamp: Date.now()
@@ -11,7 +11,7 @@ import { FAMILY, HANDLED, familyRunner, info, warn } from "./shared.js";
11
11
  function block(slug: string, title: string, markdown: string) {
12
12
  return {
13
13
  id: `${slug}-${Date.now().toString(36)}`,
14
- kind: "changelog",
14
+ kind: "changelog" as const,
15
15
  title,
16
16
  markdown,
17
17
  timestamp: Date.now()
@@ -222,7 +222,7 @@ export const workbenchCommands: SlashCommand[] = [
222
222
  {
223
223
  name: "whats-new",
224
224
  summary: "Show what changed in this build",
225
- aliases: ["changelog"],
225
+ aliases: ["changelog", "release-notes"],
226
226
  run: runChangelog
227
227
  },
228
228
  {
@@ -0,0 +1,48 @@
1
+ import { Writable, PassThrough } from 'node:stream';
2
+ import React from 'indusagi/react-host';
3
+ import { render } from 'indusagi/react-host/ink';
4
+ import stripAnsi from 'strip-ansi';
5
+ import { TerminalConsole } from './components/TerminalConsole.js';
6
+ import { resolveTheme } from './theme/resolve.js';
7
+ import { DEFAULT_SLASH_REGISTRY } from './slash/index.js';
8
+ import { DEFAULT_PREFERENCES } from '../settings/contract.js';
9
+ export const settle = (ms=55) => new Promise<void>(r=>setTimeout(r,ms));
10
+ export function fixtureConductor() {
11
+ let handler=(signal:any)=>{}, phase='idle', messages:any[]=[], queue:any[]=[], sessionId='fixture';
12
+ const model={id:'test-model',provider:'fixture',contextWindow:200000,name:'Fixture',reasoning:true};
13
+ const submissions:string[]=[];
14
+ const c:any={snapshot:()=>({phase,contextTokens:0,head:{sessionId,leaf:null},modelId:'fixture/test-model'}),model:()=>model,
15
+ stats:()=>({sessionId,userMessages:0,assistantMessages:0,toolCalls:0,toolResults:0,totalMessages:messages.length,tokens:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0},cost:0}),
16
+ pendingCount:()=>queue.length,pendingInputs:()=>queue,thinkingLevel:()=> 'off',sessionName:()=>undefined,availableModels:()=>[model],messages:()=>messages,isBusy:()=>phase!=='idle',
17
+ permissionMode:()=> 'default',setApprovalResolver:(r:any)=>{c.approvalResolver=r},subscribe:(fn:any)=>{handler=fn;return()=>{handler=()=>{}}},
18
+ submit:async(text:string)=>{submissions.push(text);if(phase==='streaming'){queue.push({text,mode:'followUp'});handler({kind:'queue',count:queue.length})}else{phase='streaming'}},
19
+ emit:(sig:any)=>handler(sig),setMessages:(m:any[])=>{messages=m},setPhase:(p:string)=>{phase=p},submissions,
20
+ abort:()=>{c.aborted=(c.aborted??0)+1;phase='idle';handler({kind:'idle'})},
21
+ cyclePermissionMode:()=> 'acceptEdits',cycleThinkingLevel:()=> 'low',dequeueLast:()=>queue.pop()?.text,steerNow:()=>false,
22
+ newSession:async()=>{messages=[];phase='idle';sessionId+='-new';handler({kind:'idle'})},executeBash:async()=>({exitCode:0,output:'SHELL_SENTINEL'})};
23
+ return c;
24
+ }
25
+ export function fixtureServices(c:any, overrides:any={}) {
26
+ const values:any={...DEFAULT_PREFERENCES,quietStartup:true,lastSeenVersion:'0.2.14',defaultProvider:'fixture',...overrides};
27
+ return {conductor:c,settings:{get:(k:string)=>values[k],set:(k:string,v:any)=>{values[k]=v},save:()=>{}},vault:{defaultAccount:async()=>undefined,listAccounts:async()=>['fixture-account']},listLoginProviders:()=>[{id:'fixture',label:'Fixture',authKind:'apiKey'}],values};
28
+ }
29
+ export async function renderTest(element:any,columns=80,rows=24) {
30
+ const frames:string[]=[];
31
+ const stdout=new Writable({write(chunk,_enc,cb){frames.push(String(chunk));cb()}});
32
+ Object.assign(stdout,{columns,rows,isTTY:true});
33
+ const stdin=new PassThrough();Object.assign(stdin,{isTTY:true,setRawMode:()=>{},ref:()=>{},unref:()=>{}});
34
+ const app=render(element,{stdout:stdout as unknown as NodeJS.WriteStream,stdin:stdin as unknown as NodeJS.ReadStream,stderr:stdout as unknown as NodeJS.WriteStream,debug:true,patchConsole:false,exitOnCtrlC:false});
35
+ let exited=false;void app.waitUntilExit().then(()=>{exited=true});
36
+ await settle(80);
37
+ return {app,frames,stdout,stdin,exited:()=>exited,
38
+ last:()=>stripAnsi(frames.filter(f=>f.length>10).at(-1)||''),raw:()=>frames.filter(f=>f.length>10).at(-1)||'',
39
+ key:async(text:string)=>{stdin.write(text);await settle()},
40
+ resize:async(cols:number,r:number)=>{Object.assign(stdout,{columns:cols,rows:r});stdout.emit('resize');await settle()},
41
+ close:()=>{app.unmount();app.cleanup();stdin.destroy();stdout.destroy()}};
42
+ }
43
+ export async function consoleTest(options:any={}) {
44
+ const c=options.conductor??fixtureConductor();
45
+ const svc=options.services===false?undefined:(options.services??fixtureServices(c,options.settings));
46
+ const view=await renderTest(React.createElement(TerminalConsole,{conductor:c,theme:resolveTheme('saffron'),slash:DEFAULT_SLASH_REGISTRY,services:svc,...options.props}),options.columns??80,options.rows??24);
47
+ return {...view,c,svc};
48
+ }
@@ -12,20 +12,22 @@
12
12
  * prefer env / CLI to avoid committing secrets.
13
13
  */
14
14
 
15
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
15
+ import { existsSync, readFileSync } from "node:fs";
16
16
  import { homedir } from "node:os";
17
- import { dirname, join } from "node:path";
17
+ import { join } from "node:path";
18
18
  import type { SarvamHostConfig } from "./types.js";
19
+ import { atomicWritePrivateFileSync, readPrivateFileSync } from "../../security/private-state.js";
19
20
 
20
21
  const ENV_KEY = "SARVAM_API_KEY";
21
22
  const ENV_DISABLE = "SARVAM_DISABLED";
22
23
  const ENV_BASE_URL = "SARVAM_API_BASE_URL";
23
24
  const ENV_BASE_PATH = "SARVAM_MCP_BASE_PATH";
24
25
 
25
- function readJsonFile(path: string): Record<string, unknown> | undefined {
26
+ function readJsonFile(path: string, sensitive = false): Record<string, unknown> | undefined {
26
27
  try {
27
- if (!existsSync(path)) return undefined;
28
- const raw = readFileSync(path, "utf8");
28
+ if (!sensitive && !existsSync(path)) return undefined;
29
+ const raw = sensitive ? readPrivateFileSync(path) : readFileSync(path, "utf8");
30
+ if (raw === null) return undefined;
29
31
  const parsed = JSON.parse(raw) as unknown;
30
32
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
31
33
  return undefined;
@@ -36,11 +38,11 @@ function readJsonFile(path: string): Record<string, unknown> | undefined {
36
38
  }
37
39
  }
38
40
 
39
- function configFromRecord(rec: Record<string, unknown> | undefined): SarvamHostConfig | undefined {
41
+ function configFromRecord(rec: Record<string, unknown> | undefined, includeApiKey = true): SarvamHostConfig | undefined {
40
42
  if (!rec) return undefined;
41
43
  const enabled = rec.enabled === false ? false : true;
42
44
  const id = typeof rec.id === "string" && rec.id.trim() ? rec.id.trim() : "sarvam";
43
- const apiKey = typeof rec.apiKey === "string" ? rec.apiKey.trim() : undefined;
45
+ const apiKey = includeApiKey && typeof rec.apiKey === "string" ? rec.apiKey.trim() : undefined;
44
46
  const baseUrl = typeof rec.baseUrl === "string" ? rec.baseUrl.trim() : undefined;
45
47
  const basePath = typeof rec.basePath === "string" ? rec.basePath.trim() : undefined;
46
48
  const command = typeof rec.command === "string" ? rec.command.trim() : undefined;
@@ -73,9 +75,9 @@ export function saveSarvamConfig(
73
75
  config: SarvamHostConfig,
74
76
  where: "profile" | "project" = "profile",
75
77
  cwd = process.cwd(),
78
+ home = homedir(),
76
79
  ): string {
77
- const path = where === "project" ? projectSarvamPath(cwd) : profileSarvamPath();
78
- mkdirSync(dirname(path), { recursive: true });
80
+ const path = where === "project" ? projectSarvamPath(cwd) : profileSarvamPath(home);
79
81
  const body: Record<string, unknown> = {
80
82
  id: config.id ?? "sarvam",
81
83
  enabled: config.enabled !== false,
@@ -84,8 +86,8 @@ export function saveSarvamConfig(
84
86
  if (config.baseUrl) body.baseUrl = config.baseUrl;
85
87
  if (config.command) body.command = config.command;
86
88
  // Only persist key if caller intentionally set it (e.g. interactive connect with key arg).
87
- if (config.apiKey) body.apiKey = config.apiKey;
88
- writeFileSync(path, JSON.stringify(body, null, 2) + "\n", "utf8");
89
+ if (where === "profile" && config.apiKey) body.apiKey = config.apiKey;
90
+ atomicWritePrivateFileSync(path, JSON.stringify(body, null, 2) + "\n");
89
91
  return path;
90
92
  }
91
93
 
@@ -118,9 +120,9 @@ export function resolveSarvamConfig(opts: ResolveSarvamOptions = {}): SarvamHost
118
120
 
119
121
  const fromArgKey = opts.apiKey?.trim();
120
122
  const fromEnvKey = env[ENV_KEY]?.trim();
121
- const fromProject = configFromRecord(readJsonFile(projectSarvamPath(cwd)));
123
+ const fromProject = configFromRecord(readJsonFile(projectSarvamPath(cwd)), false);
122
124
  const fromProfile = configFromRecord(
123
- readJsonFile(profileSarvamPath(opts.home ?? homedir())),
125
+ readJsonFile(profileSarvamPath(opts.home ?? homedir()), true),
124
126
  );
125
127
 
126
128
  const fileBase = fromProject ?? fromProfile;
@@ -9,7 +9,13 @@ import { join } from "node:path";
9
9
 
10
10
  const FALLBACK_EDITOR = "vi";
11
11
 
12
+ export interface TerminalHandoff {
13
+ stdin: { isRaw?: boolean; setRawMode?: (enabled: boolean) => unknown };
14
+ stdout: { isTTY?: boolean; write: (text: string) => unknown };
15
+ }
16
+
12
17
  export interface ExternalEditorOptions {
18
+ readonly terminal?: TerminalHandoff;
13
19
  readonly env?: NodeJS.ProcessEnv;
14
20
  readonly dir?: string;
15
21
  }
@@ -33,7 +39,17 @@ export function openInExternalEditor(
33
39
  const [editor, ...editorArgs] = resolveEditor(env);
34
40
  try {
35
41
  writeFileSync(path, buffer, "utf8");
36
- const result = spawnSync(editor, [...editorArgs, path], { stdio: "inherit" });
42
+ const terminal = options.terminal ?? {stdin:process.stdin,stdout:process.stdout};
43
+ const wasRaw = terminal.stdin.isRaw === true;
44
+ let result: ReturnType<typeof spawnSync>;
45
+ try {
46
+ terminal.stdin.setRawMode?.(false);
47
+ if (terminal.stdout.isTTY) terminal.stdout.write("\x1b[?1049h\x1b[?25h");
48
+ result = spawnSync(editor, [...editorArgs, path], { stdio: "inherit" });
49
+ } finally {
50
+ if (terminal.stdout.isTTY) terminal.stdout.write("\x1b[?1049l\x1b[?25l");
51
+ terminal.stdin.setRawMode?.(wasRaw);
52
+ }
37
53
  if (result.error || result.status !== 0) return null;
38
54
  return readFileSync(path, "utf8").replace(/\n$/, "");
39
55
  } catch {
@@ -0,0 +1,31 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ /** Execute an explicitly requested shell command with bounded output and cancellation. */
4
+ export function runShell(command: string, cwd: string, signal?: AbortSignal): Promise<{output:string;exitCode:number}> {
5
+ if (signal?.aborted) return Promise.resolve({output:'Command cancelled.',exitCode:130});
6
+ return new Promise(resolve => {
7
+ let output='', truncated=false, settled=false;
8
+ let forceKill:ReturnType<typeof setTimeout> | undefined;
9
+ const child=spawn(command,{cwd,shell:true,detached:process.platform!=='win32'});
10
+ const capture=(chunk:Buffer|string)=>{
11
+ output+=String(chunk);
12
+ if (output.length>1024*1024) {output=output.slice(-1024*1024);truncated=true;}
13
+ };
14
+ const kill=(signalName:NodeJS.Signals)=>{
15
+ try { if (process.platform!=='win32' && child.pid) process.kill(-child.pid,signalName); else child.kill(signalName); }
16
+ catch { try { child.kill(signalName); } catch { /* already exited */ } }
17
+ };
18
+ const abort=()=>{kill('SIGTERM');forceKill=setTimeout(()=>kill('SIGKILL'),1000);forceKill.unref();};
19
+ const finish=(exitCode:number)=>{
20
+ if (settled) return;settled=true;
21
+ signal?.removeEventListener('abort',abort);
22
+ if (forceKill) clearTimeout(forceKill);
23
+ resolve({output:(truncated?'[older output truncated]\n':'')+output,exitCode:signal?.aborted?130:exitCode});
24
+ };
25
+ child.stdout?.on('data',capture);child.stderr?.on('data',capture);
26
+ child.once('error',error=>{capture(error.message);finish(1)});
27
+ child.once('close',code=>finish(code??1));
28
+ signal?.addEventListener('abort',abort,{once:true});
29
+ if (signal?.aborted) abort();
30
+ });
31
+ }
@@ -110,6 +110,18 @@ export const FLAG_SPECS: readonly GroupedFlagSpec[] = [
110
110
  group: "context",
111
111
  describe: "Continue the most recent session in this directory.",
112
112
  },
113
+ {
114
+ name: "--trust-project-addons",
115
+ kind: "boolean",
116
+ group: "context",
117
+ describe: "Trust executable project addons for the canonical workspace and their current content digest.",
118
+ },
119
+ {
120
+ name: "--allow-context-import",
121
+ kind: "list",
122
+ group: "context",
123
+ describe: "Explicitly approve an exact external context file (comma-separated or repeated). Secret-like files remain blocked.",
124
+ },
113
125
  /* ---- Tools ----------------------------------------------------------- */
114
126
  {
115
127
  name: "--tools",
@@ -6,6 +6,8 @@
6
6
  * model turns through the server gateway.
7
7
  */
8
8
  import { createInterface } from "node:readline/promises";
9
+ import { setTimeout as delay } from "node:timers/promises";
10
+ import type { ServerTokenInput } from "../boot/server-token.js";
9
11
  import chalk from "chalk";
10
12
  import type { OAuthLoginCallbacks } from "indusagi/ai";
11
13
  import type { BootContext } from "../boot/contract.js";
@@ -37,11 +39,15 @@ function err(line: string): void {
37
39
  process.stderr.write(`${line}\n`);
38
40
  }
39
41
 
40
- function sleep(ms: number): Promise<void> {
41
- return new Promise((resolve) => setTimeout(resolve, ms));
42
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
43
+ return delay(ms, undefined, {signal});
42
44
  }
43
45
 
44
46
  interface DeviceCodeResponse {
47
+ access_token?: string;
48
+ refresh_token?: string;
49
+ token_type?: string;
50
+ scope?: string;
45
51
  device_code?: string;
46
52
  user_code?: string;
47
53
  verification_uri?: string;
@@ -56,6 +62,7 @@ interface DeviceCodeResponse {
56
62
  async function postJson(
57
63
  url: string,
58
64
  body: Record<string, unknown>,
65
+ signal?: AbortSignal,
59
66
  ): Promise<{ status: number; data: DeviceCodeResponse }> {
60
67
  const res = await fetch(url, {
61
68
  method: "POST",
@@ -65,6 +72,7 @@ async function postJson(
65
72
  "user-agent": USER_AGENT,
66
73
  },
67
74
  body: JSON.stringify(body),
75
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(30000)]) : AbortSignal.timeout(30000),
68
76
  });
69
77
  const data = (await res.json().catch(() => ({}))) as DeviceCodeResponse;
70
78
  return { status: res.status, data };
@@ -112,11 +120,12 @@ async function confirm(prompt: string, defaultValue: boolean): Promise<boolean>
112
120
  async function requestDeviceCode(
113
121
  serverUrl: string,
114
122
  clientId: string,
123
+ signal?: AbortSignal,
115
124
  ): Promise<DeviceCodeResponse> {
116
125
  const { status, data } = await postJson(`${serverUrl}/api/auth/device/code`, {
117
126
  client_id: clientId,
118
127
  scope: DEVICE_SCOPE,
119
- });
128
+ }, signal);
120
129
  if (status >= 400 || typeof data.device_code !== "string") {
121
130
  throw new LoginError(
122
131
  `Authorization request failed: ${data.error_description ?? data.error ?? data.message ?? `HTTP ${status}`}`,
@@ -129,18 +138,19 @@ async function pollForToken(
129
138
  serverUrl: string,
130
139
  clientId: string,
131
140
  device: DeviceCodeResponse,
132
- ): Promise<DeviceCodeResponse> {
141
+ signal?: AbortSignal,
142
+ ): Promise<ServerTokenInput> {
133
143
  let intervalMs = Math.max(1, device.interval ?? 5) * 1000;
134
144
  const deadline = Date.now() + (device.expires_in ?? 0) * 1000;
135
145
  while (Date.now() < deadline) {
136
- await sleep(intervalMs);
146
+ await sleep(intervalMs, signal);
137
147
  const { data } = await postJson(`${serverUrl}/api/auth/device/token`, {
138
148
  grant_type: DEVICE_GRANT_TYPE,
139
149
  device_code: device.device_code,
140
150
  client_id: clientId,
141
- });
151
+ }, signal);
142
152
  if (typeof data.access_token === "string") {
143
- return data;
153
+ return {...data, access_token:data.access_token};
144
154
  }
145
155
  const code = data.error;
146
156
  switch (code) {
@@ -172,14 +182,17 @@ export async function runServerTierLogin(
172
182
  serverUrl: string = INDUS_SERVER_URL,
173
183
  clientId: string = INDUS_CLIENT_ID,
174
184
  ): Promise<void> {
175
- const device = await requestDeviceCode(serverUrl, clientId);
185
+ callbacks.signal?.throwIfAborted();
186
+ const device = await requestDeviceCode(serverUrl, clientId, callbacks.signal);
187
+ callbacks.signal?.throwIfAborted();
176
188
  callbacks.onAuth({
177
189
  url: device.verification_uri_complete ?? device.verification_uri ?? "",
178
190
  instructions: `Enter code ${device.user_code} to authorize this device.`,
179
191
  });
180
192
  callbacks.onProgress?.("Waiting for authorization...");
181
- const token = await pollForToken(serverUrl, clientId, device);
182
- await storeServerToken(token);
193
+ const token = await pollForToken(serverUrl, clientId, device, callbacks.signal);
194
+ callbacks.signal?.throwIfAborted();
195
+ if (!await storeServerToken(token)) throw new LoginError("Could not save the server token.");
183
196
  callbacks.onProgress?.("Server sign-in complete.");
184
197
  }
185
198
 
@@ -195,8 +195,11 @@ export async function startOAuthLogin(
195
195
  envVar !== undefined ? `set ${envVar} and restart` : "configure its OAuth client id";
196
196
  throw new Error(`Browser sign-in for "${providerId}" is not configured: ${hint}.`);
197
197
  }
198
+ callbacks.signal?.throwIfAborted();
198
199
  const credentials = await provider.login(callbacks);
200
+ callbacks.signal?.throwIfAborted();
199
201
  const existing = await vault.listAccounts(providerId);
202
+ callbacks.signal?.throwIfAborted();
200
203
  const madeDefault = existing.length === 0;
201
204
  await vault.putOAuth(providerId, account, credentials, madeDefault);
202
205
  return { provider: providerId, account, madeDefault };
package/src/models.ts CHANGED
@@ -43,7 +43,76 @@ const GPT_6_ASTRA_BASE = {
43
43
  },
44
44
  } as const;
45
45
 
46
+ const GPT_6_SOL_BASE = {
47
+ id: "gpt-6-sol",
48
+ name: "GPT-6 Sol",
49
+ api: "openai-responses",
50
+ reasoning: true,
51
+ reasoningEfforts: ["high"],
52
+ input: ["text", "image"] as Array<"text" | "image">,
53
+ cost: {
54
+ input: 10,
55
+ cacheRead: 1,
56
+ cacheWrite: 12.5,
57
+ output: 50,
58
+ },
59
+ contextWindow: 1_050_000,
60
+ maxTokens: 128_000,
61
+ longContextPricing: {
62
+ threshold: 272_000,
63
+ multipliers: {
64
+ input: 2,
65
+ cacheRead: 2,
66
+ cacheWrite: 2,
67
+ output: 1.5,
68
+ },
69
+ },
70
+ } as const;
71
+
72
+ const GPT_6_LUNA_BASE = {
73
+ id: "gpt-6-luna",
74
+ name: "GPT-6 Luna",
75
+ api: "openai-responses",
76
+ reasoning: true,
77
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
78
+ input: ["text", "image"] as Array<"text" | "image">,
79
+ cost: {
80
+ input: 0.1,
81
+ cacheRead: 0.01,
82
+ cacheWrite: 0.125,
83
+ output: 0.5,
84
+ },
85
+ contextWindow: 1_050_000,
86
+ maxTokens: 128_000,
87
+ longContextPricing: {
88
+ threshold: 272_000,
89
+ multipliers: { input: 2, cacheRead: 2, cacheWrite: 2, output: 1.5 },
90
+ },
91
+ } as const;
92
+
46
93
  export const CODING_AGENT_MODELS: readonly CodingAgentModel[] = [
94
+ {
95
+ ...GPT_6_SOL_BASE,
96
+ provider: "openai",
97
+ baseUrl: "https://api.openai.com/v1",
98
+ },
99
+ {
100
+ ...GPT_6_SOL_BASE,
101
+ api: "openai-codex-responses",
102
+ provider: "openai-codex",
103
+ baseUrl: "https://chatgpt.com/backend-api",
104
+ },
105
+ {
106
+ ...GPT_6_LUNA_BASE,
107
+ provider: "openai",
108
+ baseUrl: "https://api.openai.com/v1",
109
+ },
110
+ {
111
+ ...GPT_6_LUNA_BASE,
112
+ api: "openai-codex-responses",
113
+ provider: "openai-codex",
114
+ baseUrl: "https://chatgpt.com/backend-api",
115
+ },
47
116
  {
48
117
  ...GPT_6_ASTRA_BASE,
49
118
  provider: "openai",
@@ -85,6 +154,13 @@ export function createCodingAgentModelRegistry(): ModelRegistry {
85
154
  }
86
155
 
87
156
  export function isReasoningEffortSupported(modelId: string | undefined, effort: string): boolean {
88
- if (modelId?.split("/").at(-1) !== "gpt-6-astra") return true;
89
- return GPT_6_ASTRA_BASE.reasoningEfforts.includes(effort as AstraReasoningLevel);
157
+ const id = modelId?.split("/").at(-1);
158
+ if (id === "gpt-6-astra") {
159
+ return GPT_6_ASTRA_BASE.reasoningEfforts.includes(effort as AstraReasoningLevel);
160
+ }
161
+ if (id === "gpt-6-sol") return effort === "high";
162
+ if (id === "gpt-6-luna") {
163
+ return GPT_6_LUNA_BASE.reasoningEfforts.includes(effort as AstraReasoningLevel);
164
+ }
165
+ return true;
90
166
  }