indusagi-coding-agent 0.2.12 → 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/.env.review.example +25 -0
- package/README.md +30 -0
- package/REVIEW_AGENT.md +137 -0
- package/dist/entry.js +1861 -979
- package/dist/guardrails.js +212 -36
- package/dist/index.js +2829 -1057
- package/dist/review-agent.js +4461 -0
- package/dist/review-server.js +4633 -0
- package/package.json +24 -7
- package/src/boot/auth-vault.ts +4 -5
- package/src/boot/contract.ts +1 -1
- 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 +26 -12
- package/src/boot/server-token.ts +20 -11
- package/src/boot/stages.ts +3 -6
- package/src/briefing/context-docs.ts +144 -12
- package/src/channels/oneshot.ts +6 -0
- package/src/conductor/catalog/catalog.ts +3 -1
- package/src/conductor/conductor.ts +77 -17
- package/src/conductor/contract.ts +9 -7
- package/src/conductor/index.ts +1 -1
- 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 +4 -4
- package/src/console/test-support.ts +48 -0
- package/src/index.ts +1 -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/catalog.ts +4 -2
- package/src/launch/contract.ts +1 -0
- package/src/launch/invocation/flags.ts +13 -1
- package/src/launch/invocation/read.ts +4 -3
- package/src/launch/login.ts +23 -10
- package/src/launch/oauth.ts +3 -0
- package/src/models.ts +166 -0
- package/src/review-agent/config.ts +75 -0
- package/src/review-agent/contract.ts +59 -0
- package/src/review-agent/github.ts +242 -0
- package/src/review-agent/index.ts +8 -0
- package/src/review-agent/prompt.ts +132 -0
- package/src/review-agent/reviewer.ts +118 -0
- package/src/review-agent/server.ts +221 -0
- package/src/review-agent/service.ts +165 -0
- package/src/review-agent/supabase.ts +209 -0
- package/src/review-agent/webhook.ts +76 -0
- 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/supabase/migrations/20260827031803_indusagi_review_agent.sql +109 -0
- package/supabase/migrations/20260827035747_atomic_webhook_enqueue.sql +75 -0
- package/supabase/migrations/20260827040104_check_run_reconciliation.sql +8 -0
- package/supabase/migrations/20260827041049_owned_job_leases.sql +80 -0
- package/supabase/migrations/20260827042500_static_token_webhooks.sql +75 -0
package/src/console/reducer.ts
CHANGED
|
@@ -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
|
|
42
|
-
|
|
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
|
|
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:
|
|
129
|
+
return { ...state, caret: moveGrapheme(state.buffer, state.caret, event.delta) };
|
|
123
130
|
case "caret:jump":
|
|
124
|
-
return { ...state, caret:
|
|
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:
|
|
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:
|
|
40
|
+
const templates: Macro[] = [];
|
|
40
41
|
const claimed = new Set<string>();
|
|
41
42
|
for (const root of roots.macros) {
|
|
42
|
-
let loaded:
|
|
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):
|
|
70
|
-
|
|
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
|
|
547
|
+
async function hasLocalProviderKey(provider: string, ctx: SlashContext) {
|
|
548
|
+
if (!provider) return false;
|
|
546
549
|
try {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
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()
|
|
@@ -147,7 +147,7 @@ const HOTKEYS: Array<[string, string]> = [
|
|
|
147
147
|
["Tab", "Accept the highlighted completion"],
|
|
148
148
|
["Ctrl+C", "Interrupt the running turn"],
|
|
149
149
|
["Ctrl+T", "Toggle reasoning / thinking rows"],
|
|
150
|
-
["/thinking", "Cycle
|
|
150
|
+
["/thinking", "Cycle the selected model's supported reasoning efforts"],
|
|
151
151
|
["Ctrl+V", "Attach an image from the clipboard"],
|
|
152
152
|
["Ctrl+G", "Edit the composer in your external editor"],
|
|
153
153
|
["Ctrl+O", "Expand or collapse full tool output"],
|
|
@@ -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
|
{
|
|
@@ -232,7 +232,7 @@ export const workbenchCommands: SlashCommand[] = [
|
|
|
232
232
|
},
|
|
233
233
|
{
|
|
234
234
|
name: "thinking",
|
|
235
|
-
summary: "Cycle
|
|
235
|
+
summary: "Cycle reasoning effort using the selected model's supported levels",
|
|
236
236
|
run: (ctx) => {
|
|
237
237
|
ctx.conductor.cycleThinkingLevel();
|
|
238
238
|
return HANDLED;
|
|
@@ -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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -15,4 +15,5 @@ export * as addons from "./addons/index.js";
|
|
|
15
15
|
export * as channels from "./channels/index.js";
|
|
16
16
|
export * as runtimeBridge from "./runtime-bridge/index.js";
|
|
17
17
|
export * as insight from "./insight/index.js";
|
|
18
|
+
export * as reviewAgent from "./review-agent/index.js";
|
|
18
19
|
export { VERSION } from "./workspace/index.js";
|
|
@@ -12,20 +12,22 @@
|
|
|
12
12
|
* prefer env / CLI to avoid committing secrets.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { existsSync, readFileSync
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/src/launch/catalog.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { ModelRegistry, type KnownProvider, type Model } from "indusagi/ai";
|
|
5
5
|
import type { CatalogFilter } from "./contract.js";
|
|
6
|
+
import { applyCodingAgentModelOverlay } from "../models.js";
|
|
6
7
|
|
|
7
8
|
export interface CatalogIo {
|
|
8
9
|
print(line: string): void;
|
|
@@ -22,10 +23,11 @@ export interface CatalogModelSource {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function registrySource(registry: ModelRegistry = new ModelRegistry()): CatalogModelSource {
|
|
26
|
+
const overlaid = applyCodingAgentModelOverlay(registry);
|
|
25
27
|
return {
|
|
26
|
-
providers: (): readonly (KnownProvider | string)[] =>
|
|
28
|
+
providers: (): readonly (KnownProvider | string)[] => overlaid.getProviders(),
|
|
27
29
|
models: (provider: string): readonly Model<Model<string>["api"]>[] =>
|
|
28
|
-
|
|
30
|
+
overlaid.getModels(provider),
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
package/src/launch/contract.ts
CHANGED
|
@@ -20,7 +20,7 @@ export const FLAG_GROUPS: readonly { readonly id: FlagGroup; readonly title: str
|
|
|
20
20
|
{ id: "meta", title: "Meta" },
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
-
const THINKING_CHOICES = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
23
|
+
const THINKING_CHOICES = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
24
24
|
|
|
25
25
|
export const FLAG_SPECS: readonly GroupedFlagSpec[] = [
|
|
26
26
|
/* ---- Output ---------------------------------------------------------- */
|
|
@@ -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",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isToolName,
|
|
10
10
|
} from "../contract.js";
|
|
11
11
|
import { FLAG_SPECS, type GroupedFlagSpec } from "./flags.js";
|
|
12
|
+
import { isReasoningEffortSupported } from "../../models.js";
|
|
12
13
|
|
|
13
14
|
interface ParseState {
|
|
14
15
|
flags: Record<string, FlagValue>;
|
|
@@ -159,9 +160,9 @@ function deriveMode(flags: Record<string, FlagValue>): Invocation["mode"] {
|
|
|
159
160
|
return "text";
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
function resolveThinking(flags: Record<string, FlagValue
|
|
163
|
+
function resolveThinking(flags: Record<string, FlagValue>, modelId: string | undefined): Invocation["thinking"] {
|
|
163
164
|
const raw = readString(flags, "thinking");
|
|
164
|
-
return raw !== undefined && isThinkingEffort(raw) ? raw : undefined;
|
|
165
|
+
return raw !== undefined && isThinkingEffort(raw) && isReasoningEffortSupported(modelId, raw) ? raw : undefined;
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
function resolveTools(flags: Record<string, FlagValue>): Invocation["tools"] {
|
|
@@ -215,7 +216,7 @@ export function readInvocation(argv: readonly string[]): Invocation {
|
|
|
215
216
|
const cwd = readString(flags, "cwd");
|
|
216
217
|
const system = readString(flags, "system");
|
|
217
218
|
const appendSystem = readString(flags, "append-system");
|
|
218
|
-
const thinking = resolveThinking(flags);
|
|
219
|
+
const thinking = resolveThinking(flags, model);
|
|
219
220
|
const tools = resolveTools(flags);
|
|
220
221
|
return {
|
|
221
222
|
mode,
|
package/src/launch/login.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/launch/oauth.ts
CHANGED
|
@@ -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 };
|