indusagi-coding-agent 0.2.8 → 0.2.10

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 (222) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/entry.js +4 -4
  3. package/dist/index.js +4 -4
  4. package/package.json +3 -2
  5. package/src/_decls/entry.ts +18 -0
  6. package/src/_decls/guardrails.ts +35 -0
  7. package/src/_decls/index.ts +26 -0
  8. package/src/addons/contract.ts +236 -0
  9. package/src/addons/dispatch/event-dispatcher.ts +164 -0
  10. package/src/addons/dispatch/index.ts +25 -0
  11. package/src/addons/dispatch/tool-interceptor.ts +208 -0
  12. package/src/addons/host.ts +225 -0
  13. package/src/addons/index.ts +112 -0
  14. package/src/addons/manifest.ts +158 -0
  15. package/src/addons/sandbox.ts +170 -0
  16. package/src/addons/surface.ts +78 -0
  17. package/src/boot/auth-vault.ts +195 -0
  18. package/src/boot/boot.ts +138 -0
  19. package/src/boot/contract.ts +238 -0
  20. package/src/boot/heap.ts +59 -0
  21. package/src/boot/index.ts +28 -0
  22. package/src/boot/invocation.ts +93 -0
  23. package/src/boot/runners/addon-wiring.ts +153 -0
  24. package/src/boot/runners/checkpoint.ts +169 -0
  25. package/src/boot/runners/delegate-runner.ts +294 -0
  26. package/src/boot/runners/index.ts +13 -0
  27. package/src/boot/runners/link-runner.ts +45 -0
  28. package/src/boot/runners/memdir.ts +168 -0
  29. package/src/boot/runners/oneshot-runner.ts +58 -0
  30. package/src/boot/runners/read-state.ts +90 -0
  31. package/src/boot/runners/registry.ts +42 -0
  32. package/src/boot/runners/repl-runner.ts +143 -0
  33. package/src/boot/runners/server-mode.ts +121 -0
  34. package/src/boot/runners/session.ts +641 -0
  35. package/src/boot/server-token.ts +148 -0
  36. package/src/boot/stages.ts +167 -0
  37. package/src/boot/upgrade/apply.ts +94 -0
  38. package/src/boot/upgrade/index.ts +13 -0
  39. package/src/boot/upgrade/upgrades.ts +289 -0
  40. package/src/briefing/compose.ts +150 -0
  41. package/src/briefing/context-docs.ts +19 -0
  42. package/src/briefing/contract.ts +717 -0
  43. package/src/briefing/index.ts +31 -0
  44. package/src/briefing/macros.ts +97 -0
  45. package/src/briefing/skills.ts +47 -0
  46. package/src/capability-deck/bridge-ledger/index.ts +27 -0
  47. package/src/capability-deck/bridge-ledger/key.ts +67 -0
  48. package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
  49. package/src/capability-deck/bridge-ledger/network.ts +117 -0
  50. package/src/capability-deck/builtin-bridge.ts +312 -0
  51. package/src/capability-deck/cards/bg-process-card.ts +335 -0
  52. package/src/capability-deck/cards/index.ts +115 -0
  53. package/src/capability-deck/cards/memory-card.ts +146 -0
  54. package/src/capability-deck/cards/plan-file.ts +97 -0
  55. package/src/capability-deck/cards/plan-tools.ts +185 -0
  56. package/src/capability-deck/cards/saas-card.ts +183 -0
  57. package/src/capability-deck/cards/task-card.ts +207 -0
  58. package/src/capability-deck/cards/todo-card.ts +168 -0
  59. package/src/capability-deck/cards/workflow-card.ts +247 -0
  60. package/src/capability-deck/contract.ts +388 -0
  61. package/src/capability-deck/index.ts +48 -0
  62. package/src/capability-deck/manifest.ts +109 -0
  63. package/src/capability-deck/provision.ts +169 -0
  64. package/src/channels/contract.ts +191 -0
  65. package/src/channels/framer.ts +50 -0
  66. package/src/channels/index.ts +101 -0
  67. package/src/channels/link/dialog.ts +129 -0
  68. package/src/channels/link/driver.ts +190 -0
  69. package/src/channels/link/index.ts +34 -0
  70. package/src/channels/link/server.ts +134 -0
  71. package/src/channels/oneshot.ts +90 -0
  72. package/src/channels/ops.ts +65 -0
  73. package/src/channels/session-ops.ts +81 -0
  74. package/src/conductor/bash-guard.ts +599 -0
  75. package/src/conductor/catalog/catalog.ts +116 -0
  76. package/src/conductor/catalog/index.ts +8 -0
  77. package/src/conductor/catalog/matcher.ts +134 -0
  78. package/src/conductor/conductor.ts +234 -0
  79. package/src/conductor/contract.ts +842 -0
  80. package/src/conductor/diagnostics.ts +227 -0
  81. package/src/conductor/index.ts +33 -0
  82. package/src/conductor/permissions.ts +588 -0
  83. package/src/conductor/quota-error.ts +49 -0
  84. package/src/conductor/signal-hub/hub.ts +46 -0
  85. package/src/conductor/signal-hub/index.ts +2 -0
  86. package/src/conductor/signal-hub/translate.test.ts +81 -0
  87. package/src/conductor/signal-hub/translate.ts +74 -0
  88. package/src/conductor/skill-parse/index.ts +2 -0
  89. package/src/conductor/skill-parse/parse.ts +108 -0
  90. package/src/conductor/transcript-store/index.ts +22 -0
  91. package/src/conductor/transcript-store/serialize.ts +116 -0
  92. package/src/conductor/transcript-store/store.ts +205 -0
  93. package/src/console/auth-status.ts +56 -0
  94. package/src/console/components/AgentsView.ts +165 -0
  95. package/src/console/components/BackgroundAgents.ts +155 -0
  96. package/src/console/components/Banner.ts +334 -0
  97. package/src/console/components/Composer.ts +94 -0
  98. package/src/console/components/StatusBar.ts +49 -0
  99. package/src/console/components/TerminalConsole.ts +1090 -0
  100. package/src/console/components/WorkingIndicator.ts +98 -0
  101. package/src/console/components/banner-sweep.ts +24 -0
  102. package/src/console/components/welcome.ts +74 -0
  103. package/src/console/contract.ts +630 -0
  104. package/src/console/index.ts +34 -0
  105. package/src/console/input/complete.ts +127 -0
  106. package/src/console/input/dir-reader.ts +34 -0
  107. package/src/console/input/index.ts +23 -0
  108. package/src/console/input/keymap.ts +159 -0
  109. package/src/console/input/paste.ts +104 -0
  110. package/src/console/mount.ts +56 -0
  111. package/src/console/overlays/approval-queue.ts +57 -0
  112. package/src/console/overlays/approval.ts +130 -0
  113. package/src/console/overlays/auth.ts +342 -0
  114. package/src/console/overlays/boards.ts +308 -0
  115. package/src/console/overlays/host.ts +36 -0
  116. package/src/console/overlays/index.ts +26 -0
  117. package/src/console/overlays/pickers.ts +258 -0
  118. package/src/console/overlays/sessions.ts +190 -0
  119. package/src/console/reducer.ts +182 -0
  120. package/src/console/slash/builtins.ts +81 -0
  121. package/src/console/slash/commands/dynamic.ts +83 -0
  122. package/src/console/slash/commands/integrations.ts +695 -0
  123. package/src/console/slash/commands/shared.ts +75 -0
  124. package/src/console/slash/commands/transcript.ts +263 -0
  125. package/src/console/slash/commands/workbench.ts +246 -0
  126. package/src/console/slash/index.ts +15 -0
  127. package/src/console/slash/registry.ts +70 -0
  128. package/src/console/slash/resolve.ts +63 -0
  129. package/src/console/startup.ts +209 -0
  130. package/src/console/theme/adapter.ts +45 -0
  131. package/src/console/theme/index.ts +7 -0
  132. package/src/console/theme/palette.ts +68 -0
  133. package/src/console/theme/resolve.ts +39 -0
  134. package/src/console/theme/tokens.ts +71 -0
  135. package/src/entry.ts +55 -0
  136. package/src/guardrails.ts +37 -0
  137. package/src/index.ts +18 -0
  138. package/src/insight/channel.ts +88 -0
  139. package/src/insight/contract.ts +185 -0
  140. package/src/insight/index.ts +110 -0
  141. package/src/insight/recorder.ts +213 -0
  142. package/src/insight/redaction.ts +157 -0
  143. package/src/insight/replay.ts +158 -0
  144. package/src/insight/sampling.ts +70 -0
  145. package/src/insight/serialize.ts +50 -0
  146. package/src/insight/sinks/console.ts +64 -0
  147. package/src/insight/sinks/file.ts +40 -0
  148. package/src/insight/sinks/index.ts +24 -0
  149. package/src/insight/sinks/stream.ts +54 -0
  150. package/src/integrations/sarvam/attach.ts +239 -0
  151. package/src/integrations/sarvam/config.ts +156 -0
  152. package/src/integrations/sarvam/index.ts +25 -0
  153. package/src/integrations/sarvam/sarvam.test.ts +60 -0
  154. package/src/integrations/sarvam/types.ts +27 -0
  155. package/src/integrations/zoho/attach.ts +342 -0
  156. package/src/integrations/zoho/config.ts +125 -0
  157. package/src/integrations/zoho/index.ts +27 -0
  158. package/src/integrations/zoho/types.ts +21 -0
  159. package/src/integrations/zoho/zoho.test.ts +50 -0
  160. package/src/kit/clipboard-image.ts +107 -0
  161. package/src/kit/external-editor.ts +48 -0
  162. package/src/kit/image.ts +59 -0
  163. package/src/kit/index.ts +51 -0
  164. package/src/kit/shell.ts +19 -0
  165. package/src/kit/tool-fetch.ts +85 -0
  166. package/src/launch/catalog.ts +148 -0
  167. package/src/launch/contract.ts +187 -0
  168. package/src/launch/credentials.ts +625 -0
  169. package/src/launch/index.ts +98 -0
  170. package/src/launch/invocation/attachments.ts +179 -0
  171. package/src/launch/invocation/flags.ts +196 -0
  172. package/src/launch/invocation/index.ts +25 -0
  173. package/src/launch/invocation/read.ts +260 -0
  174. package/src/launch/invocation/usage.ts +67 -0
  175. package/src/launch/login.ts +324 -0
  176. package/src/launch/oauth.test.ts +18 -0
  177. package/src/launch/oauth.ts +203 -0
  178. package/src/launch/packages.ts +194 -0
  179. package/src/launch/pickers.ts +189 -0
  180. package/src/runtime-bridge/bridges/_drive.ts +96 -0
  181. package/src/runtime-bridge/bridges/builtins.ts +68 -0
  182. package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
  183. package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
  184. package/src/runtime-bridge/bridges/index.ts +33 -0
  185. package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
  186. package/src/runtime-bridge/broker.ts +227 -0
  187. package/src/runtime-bridge/contract.ts +122 -0
  188. package/src/runtime-bridge/index.ts +79 -0
  189. package/src/runtime-bridge/sink.ts +180 -0
  190. package/src/sessions/contract.ts +81 -0
  191. package/src/sessions/index.ts +13 -0
  192. package/src/sessions/library.ts +229 -0
  193. package/src/settings/contract.ts +114 -0
  194. package/src/settings/index.ts +32 -0
  195. package/src/settings/manager.ts +117 -0
  196. package/src/transcript-export/index.ts +45 -0
  197. package/src/transcript-export/publish.ts +260 -0
  198. package/src/transcript-export/sgr.ts +315 -0
  199. package/src/transcript-export/template.ts +272 -0
  200. package/src/transcript-export/theme-bridge.ts +150 -0
  201. package/src/window-budget/budget/estimate.ts +135 -0
  202. package/src/window-budget/budget/gate.ts +33 -0
  203. package/src/window-budget/budget/index.ts +16 -0
  204. package/src/window-budget/budget/slice.ts +56 -0
  205. package/src/window-budget/condenser.ts +58 -0
  206. package/src/window-budget/contract.ts +184 -0
  207. package/src/window-budget/index.ts +19 -0
  208. package/src/window-budget/microcompact.ts +95 -0
  209. package/src/window-budget/rehydrate.ts +136 -0
  210. package/src/window-budget/summarize/condense.ts +103 -0
  211. package/src/window-budget/summarize/index.ts +14 -0
  212. package/src/window-budget/summarize/prompt.ts +149 -0
  213. package/src/workflow-engine/agent-runner.ts +181 -0
  214. package/src/workflow-engine/display.ts +224 -0
  215. package/src/workflow-engine/engine.ts +294 -0
  216. package/src/workflow-engine/index.ts +23 -0
  217. package/src/workflow-engine/parse.ts +172 -0
  218. package/src/workflow-engine/structured-output.ts +35 -0
  219. package/src/workspace/brand.ts +29 -0
  220. package/src/workspace/index.ts +18 -0
  221. package/src/workspace/locator.ts +103 -0
  222. package/src/workspace/runtime-detect.ts +64 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Runner: `oneshot` — single non-interactive request to stdout.
3
+ *
4
+ * Drives the Phase-8 oneshot channel: it assembles a {@link SessionConductor}
5
+ * for the invocation, builds a {@link ChannelContext} over `process.stdout`
6
+ * (clean text, or a streamed NDJSON event log), runs every prompt to settlement
7
+ * via {@link runOneshot}, and resolves the channel exit code. The output shape is
8
+ * NDJSON when the invocation carries a `json` flag, otherwise clean text.
9
+ */
10
+ import { inertDialog, ndjsonFramer, runOneshot } from "../../channels/index.js";
11
+ import type { Invocation, Runner } from "../contract.js";
12
+ import { oneshotPrompts, buildSessionConductor } from "./session.js";
13
+
14
+ const EXIT_NO_INPUT = 2;
15
+
16
+ const STDOUT = {
17
+ write(chunk: string | Uint8Array, cb?: (err?: Error | null) => void) {
18
+ return process.stdout.write(chunk, cb);
19
+ },
20
+ };
21
+
22
+ function shapeOf(inv: Invocation): "ndjson" | "text" {
23
+ return inv.flags.json === true || inv.flags.json === "true" ? "ndjson" : "text";
24
+ }
25
+
26
+ /**
27
+ * The one-shot runner.
28
+ *
29
+ * {@link Runner.accepts} matches invocations whose resolved mode is `oneshot`.
30
+ * {@link Runner.run} assembles the conductor and channel context, then delegates
31
+ * the run to {@link runOneshot} and returns its exit code. With no request text
32
+ * it writes a short notice and exits non-zero rather than mounting an empty run.
33
+ */
34
+ export const oneshotRunner: Runner = {
35
+ id: "oneshot",
36
+ accepts(inv) {
37
+ return inv.mode === "oneshot";
38
+ },
39
+ async run(ctx) {
40
+ const prompts = oneshotPrompts(ctx);
41
+ if (prompts.length === 0) {
42
+ process.stderr.write("oneshot: no request text supplied\n");
43
+ return EXIT_NO_INPUT;
44
+ }
45
+ const conductor = await buildSessionConductor(ctx);
46
+ const channel = {
47
+ conductor,
48
+ out: STDOUT,
49
+ framer: ndjsonFramer,
50
+ dialog: inertDialog,
51
+ };
52
+ const request = {
53
+ shape: shapeOf(ctx.invocation),
54
+ prompts,
55
+ };
56
+ return runOneshot(channel, request);
57
+ },
58
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Per-session read-state store — the product half of the read-before-edit gate
3
+ * (read-edit-gate, #11).
4
+ *
5
+ * The framework's file tools (read/write/edit) consult a host-injected handle on
6
+ * `ctx.framework` under the string-literal key {@link READ_STATE_HANDLE_KEY} to
7
+ * decide whether an edit/write may proceed: a file must have been read before it
8
+ * is mutated, and an edit is refused when the file changed on disk since that
9
+ * read. This module supplies the missing host piece — an in-memory map of which
10
+ * files have been read this session and the stat fingerprint at read time — so
11
+ * the gate has somewhere to record and look up that state.
12
+ */
13
+ import { normalize } from "node:path";
14
+
15
+ /**
16
+ * The string-literal key under which the host wires this store onto the deck's
17
+ * `ctx.framework` bag.
18
+ *
19
+ * Mirrors the existing `DELEGATE_HANDLE_KEY = "delegate"` and
20
+ * `MEMORY_HANDLE_KEY = "memoryStore"` conventions. The framework's file tools
21
+ * read `ctx.framework["readState"]` by the same literal, so the two sides agree
22
+ * by string rather than by a shared imported symbol.
23
+ */
24
+ export const READ_STATE_HANDLE_KEY = "readState";
25
+
26
+ /**
27
+ * One file's read fingerprint, recorded when the agent reads it this session.
28
+ *
29
+ * - `mtimeMs` — the file's last-modified time (ms) at read. The gate refuses an
30
+ * edit when the on-disk mtime has advanced past this. (The framework's stat
31
+ * result names this `modifiedMs`; the host maps it to `mtimeMs` here.)
32
+ * - `size` — the file's byte length at read, a cheap secondary change signal.
33
+ * - `contentHash` — optional digest of the content read, for a content-equality
34
+ * fallback when the mtime advanced but the bytes are unchanged.
35
+ * - `readAt` — when the host recorded this read (ms since epoch).
36
+ */
37
+ export interface ReadStateRecord {
38
+ readonly mtimeMs: number;
39
+ readonly size: number;
40
+ readonly contentHash?: string;
41
+ readonly readAt: number;
42
+ }
43
+
44
+ /**
45
+ * In-memory map of read files for one session, keyed by normalized absolute path.
46
+ *
47
+ * The framework gate `set`s a record after every read and after every successful
48
+ * mutation, and `get`s/`has`-checks it before an edit. Keys are normalized in one
49
+ * place (`key`) so writes and lookups of the same file land on the same entry no
50
+ * matter how the caller spelled the path.
51
+ */
52
+ export class ReadStateStore {
53
+ private readonly entries = new Map<string, ReadStateRecord>();
54
+
55
+ /** Normalize an absolute path into the store's canonical key. */
56
+ private key(absPath: string): string {
57
+ return normalize(absPath);
58
+ }
59
+
60
+ /** The recorded read fingerprint for a path, or `undefined` if never read. */
61
+ get(absPath: string): ReadStateRecord | undefined {
62
+ return this.entries.get(this.key(absPath));
63
+ }
64
+
65
+ /** Record (or replace) the read fingerprint for a path; returns this store. */
66
+ set(absPath: string, value: ReadStateRecord): this {
67
+ this.entries.set(this.key(absPath), value);
68
+ return this;
69
+ }
70
+
71
+ /** Whether a path has a recorded read fingerprint this session. */
72
+ has(absPath: string): boolean {
73
+ return this.entries.has(this.key(absPath));
74
+ }
75
+
76
+ /** Drop a path's recorded read fingerprint; returns whether one existed. */
77
+ delete(absPath: string): boolean {
78
+ return this.entries.delete(this.key(absPath));
79
+ }
80
+
81
+ /** Forget every recorded read for this session. */
82
+ clear(): void {
83
+ this.entries.clear();
84
+ }
85
+ }
86
+
87
+ /** Mint a fresh, empty per-session read-state store. */
88
+ export function createReadStateStore(): ReadStateStore {
89
+ return new ReadStateStore();
90
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Runner registry — the table-driven replacement for a mode `if/else` ladder.
3
+ *
4
+ * The boot pipeline ends by handing the resolved {@link Invocation} to exactly
5
+ * one {@link Runner}. Rather than branch on the mode string at the call site,
6
+ * dispatch is data: {@link RUNNERS} lists every available runner in priority
7
+ * order, and {@link selectRunner} returns the first whose {@link Runner.accepts}
8
+ * predicate matches. Adding or reordering modes is a one-line edit to the table,
9
+ * never a change to control flow.
10
+ *
11
+ * The interactive REPL is both first in the table and the guaranteed fallback:
12
+ * if no runner claims an invocation, {@link selectRunner} returns it, so a bare
13
+ * command line lands in the interactive session.
14
+ */
15
+ import type { Invocation, Runner } from "../contract.js";
16
+ import { replRunner } from "./repl-runner.js";
17
+ import { oneshotRunner } from "./oneshot-runner.js";
18
+ import { linkRunner } from "./link-runner.js";
19
+
20
+ /**
21
+ * Every runner the boot layer can dispatch to, in match-priority order.
22
+ *
23
+ * {@link selectRunner} scans this list front-to-back and returns the first
24
+ * accepting runner; `replRunner` leads the list and also serves as the default.
25
+ */
26
+ export const RUNNERS: readonly Runner[] = [replRunner, oneshotRunner, linkRunner];
27
+
28
+ /**
29
+ * Choose the runner for a parsed {@link Invocation}.
30
+ *
31
+ * Returns the first runner in {@link RUNNERS} that {@link Runner.accepts | accepts}
32
+ * the invocation; if none does, falls back to the interactive REPL runner so
33
+ * dispatch is total and a runner is always returned.
34
+ */
35
+ export function selectRunner(inv: Invocation): Runner {
36
+ for (const runner of RUNNERS) {
37
+ if (runner.accepts(inv)) {
38
+ return runner;
39
+ }
40
+ }
41
+ return replRunner;
42
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Runner: `repl` — interactive terminal session.
3
+ *
4
+ * Mounts the Phase-7 console for a live session: it assembles a
5
+ * {@link SessionConductor} for the invocation and renders the interactive surface
6
+ * via {@link mountConsole}, seeding it with the positional prompt when one was
7
+ * supplied. The runner resolves the success exit code once the surface is
8
+ * dismissed.
9
+ */
10
+ import { mountConsole } from "../../console/index.js";
11
+ import { listLoginProviders, openLoginUrl, startOAuthLogin } from "../../launch/oauth.js";
12
+ import { pickResumeTarget } from "../../launch/pickers.js";
13
+ import { SessionLibrary } from "../../sessions/library.js";
14
+ import { PreferenceStore } from "../../settings/manager.js";
15
+ import { sessionScopeDir } from "./session.js";
16
+ import { createAuthVault } from "../auth-vault.js";
17
+ import type { BootContext, Runner } from "../contract.js";
18
+ import { buildSessionConductor } from "./session.js";
19
+ import { resolveServerGatewayUrls } from "./server-mode.js";
20
+
21
+ const EXIT_OK = 0;
22
+
23
+ /**
24
+ * Build the in-memory overlay services the interactive console expects.
25
+ *
26
+ * The console calls a few helpers directly (`listLoginProviders`,
27
+ * `startOAuthLogin`, vault access, gateway re-routing). All of those are
28
+ * wired here from the boot-layer pieces so the conductor and the UI share one
29
+ * credential store and one gateway-base-url mapping.
30
+ */
31
+ export function buildOverlayServices(ctx: BootContext, conductor: { updateGatewayBaseUrls(map: Record<string, string>): void }) {
32
+ const cwd = ctx.invocation.cwd ?? process.cwd();
33
+ const vault = createAuthVault(ctx.workspace.authPath);
34
+ return {
35
+ conductor,
36
+ settings: PreferenceStore.fromWorkspace(ctx.workspace, cwd),
37
+ sessions: new SessionLibrary({
38
+ sessionsDir: sessionScopeDir(ctx.workspace.sessionsDir, ctx.invocation.cwd ?? process.cwd()),
39
+ }),
40
+ listLoginProviders,
41
+ startOAuthLogin,
42
+ openLoginUrl,
43
+ vault,
44
+ async refreshGatewayRouting() {
45
+ const map = await resolveServerGatewayUrls(ctx, vault);
46
+ conductor.updateGatewayBaseUrls(map);
47
+ },
48
+ };
49
+ }
50
+
51
+ function toResumeRef(row: { id: string; path: string; lastModified?: number; messageCount?: number; preview?: string; name?: string; size?: number }) {
52
+ const lastModified = row.lastModified ?? 0;
53
+ return {
54
+ id: row.id,
55
+ path: row.path,
56
+ cwd: "",
57
+ lastModified,
58
+ created: new Date(lastModified),
59
+ modified: new Date(lastModified),
60
+ messageCount: row.messageCount ?? 0,
61
+ firstMessage: row.preview ?? "",
62
+ allMessagesText: row.preview ?? "",
63
+ ...row.name !== undefined ? { name: row.name } : {},
64
+ ...row.size !== undefined ? { size: row.size } : {},
65
+ };
66
+ }
67
+
68
+ async function chooseResumeTarget(ctx: BootContext, library: SessionLibrary): Promise<string | null> {
69
+ const inv = ctx.invocation;
70
+ if (inv.continueLatest === true) {
71
+ const rows = await library.list();
72
+ return rows.length > 0 ? rows[0].id : null;
73
+ }
74
+ const rows = await library.list();
75
+ if (rows.length === 0) return null;
76
+ const refs = rows.map(toResumeRef);
77
+ const idByPath = new Map(rows.map((row) => [row.path, row.id]));
78
+ const load = async () => refs;
79
+ const outcome = await pickResumeTarget(load, load);
80
+ if (outcome.fault !== undefined) {
81
+ throw outcome.fault.cause ?? new Error(outcome.fault.message);
82
+ }
83
+ if (outcome.path === null) return null;
84
+ return idByPath.get(outcome.path) ?? null;
85
+ }
86
+
87
+ /**
88
+ * Apply `--resume` / `--continue` to a freshly-built conductor.
89
+ *
90
+ * Tolerates a missing resume target by logging and starting a fresh session;
91
+ * tolerance preserves the interactive boot flow when the prior session was
92
+ * deleted between launches.
93
+ */
94
+ export async function applyResume(ctx: BootContext, conductor: { resume(id: string): Promise<void>; snapshot(): { phase: string } }): Promise<void> {
95
+ const inv = ctx.invocation;
96
+ if (inv.resume !== true && inv.continueLatest !== true) return;
97
+ const library = new SessionLibrary({
98
+ sessionsDir: sessionScopeDir(ctx.workspace.sessionsDir, ctx.invocation.cwd ?? process.cwd()),
99
+ });
100
+ try {
101
+ const sessionId = await chooseResumeTarget(ctx, library);
102
+ if (sessionId === null) {
103
+ process.stderr.write("No prior session to resume; starting a fresh one.\n");
104
+ return;
105
+ }
106
+ await conductor.resume(sessionId);
107
+ const state = conductor.snapshot();
108
+ if (state.phase === "faulted") {
109
+ process.stderr.write(
110
+ `Could not resume session "${sessionId}"; starting a fresh one.\n`,
111
+ );
112
+ }
113
+ } catch (cause) {
114
+ const detail = cause instanceof Error ? cause.message : String(cause);
115
+ process.stderr.write(`Resume failed (${detail}); starting a fresh session.\n`);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * The interactive-REPL runner.
121
+ *
122
+ * {@link Runner.accepts} matches invocations whose resolved mode is `repl`; it is
123
+ * also the registry default, so it is reached for the empty / interactive command
124
+ * line. {@link Runner.run} assembles the conductor, honours `--resume` /
125
+ * `--continue`, and mounts the console.
126
+ */
127
+ export const replRunner: Runner = {
128
+ id: "repl",
129
+ accepts(inv) {
130
+ return inv.mode === "repl";
131
+ },
132
+ async run(ctx) {
133
+ const conductor = await buildSessionConductor(ctx);
134
+ await applyResume(ctx, conductor);
135
+ const services = buildOverlayServices(ctx, conductor);
136
+ const initialInput = ctx.invocation.prompt;
137
+ await mountConsole(
138
+ conductor,
139
+ initialInput === undefined ? { services } : { initialInput, services },
140
+ );
141
+ return EXIT_OK;
142
+ },
143
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Boot helper: decide whether a provider runs in **server mode** (Case B).
3
+ *
4
+ * A model turn takes one of three paths (see LOGIN_GATEWAY_PLAN.md §1):
5
+ * - **A. Local key** — a usable key for the provider is stored in the on-disk
6
+ * auth vault (or exported in the environment). The framework calls the
7
+ * provider directly; behaviour is unchanged and this module returns
8
+ * `{ serverMode: false }`.
9
+ * - **B. Server tier** — the provider is gateway-eligible, the user has **no**
10
+ * usable local key, and they hold a **valid server token**. The session token
11
+ * is vended as the provider "key" and the model is re-routed through the indus
12
+ * gateway. This module returns `{ serverMode: true, token }`.
13
+ * - **C. Blocked** — no local key and no valid token: `{ serverMode: false }`
14
+ * (the resolver then falls through to the framework's own env lookup, which
15
+ * surfaces the usual "no API key" guidance pointing at `indus login`).
16
+ *
17
+ * The two consumers in {@link file://./session.ts} call this with the SAME
18
+ * `(ctx, provider, vault)` triple so the key-resolver fallback and the
19
+ * gateway-base-url computation always agree on whether server mode is active.
20
+ */
21
+ import { INDUS_SERVER_URL, getServerToken, hasValidServerToken } from "../server-token.js";
22
+ import type { AuthVault } from "../../launch/contract.js";
23
+ import type { BootContext } from "../contract.js";
24
+
25
+ /**
26
+ * The outcome of {@link resolveServerMode}: whether the provider should route
27
+ * through the gateway this run, and (when so) the session token to hand back as
28
+ * the provider api key.
29
+ */
30
+ export interface ServerModeResult {
31
+ /** `true` => bind the gateway base url and use {@link token} as the key. */
32
+ serverMode: boolean;
33
+ /** The session token (`access_token`) to vend as the key, when `serverMode`. */
34
+ token?: string;
35
+ }
36
+
37
+ const GATEWAY_ELIGIBLE_PROVIDERS: ReadonlySet<string> = new Set([
38
+ "minimax",
39
+ "sarvam",
40
+ ]);
41
+
42
+ /**
43
+ * Whether `provider` may be routed through the indus gateway. Excludes the
44
+ * non-portable providers (vertex / bedrock / codex) and the deferred opencode
45
+ * `anthropic-messages` api per the §B.4 caveat.
46
+ */
47
+ export function isGatewayEligibleProvider(provider: string): boolean {
48
+ return GATEWAY_ELIGIBLE_PROVIDERS.has(provider);
49
+ }
50
+
51
+ /** The server-tier provider slugs (for default-model selection / listing). */
52
+ export function serverTierProviders(): readonly string[] {
53
+ return [...GATEWAY_ELIGIBLE_PROVIDERS];
54
+ }
55
+
56
+ async function hasUsableLocalKey(provider: string, vault: AuthVault): Promise<boolean> {
57
+ try {
58
+ const accounts: string[] = [];
59
+ const def = await vault.defaultAccount(provider);
60
+ if (def !== undefined && def.length > 0) accounts.push(def);
61
+ for (const account of await vault.listAccounts(provider)) {
62
+ if (!accounts.includes(account)) accounts.push(account);
63
+ }
64
+ for (const account of accounts) {
65
+ const key = await vault.readUsableKey(provider, account);
66
+ if (typeof key === "string" && key.length > 0) return true;
67
+ }
68
+ } catch {
69
+ // ignore
70
+ }
71
+ return false;
72
+ }
73
+
74
+ /**
75
+ * Decide whether `provider` runs in server mode for this session.
76
+ *
77
+ * Server mode is active when ALL hold:
78
+ * 1. {@link isGatewayEligibleProvider}(provider) — excludes vertex/bedrock/codex
79
+ * and the deferred opencode `anthropic-messages` api (§B.4);
80
+ * 2. no EXPLICIT local key — the vault holds no usable key for the provider
81
+ * (an ambient env var is intentionally ignored for server-tier providers);
82
+ * 3. {@link hasValidServerToken}() — a stored, non-expired server token exists.
83
+ *
84
+ * Returns `{ serverMode: false }` whenever any condition fails, so the
85
+ * local-key (Case A) path is preserved byte-for-byte: the resolver then returns
86
+ * `undefined` and no gateway base url is bound.
87
+ *
88
+ * @param _ctx the boot context (reserved for future per-run policy; the vault is
89
+ * passed explicitly so callers share one instance)
90
+ * @param provider the framework provider slug to evaluate
91
+ * @param vault the on-disk credential vault to probe for a local key
92
+ */
93
+ export async function resolveServerMode(_ctx: BootContext, provider: string, vault: AuthVault): Promise<ServerModeResult> {
94
+ if (!isGatewayEligibleProvider(provider)) return { serverMode: false };
95
+ if (await hasUsableLocalKey(provider, vault)) return { serverMode: false };
96
+ if (!(await hasValidServerToken())) return { serverMode: false };
97
+ const token = await getServerToken();
98
+ const access = token?.access_token;
99
+ if (typeof access !== "string" || access.length === 0) return { serverMode: false };
100
+ return { serverMode: true, token: access };
101
+ }
102
+
103
+ /**
104
+ * Build the per-provider gateway base-url map for THIS session: for every
105
+ * gateway-eligible provider currently in server mode (no explicit local key +
106
+ * valid token), map `provider -> ${INDUS_SERVER_URL}/gateway/${provider}`.
107
+ *
108
+ * The conductor consults this map at EVERY model bind — including a runtime
109
+ * `/model` switch — so selecting ANY server-tier model (not just the one the
110
+ * session launched on) routes through the gateway. Empty when nothing qualifies.
111
+ */
112
+ export async function resolveServerGatewayUrls(ctx: BootContext, vault: AuthVault): Promise<Record<string, string>> {
113
+ const map: Record<string, string> = {};
114
+ for (const provider of GATEWAY_ELIGIBLE_PROVIDERS) {
115
+ const result = await resolveServerMode(ctx, provider, vault);
116
+ if (result.serverMode) {
117
+ map[provider] = `${INDUS_SERVER_URL}/gateway/${provider}`;
118
+ }
119
+ }
120
+ return map;
121
+ }