indusagi-coding-agent 0.2.10 → 0.2.12

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.12]
4
+
5
+ ### Fixed
6
+ - **One-shot NDJSON routing**: combining `--print --json` now runs one-shot mode with structured signal output instead of selecting the stdin-driven RPC mode.
7
+ - **Structured tool signal contract**: `tool_start` and `tool_end` types now include the arguments, result, and error fields already emitted at runtime.
8
+
9
+ ## [0.2.11]
10
+
11
+ ### Fixed
12
+ - **Fatal startup visibility**: uncaught boot failures now print their actual message and exit with status `1` instead of producing an empty failure log.
13
+
3
14
  ## [0.2.10]
4
15
 
5
16
  ### Fixed
package/dist/entry.js CHANGED
@@ -18,7 +18,7 @@ var BRAND = Object.freeze({
18
18
  envShareViewer: "INDUSAGI_SHARE_VIEWER_URL",
19
19
  shareViewerUrl: "https://buildwithindusagi.ai/session/"
20
20
  });
21
- var VERSION = "0.2.10";
21
+ var VERSION = "0.2.12";
22
22
 
23
23
  // src/launch/credentials.ts
24
24
  import { createInterface as createInterface2 } from "node:readline/promises";
@@ -1441,8 +1441,8 @@ function readBool(flags, key) {
1441
1441
  return flags[key] === true;
1442
1442
  }
1443
1443
  function deriveMode(flags) {
1444
- if (readBool(flags, "json")) return "rpc";
1445
1444
  if (readBool(flags, "print") && !readBool(flags, "interactive")) return "json";
1445
+ if (readBool(flags, "json")) return "rpc";
1446
1446
  return "text";
1447
1447
  }
1448
1448
  function resolveThinking(flags) {
@@ -16812,13 +16812,26 @@ function isProgramEntry() {
16812
16812
  return false;
16813
16813
  }
16814
16814
  }
16815
+ async function resolveBoot(result) {
16816
+ try {
16817
+ return await result;
16818
+ } catch (error) {
16819
+ const message = error instanceof Error ? error.message : String(error);
16820
+ process.stderr.write(`${BRAND.name}: ${message}
16821
+ `);
16822
+ return 1;
16823
+ }
16824
+ }
16815
16825
  process.title = BRAND.binNames[0];
16816
16826
  installMcpNoiseFilter();
16817
16827
  if (isProgramEntry()) {
16818
16828
  ensureHeapHeadroom();
16819
- void boot(processArgv.slice(2)).then((code) => {
16829
+ void resolveBoot(boot(processArgv.slice(2))).then((code) => {
16820
16830
  process.exitCode = code;
16821
16831
  process.stdout.write("", () => process.exit(code));
16822
16832
  setTimeout(() => process.exit(code), 1e3).unref();
16823
16833
  });
16824
16834
  }
16835
+ export {
16836
+ resolveBoot
16837
+ };
package/dist/index.js CHANGED
@@ -50,7 +50,7 @@ var BRAND = Object.freeze({
50
50
  envShareViewer: "INDUSAGI_SHARE_VIEWER_URL",
51
51
  shareViewerUrl: "https://buildwithindusagi.ai/session/"
52
52
  });
53
- var VERSION = "0.2.10";
53
+ var VERSION = "0.2.12";
54
54
 
55
55
  // src/launch/credentials.ts
56
56
  import { createInterface as createInterface2 } from "node:readline/promises";
@@ -1520,8 +1520,8 @@ function readBool(flags, key) {
1520
1520
  return flags[key] === true;
1521
1521
  }
1522
1522
  function deriveMode(flags) {
1523
- if (readBool(flags, "json")) return "rpc";
1524
1523
  if (readBool(flags, "print") && !readBool(flags, "interactive")) return "json";
1524
+ if (readBool(flags, "json")) return "rpc";
1525
1525
  return "text";
1526
1526
  }
1527
1527
  function resolveThinking(flags) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indusagi-coding-agent",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
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",
@@ -22,18 +22,15 @@
22
22
  "node": ">=20"
23
23
  },
24
24
  "main": "./dist/index.js",
25
- "types": "./dist/types/index.d.ts",
26
25
  "bin": {
27
26
  "indus": "dist/entry.js",
28
27
  "indusagi": "dist/entry.js"
29
28
  },
30
29
  "exports": {
31
30
  ".": {
32
- "types": "./dist/types/index.d.ts",
33
31
  "import": "./dist/index.js"
34
32
  },
35
33
  "./guardrails": {
36
- "types": "./dist/types/guardrails.d.ts",
37
34
  "import": "./dist/guardrails.js"
38
35
  },
39
36
  "./package.json": "./package.json"
@@ -49,9 +46,9 @@
49
46
  "scripts": {
50
47
  "typecheck": "tsc -p tsconfig.json --noEmit",
51
48
  "build": "node build.mjs",
52
- "lineage-scan": "node scripts/lineage-scan.mjs",
53
49
  "test": "vitest --run",
54
50
  "test:watch": "vitest",
51
+ "prepublishOnly": "npm test && npm run build",
55
52
  "start": "node dist/entry.js",
56
53
  "start:help": "node dist/entry.js --help"
57
54
  },
@@ -131,6 +131,7 @@ export type SessionSignal = {
131
131
  readonly kind: "tool_start";
132
132
  readonly id: string;
133
133
  readonly name: string;
134
+ readonly args: unknown;
134
135
  } | {
135
136
  readonly kind: "tool_update";
136
137
  readonly id: string;
@@ -140,6 +141,8 @@ export type SessionSignal = {
140
141
  readonly kind: "tool_end";
141
142
  readonly id: string;
142
143
  readonly ok: boolean;
144
+ readonly result: unknown;
145
+ readonly isError: boolean;
143
146
  } | {
144
147
  readonly kind: "turn_end";
145
148
  readonly usage: Usage;
@@ -52,7 +52,7 @@ export const TRANSLATOR_TABLE: TranslatorTable = {
52
52
  },
53
53
  turn_end: (event) => [{ kind: "turn_end", usage: usageOf(event.message) }],
54
54
  message_update: (event) => translateStreamEvent(event.assistantMessageEvent),
55
- tool_execution_start: (event) => [{ kind: "tool_start", id: event.toolCallId, name: event.toolName, args: event.args } as SessionSignal],
55
+ tool_execution_start: (event) => [{ kind: "tool_start", id: event.toolCallId, name: event.toolName, args: event.args }],
56
56
  tool_execution_update: (event) => [{
57
57
  kind: "tool_update",
58
58
  id: event.toolCallId,
@@ -65,7 +65,7 @@ export const TRANSLATOR_TABLE: TranslatorTable = {
65
65
  ok: !event.isError,
66
66
  result: event.result,
67
67
  isError: event.isError,
68
- } as SessionSignal],
68
+ }],
69
69
  };
70
70
 
71
71
  export function translateAgentEvent(event: AgentEvent): SessionSignal[] {
package/src/entry.ts CHANGED
@@ -43,11 +43,21 @@ function isProgramEntry(): boolean {
43
43
  }
44
44
  }
45
45
 
46
+ export async function resolveBoot(result: Promise<number>): Promise<number> {
47
+ try {
48
+ return await result;
49
+ } catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ process.stderr.write(`${BRAND.name}: ${message}\n`);
52
+ return 1;
53
+ }
54
+ }
55
+
46
56
  process.title = BRAND.binNames[0];
47
57
  installMcpNoiseFilter();
48
58
  if (isProgramEntry()) {
49
59
  ensureHeapHeadroom();
50
- void boot(processArgv.slice(2)).then((code) => {
60
+ void resolveBoot(boot(processArgv.slice(2))).then((code) => {
51
61
  process.exitCode = code;
52
62
  process.stdout.write("", () => process.exit(code));
53
63
  setTimeout(() => process.exit(code), 1e3).unref();
@@ -154,8 +154,8 @@ function readBool(flags: Record<string, FlagValue>, key: string): boolean {
154
154
  }
155
155
 
156
156
  function deriveMode(flags: Record<string, FlagValue>): Invocation["mode"] {
157
- if (readBool(flags, "json")) return "rpc";
158
157
  if (readBool(flags, "print") && !readBool(flags, "interactive")) return "json";
158
+ if (readBool(flags, "json")) return "rpc";
159
159
  return "text";
160
160
  }
161
161
 
@@ -26,4 +26,4 @@ export const BRAND: Brand = Object.freeze({
26
26
  /**
27
27
  * The product version — printed by `--version` and shown on the startup banner.
28
28
  */
29
- export const VERSION = "0.2.10";
29
+ export const VERSION = "0.2.12";
@@ -1,81 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { translateAgentEvent } from "./translate.js";
3
-
4
- type InputEvent = Parameters<typeof translateAgentEvent>[0];
5
-
6
- describe("translateAgentEvent tool signals", () => {
7
- it("includes tool arguments in tool_start", () => {
8
- const event: InputEvent = {
9
- type: "tool_execution_start",
10
- toolCallId: "call-1",
11
- toolName: "read",
12
- args: { path: "README.md" },
13
- };
14
-
15
- expect(translateAgentEvent(event)).toEqual([{
16
- kind: "tool_start",
17
- id: "call-1",
18
- name: "read",
19
- args: { path: "README.md" },
20
- }]);
21
- });
22
-
23
- it("includes tool results and error status in tool_end", () => {
24
- const event: InputEvent = {
25
- type: "tool_execution_end",
26
- toolCallId: "call-2",
27
- toolName: "write",
28
- result: { content: "permission denied" },
29
- isError: true,
30
- };
31
-
32
- expect(translateAgentEvent(event)).toEqual([{
33
- kind: "tool_end",
34
- id: "call-2",
35
- ok: false,
36
- result: { content: "permission denied" },
37
- isError: true,
38
- }]);
39
- });
40
- });
41
-
42
- describe("translateAgentEvent preserved signals", () => {
43
- it("preserves prompt extraction", () => {
44
- const event = {
45
- type: "message_end",
46
- message: {
47
- role: "user",
48
- content: [{ type: "text", text: "hello " }, { type: "text", text: "world" }],
49
- timestamp: 1,
50
- },
51
- } as unknown as InputEvent;
52
-
53
- expect(translateAgentEvent(event)).toEqual([{ kind: "prompt", text: "hello world" }]);
54
- });
55
-
56
- it("preserves text and thinking deltas", () => {
57
- const message = { role: "assistant", content: [], timestamp: 1 };
58
-
59
- expect(translateAgentEvent({
60
- type: "message_update",
61
- message,
62
- assistantMessageEvent: { type: "text_delta", delta: "answer" },
63
- } as unknown as InputEvent)).toEqual([{ kind: "text", delta: "answer" }]);
64
- expect(translateAgentEvent({
65
- type: "message_update",
66
- message,
67
- assistantMessageEvent: { type: "thinking_delta", delta: "reason" },
68
- } as unknown as InputEvent)).toEqual([{ kind: "thinking", delta: "reason" }]);
69
- });
70
-
71
- it("preserves turn_end usage", () => {
72
- const usage = { input: 1, output: 2, cacheRead: 3, cacheWrite: 4, totalTokens: 10, cost: { total: 0.5 } };
73
- const event = {
74
- type: "turn_end",
75
- message: { role: "assistant", content: [], usage, timestamp: 1 },
76
- toolResults: [],
77
- } as unknown as InputEvent;
78
-
79
- expect(translateAgentEvent(event)).toEqual([{ kind: "turn_end", usage }]);
80
- });
81
- });
@@ -1,60 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { resolveSarvamConfig, redactApiKey } from "./config.js";
3
-
4
- describe("resolveSarvamConfig", () => {
5
- const isolatedHome = "/tmp/indusagi-sarvam-test-home-empty";
6
-
7
- it("returns undefined when nothing is configured", () => {
8
- expect(
9
- resolveSarvamConfig({ env: {}, cwd: "/tmp", home: isolatedHome, disabled: false }),
10
- ).toBeUndefined();
11
- });
12
-
13
- it("prefers explicit key over env", () => {
14
- const cfg = resolveSarvamConfig({
15
- apiKey: "key-from-arg",
16
- env: { SARVAM_API_KEY: "key-from-env" },
17
- cwd: "/tmp",
18
- home: isolatedHome,
19
- });
20
- expect(cfg?.apiKey).toBe("key-from-arg");
21
- });
22
-
23
- it("honors SARVAM_API_KEY", () => {
24
- const cfg = resolveSarvamConfig({
25
- env: { SARVAM_API_KEY: "sk-test-123" },
26
- cwd: "/tmp",
27
- home: isolatedHome,
28
- });
29
- expect(cfg?.apiKey).toBe("sk-test-123");
30
- expect(cfg?.id).toBe("sarvam");
31
- });
32
-
33
- it("disabled wins", () => {
34
- const cfg = resolveSarvamConfig({
35
- apiKey: "sk-test",
36
- disabled: true,
37
- cwd: "/tmp",
38
- home: isolatedHome,
39
- });
40
- expect(cfg).toBeUndefined();
41
- });
42
-
43
- it("honors basePath from options", () => {
44
- const cfg = resolveSarvamConfig({
45
- apiKey: "sk-test",
46
- basePath: "/tmp/sarvam-out",
47
- cwd: "/tmp",
48
- home: isolatedHome,
49
- });
50
- expect(cfg?.basePath).toBe("/tmp/sarvam-out");
51
- });
52
- });
53
-
54
- describe("redactApiKey", () => {
55
- it("masks middle", () => {
56
- const r = redactApiKey("abcdefghijklmnop");
57
- expect(r).toContain("…");
58
- expect(r).not.toBe("abcdefghijklmnop");
59
- });
60
- });
@@ -1,50 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { resolveZohoConfig, redactUrl } from "./config.js";
3
-
4
- describe("resolveZohoConfig", () => {
5
- const isolatedHome = "/tmp/indusagi-zoho-test-home-empty";
6
-
7
- it("returns undefined when nothing is configured", () => {
8
- expect(
9
- resolveZohoConfig({ env: {}, cwd: "/tmp", home: isolatedHome, disabled: false }),
10
- ).toBeUndefined();
11
- });
12
-
13
- it("prefers explicit url over env", () => {
14
- const cfg = resolveZohoConfig({
15
- url: "https://mcp.example/a",
16
- env: { ZOHO_MCP_URL: "https://mcp.example/b" },
17
- cwd: "/tmp",
18
- home: isolatedHome,
19
- });
20
- expect(cfg?.url).toBe("https://mcp.example/a");
21
- });
22
-
23
- it("honors ZOHO_MCP_URL", () => {
24
- const cfg = resolveZohoConfig({
25
- env: { ZOHO_MCP_URL: "https://mcp.zoho.example/x" },
26
- cwd: "/tmp",
27
- home: isolatedHome,
28
- });
29
- expect(cfg?.url).toBe("https://mcp.zoho.example/x");
30
- expect(cfg?.id).toBe("zoho");
31
- });
32
-
33
- it("disabled wins", () => {
34
- const cfg = resolveZohoConfig({
35
- url: "https://mcp.example/a",
36
- disabled: true,
37
- cwd: "/tmp",
38
- home: isolatedHome,
39
- });
40
- expect(cfg).toBeUndefined();
41
- });
42
- });
43
-
44
- describe("redactUrl", () => {
45
- it("keeps host", () => {
46
- const r = redactUrl("https://mcp.zoho.in/secret/path/more");
47
- expect(r).toContain("mcp.zoho.in");
48
- expect(r).toContain("…");
49
- });
50
- });
@@ -1,18 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { hasRegisteredOAuthClientId, launchAttempts } from "./oauth.js";
3
-
4
- afterEach(() => vi.unstubAllEnvs());
5
-
6
- describe("OpenAI Codex OAuth registration", () => {
7
- it("uses the framework client when no environment override exists", () => {
8
- vi.stubEnv("INDUSAGI_OPENAI_CODEX_CLIENT_ID", "");
9
- expect(hasRegisteredOAuthClientId("openai-codex")).toBe(true);
10
- });
11
-
12
- it("opens ampersand-heavy OAuth URLs on Windows without cmd parsing", () => {
13
- const url = "https://example.com/oauth?response_type=code&client_id=client&scope=openid";
14
- expect(launchAttempts(url, "win32")).toEqual([
15
- { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", url] },
16
- ]);
17
- });
18
- });