indusagi-coding-agent 0.2.5 → 0.2.9

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 (224) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +9 -5
  3. package/dist/entry.js +14267 -24282
  4. package/dist/guardrails.js +924 -7867
  5. package/dist/index.js +13706 -23775
  6. package/package.json +3 -2
  7. package/src/_decls/entry.ts +18 -0
  8. package/src/_decls/guardrails.ts +35 -0
  9. package/src/_decls/index.ts +26 -0
  10. package/src/addons/contract.ts +236 -0
  11. package/src/addons/dispatch/event-dispatcher.ts +164 -0
  12. package/src/addons/dispatch/index.ts +25 -0
  13. package/src/addons/dispatch/tool-interceptor.ts +208 -0
  14. package/src/addons/host.ts +225 -0
  15. package/src/addons/index.ts +112 -0
  16. package/src/addons/manifest.ts +158 -0
  17. package/src/addons/sandbox.ts +170 -0
  18. package/src/addons/surface.ts +78 -0
  19. package/src/boot/auth-vault.ts +195 -0
  20. package/src/boot/boot.ts +138 -0
  21. package/src/boot/contract.ts +238 -0
  22. package/src/boot/heap.ts +59 -0
  23. package/src/boot/index.ts +28 -0
  24. package/src/boot/invocation.ts +93 -0
  25. package/src/boot/runners/addon-wiring.ts +153 -0
  26. package/src/boot/runners/checkpoint.ts +169 -0
  27. package/src/boot/runners/delegate-runner.ts +294 -0
  28. package/src/boot/runners/index.ts +13 -0
  29. package/src/boot/runners/link-runner.ts +45 -0
  30. package/src/boot/runners/memdir.ts +168 -0
  31. package/src/boot/runners/oneshot-runner.ts +58 -0
  32. package/src/boot/runners/read-state.ts +90 -0
  33. package/src/boot/runners/registry.ts +42 -0
  34. package/src/boot/runners/repl-runner.ts +143 -0
  35. package/src/boot/runners/server-mode.ts +121 -0
  36. package/src/boot/runners/session.ts +641 -0
  37. package/src/boot/server-token.ts +148 -0
  38. package/src/boot/stages.ts +167 -0
  39. package/src/boot/upgrade/apply.ts +94 -0
  40. package/src/boot/upgrade/index.ts +13 -0
  41. package/src/boot/upgrade/upgrades.ts +289 -0
  42. package/src/briefing/compose.ts +150 -0
  43. package/src/briefing/context-docs.ts +19 -0
  44. package/src/briefing/contract.ts +717 -0
  45. package/src/briefing/index.ts +31 -0
  46. package/src/briefing/macros.ts +97 -0
  47. package/src/briefing/skills.ts +47 -0
  48. package/src/capability-deck/bridge-ledger/index.ts +27 -0
  49. package/src/capability-deck/bridge-ledger/key.ts +67 -0
  50. package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
  51. package/src/capability-deck/bridge-ledger/network.ts +117 -0
  52. package/src/capability-deck/builtin-bridge.ts +312 -0
  53. package/src/capability-deck/cards/bg-process-card.ts +335 -0
  54. package/src/capability-deck/cards/index.ts +115 -0
  55. package/src/capability-deck/cards/memory-card.ts +146 -0
  56. package/src/capability-deck/cards/plan-file.ts +97 -0
  57. package/src/capability-deck/cards/plan-tools.ts +185 -0
  58. package/src/capability-deck/cards/saas-card.ts +183 -0
  59. package/src/capability-deck/cards/task-card.ts +207 -0
  60. package/src/capability-deck/cards/todo-card.ts +168 -0
  61. package/src/capability-deck/cards/workflow-card.ts +247 -0
  62. package/src/capability-deck/contract.ts +388 -0
  63. package/src/capability-deck/index.ts +48 -0
  64. package/src/capability-deck/manifest.ts +109 -0
  65. package/src/capability-deck/provision.ts +169 -0
  66. package/src/channels/contract.ts +191 -0
  67. package/src/channels/framer.ts +50 -0
  68. package/src/channels/index.ts +101 -0
  69. package/src/channels/link/dialog.ts +129 -0
  70. package/src/channels/link/driver.ts +190 -0
  71. package/src/channels/link/index.ts +34 -0
  72. package/src/channels/link/server.ts +134 -0
  73. package/src/channels/oneshot.ts +90 -0
  74. package/src/channels/ops.ts +65 -0
  75. package/src/channels/session-ops.ts +81 -0
  76. package/src/conductor/bash-guard.ts +599 -0
  77. package/src/conductor/catalog/catalog.ts +116 -0
  78. package/src/conductor/catalog/index.ts +8 -0
  79. package/src/conductor/catalog/matcher.ts +134 -0
  80. package/src/conductor/conductor.ts +234 -0
  81. package/src/conductor/contract.ts +842 -0
  82. package/src/conductor/diagnostics.ts +227 -0
  83. package/src/conductor/index.ts +33 -0
  84. package/src/conductor/permissions.ts +588 -0
  85. package/src/conductor/quota-error.ts +49 -0
  86. package/src/conductor/signal-hub/hub.ts +46 -0
  87. package/src/conductor/signal-hub/index.ts +2 -0
  88. package/src/conductor/signal-hub/translate.test.ts +81 -0
  89. package/src/conductor/signal-hub/translate.ts +74 -0
  90. package/src/conductor/skill-parse/index.ts +2 -0
  91. package/src/conductor/skill-parse/parse.ts +108 -0
  92. package/src/conductor/transcript-store/index.ts +22 -0
  93. package/src/conductor/transcript-store/serialize.ts +116 -0
  94. package/src/conductor/transcript-store/store.ts +205 -0
  95. package/src/console/auth-status.ts +56 -0
  96. package/src/console/components/AgentsView.ts +165 -0
  97. package/src/console/components/BackgroundAgents.ts +155 -0
  98. package/src/console/components/Banner.ts +334 -0
  99. package/src/console/components/Composer.ts +94 -0
  100. package/src/console/components/StatusBar.ts +49 -0
  101. package/src/console/components/TerminalConsole.ts +1090 -0
  102. package/src/console/components/WorkingIndicator.ts +98 -0
  103. package/src/console/components/banner-sweep.ts +24 -0
  104. package/src/console/components/welcome.ts +74 -0
  105. package/src/console/contract.ts +630 -0
  106. package/src/console/index.ts +34 -0
  107. package/src/console/input/complete.ts +127 -0
  108. package/src/console/input/dir-reader.ts +34 -0
  109. package/src/console/input/index.ts +23 -0
  110. package/src/console/input/keymap.ts +159 -0
  111. package/src/console/input/paste.ts +104 -0
  112. package/src/console/mount.ts +56 -0
  113. package/src/console/overlays/approval-queue.ts +57 -0
  114. package/src/console/overlays/approval.ts +130 -0
  115. package/src/console/overlays/auth.ts +342 -0
  116. package/src/console/overlays/boards.ts +308 -0
  117. package/src/console/overlays/host.ts +36 -0
  118. package/src/console/overlays/index.ts +26 -0
  119. package/src/console/overlays/pickers.ts +258 -0
  120. package/src/console/overlays/sessions.ts +190 -0
  121. package/src/console/reducer.ts +182 -0
  122. package/src/console/slash/builtins.ts +81 -0
  123. package/src/console/slash/commands/dynamic.ts +83 -0
  124. package/src/console/slash/commands/integrations.ts +695 -0
  125. package/src/console/slash/commands/shared.ts +75 -0
  126. package/src/console/slash/commands/transcript.ts +263 -0
  127. package/src/console/slash/commands/workbench.ts +246 -0
  128. package/src/console/slash/index.ts +15 -0
  129. package/src/console/slash/registry.ts +70 -0
  130. package/src/console/slash/resolve.ts +63 -0
  131. package/src/console/startup.ts +209 -0
  132. package/src/console/theme/adapter.ts +45 -0
  133. package/src/console/theme/index.ts +7 -0
  134. package/src/console/theme/palette.ts +68 -0
  135. package/src/console/theme/resolve.ts +39 -0
  136. package/src/console/theme/tokens.ts +71 -0
  137. package/src/entry.ts +55 -0
  138. package/src/guardrails.ts +37 -0
  139. package/src/index.ts +18 -0
  140. package/src/insight/channel.ts +88 -0
  141. package/src/insight/contract.ts +185 -0
  142. package/src/insight/index.ts +110 -0
  143. package/src/insight/recorder.ts +213 -0
  144. package/src/insight/redaction.ts +157 -0
  145. package/src/insight/replay.ts +158 -0
  146. package/src/insight/sampling.ts +70 -0
  147. package/src/insight/serialize.ts +50 -0
  148. package/src/insight/sinks/console.ts +64 -0
  149. package/src/insight/sinks/file.ts +40 -0
  150. package/src/insight/sinks/index.ts +24 -0
  151. package/src/insight/sinks/stream.ts +54 -0
  152. package/src/integrations/sarvam/attach.ts +239 -0
  153. package/src/integrations/sarvam/config.ts +156 -0
  154. package/src/integrations/sarvam/index.ts +25 -0
  155. package/src/integrations/sarvam/sarvam.test.ts +60 -0
  156. package/src/integrations/sarvam/types.ts +27 -0
  157. package/src/integrations/zoho/attach.ts +342 -0
  158. package/src/integrations/zoho/config.ts +125 -0
  159. package/src/integrations/zoho/index.ts +27 -0
  160. package/src/integrations/zoho/types.ts +21 -0
  161. package/src/integrations/zoho/zoho.test.ts +50 -0
  162. package/src/kit/clipboard-image.ts +107 -0
  163. package/src/kit/external-editor.ts +48 -0
  164. package/src/kit/image.ts +59 -0
  165. package/src/kit/index.ts +51 -0
  166. package/src/kit/shell.ts +19 -0
  167. package/src/kit/tool-fetch.ts +85 -0
  168. package/src/launch/catalog.ts +148 -0
  169. package/src/launch/contract.ts +187 -0
  170. package/src/launch/credentials.ts +625 -0
  171. package/src/launch/index.ts +98 -0
  172. package/src/launch/invocation/attachments.ts +179 -0
  173. package/src/launch/invocation/flags.ts +196 -0
  174. package/src/launch/invocation/index.ts +25 -0
  175. package/src/launch/invocation/read.ts +260 -0
  176. package/src/launch/invocation/usage.ts +67 -0
  177. package/src/launch/login.ts +324 -0
  178. package/src/launch/oauth.test.ts +18 -0
  179. package/src/launch/oauth.ts +203 -0
  180. package/src/launch/packages.ts +194 -0
  181. package/src/launch/pickers.ts +189 -0
  182. package/src/runtime-bridge/bridges/_drive.ts +96 -0
  183. package/src/runtime-bridge/bridges/builtins.ts +68 -0
  184. package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
  185. package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
  186. package/src/runtime-bridge/bridges/index.ts +33 -0
  187. package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
  188. package/src/runtime-bridge/broker.ts +227 -0
  189. package/src/runtime-bridge/contract.ts +122 -0
  190. package/src/runtime-bridge/index.ts +79 -0
  191. package/src/runtime-bridge/sink.ts +180 -0
  192. package/src/sessions/contract.ts +81 -0
  193. package/src/sessions/index.ts +13 -0
  194. package/src/sessions/library.ts +229 -0
  195. package/src/settings/contract.ts +114 -0
  196. package/src/settings/index.ts +32 -0
  197. package/src/settings/manager.ts +117 -0
  198. package/src/transcript-export/index.ts +45 -0
  199. package/src/transcript-export/publish.ts +260 -0
  200. package/src/transcript-export/sgr.ts +315 -0
  201. package/src/transcript-export/template.ts +272 -0
  202. package/src/transcript-export/theme-bridge.ts +150 -0
  203. package/src/window-budget/budget/estimate.ts +135 -0
  204. package/src/window-budget/budget/gate.ts +33 -0
  205. package/src/window-budget/budget/index.ts +16 -0
  206. package/src/window-budget/budget/slice.ts +56 -0
  207. package/src/window-budget/condenser.ts +58 -0
  208. package/src/window-budget/contract.ts +184 -0
  209. package/src/window-budget/index.ts +19 -0
  210. package/src/window-budget/microcompact.ts +95 -0
  211. package/src/window-budget/rehydrate.ts +136 -0
  212. package/src/window-budget/summarize/condense.ts +103 -0
  213. package/src/window-budget/summarize/index.ts +14 -0
  214. package/src/window-budget/summarize/prompt.ts +149 -0
  215. package/src/workflow-engine/agent-runner.ts +181 -0
  216. package/src/workflow-engine/display.ts +224 -0
  217. package/src/workflow-engine/engine.ts +294 -0
  218. package/src/workflow-engine/index.ts +23 -0
  219. package/src/workflow-engine/parse.ts +172 -0
  220. package/src/workflow-engine/structured-output.ts +35 -0
  221. package/src/workspace/brand.ts +29 -0
  222. package/src/workspace/index.ts +18 -0
  223. package/src/workspace/locator.ts +103 -0
  224. package/src/workspace/runtime-detect.ts +64 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Addon sandbox — the SINGLE jiti-backed {@link ModuleLoader}.
3
+ *
4
+ * This module turns a path on disk into a loaded {@link AddonManifest}.
5
+ */
6
+ import { createJiti } from "jiti";
7
+ import { homedir } from "node:os";
8
+ import { isAbsolute, resolve } from "node:path";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import * as agentNamespace from "indusagi/agent";
11
+ import * as aiNamespace from "indusagi/ai";
12
+ import * as tuiNamespace from "indusagi/tui";
13
+ import { isPackagedBinary } from "../workspace/runtime-detect.js";
14
+ import type {
15
+ AddonManifest,
16
+ BundledNamespace,
17
+ ModuleLoader,
18
+ } from "./contract.js";
19
+ import { BUNDLED_NAMESPACES } from "./contract.js";
20
+
21
+ const NAMESPACE_OBJECTS: Readonly<Record<BundledNamespace, unknown>> = {
22
+ "indusagi/agent": agentNamespace,
23
+ "indusagi/ai": aiNamespace,
24
+ "indusagi/tui": tuiNamespace,
25
+ "@sinclair/typebox": {},
26
+ };
27
+
28
+ const INVISIBLE_CODE_POINTS: readonly number[] = [
29
+ 160, 8203, 8204, 8205, 8206, 8207, 8288, 65279, 8239, 8287, 12288,
30
+ 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202,
31
+ ];
32
+
33
+ const INVISIBLE_SET = new Set<number>(INVISIBLE_CODE_POINTS);
34
+
35
+ export function scrubInvisible(raw: string): string {
36
+ let out = "";
37
+ for (const char of raw) {
38
+ const code = char.codePointAt(0);
39
+ if (code !== undefined && INVISIBLE_SET.has(code)) {
40
+ continue;
41
+ }
42
+ out += char;
43
+ }
44
+ return out.trim();
45
+ }
46
+
47
+ export function expandPath(raw: string, home: string = homedir()): string {
48
+ const cleaned = scrubInvisible(raw);
49
+ if (cleaned === "~") {
50
+ return home;
51
+ }
52
+ if (cleaned.startsWith("~/") || cleaned.startsWith("~\\")) {
53
+ return home + cleaned.slice(1);
54
+ }
55
+ return cleaned;
56
+ }
57
+
58
+ export function resolvePath(
59
+ raw: string,
60
+ base: string = process.cwd(),
61
+ home: string = homedir(),
62
+ ): string {
63
+ const expanded = expandPath(raw, home);
64
+ return isAbsolute(expanded) ? resolve(expanded) : resolve(base, expanded);
65
+ }
66
+
67
+ function buildVirtualModules(): Record<string, unknown> {
68
+ const map: Record<string, unknown> = {};
69
+ for (const specifier of BUNDLED_NAMESPACES) {
70
+ map[specifier] = NAMESPACE_OBJECTS[specifier];
71
+ }
72
+ return map;
73
+ }
74
+
75
+ function resolveSpecifierPath(specifier: string): string | undefined {
76
+ try {
77
+ const resolved = import.meta.resolve(specifier);
78
+ return resolved.startsWith("file:") ? fileURLToPath(resolved) : resolved;
79
+ } catch {
80
+ return undefined;
81
+ }
82
+ }
83
+
84
+ function buildAliasMap(): Record<string, string> {
85
+ const map: Record<string, string> = {};
86
+ for (const specifier of BUNDLED_NAMESPACES) {
87
+ const path = resolveSpecifierPath(specifier);
88
+ if (path !== undefined) {
89
+ map[specifier] = path;
90
+ }
91
+ }
92
+ return map;
93
+ }
94
+
95
+ interface JitiOptions {
96
+ moduleCache?: boolean;
97
+ virtualModules?: Record<string, unknown>;
98
+ alias?: Record<string, string>;
99
+ tryNative?: boolean;
100
+ }
101
+
102
+ function jitiOptionsForRuntime(): JitiOptions {
103
+ const base = { moduleCache: false };
104
+ if (isPackagedBinary()) {
105
+ return { ...base, virtualModules: buildVirtualModules(), tryNative: false };
106
+ }
107
+ return { ...base, alias: buildAliasMap() };
108
+ }
109
+
110
+ function isObject(value: unknown): value is Record<string, unknown> {
111
+ return typeof value === "object" && value !== null;
112
+ }
113
+
114
+ function pickManifestObject(imported: unknown): Record<string, unknown> | undefined {
115
+ if (!isObject(imported)) {
116
+ return undefined;
117
+ }
118
+ const direct = imported;
119
+ if (typeof direct.register === "function") {
120
+ return imported;
121
+ }
122
+ const fallback = imported.default;
123
+ return isObject(fallback) ? fallback : imported;
124
+ }
125
+
126
+ function toManifest(imported: unknown): AddonManifest | undefined {
127
+ const candidate = pickManifestObject(imported);
128
+ if (candidate === undefined) {
129
+ return undefined;
130
+ }
131
+ const register = candidate.register;
132
+ if (typeof register !== "function") {
133
+ return undefined;
134
+ }
135
+ const id = candidate.id;
136
+ const version = candidate.version;
137
+ return {
138
+ register: register.bind(candidate),
139
+ ...(typeof id === "string" ? { id } : {}),
140
+ ...(typeof version === "string" ? { version } : {}),
141
+ };
142
+ }
143
+
144
+ export interface JitiLoaderOptions {
145
+ readonly base?: string;
146
+ readonly home?: string;
147
+ }
148
+
149
+ /**
150
+ * Construct the default jiti-backed {@link ModuleLoader}.
151
+ */
152
+ export function createJitiLoader(options: JitiLoaderOptions = {}): ModuleLoader {
153
+ const jiti = createJiti(import.meta.url, jitiOptionsForRuntime() as Parameters<typeof createJiti>[1]);
154
+ const base = options.base ?? process.cwd();
155
+ const home = options.home ?? homedir();
156
+ return {
157
+ async load(path: string): Promise<AddonManifest> {
158
+ const absolute = resolvePath(path, base, home);
159
+ const specifier = pathToFileURL(absolute).href;
160
+ const imported = (await jiti.import(specifier)) as unknown;
161
+ const manifest = toManifest(imported);
162
+ if (manifest === undefined) {
163
+ throw new Error(
164
+ `addon at ${absolute} does not export a register() entry point`,
165
+ );
166
+ }
167
+ return manifest;
168
+ },
169
+ };
170
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * AddonSurface implementation — the recording registration API.
3
+ */
4
+ import type {
5
+ AddonCommand,
6
+ AddonId,
7
+ AddonSurface,
8
+ AddonTool,
9
+ EventSubscription,
10
+ FrameworkHandles,
11
+ HookEvent,
12
+ HookHandler,
13
+ RegisteredManifest,
14
+ ToolInterceptor,
15
+ } from "./contract.js";
16
+
17
+ class RecordingSurface implements AddonSurface {
18
+ readonly id: AddonId;
19
+ readonly handles: FrameworkHandles;
20
+ readonly version: string | undefined;
21
+ readonly subscriptions: EventSubscription[] = [];
22
+ readonly interceptors: ToolInterceptor[] = [];
23
+ readonly commands: AddonCommand[] = [];
24
+ readonly tools: AddonTool[] = [];
25
+
26
+ constructor(id: AddonId, handles: FrameworkHandles, version?: string) {
27
+ this.id = id;
28
+ this.handles = handles;
29
+ this.version = version;
30
+ }
31
+
32
+ on<TPayload>(event: HookEvent, handler: HookHandler<TPayload>): void {
33
+ this.subscriptions.push({
34
+ event,
35
+ handler: handler as HookHandler,
36
+ addon: this.id,
37
+ });
38
+ }
39
+
40
+ interceptTool(
41
+ name: string | "*",
42
+ interceptor: Omit<ToolInterceptor, "match" | "addon">,
43
+ ): void {
44
+ this.interceptors.push({ ...interceptor, match: name, addon: this.id });
45
+ }
46
+
47
+ addCommand(name: string, handler: Omit<AddonCommand, "name" | "addon">): void {
48
+ this.commands.push({ ...handler, name, addon: this.id });
49
+ }
50
+
51
+ addTool(card: AddonTool): void {
52
+ this.tools.push(card);
53
+ }
54
+
55
+ manifest(): RegisteredManifest {
56
+ const base = {
57
+ addon: this.id,
58
+ subscriptions: Object.freeze([...this.subscriptions]),
59
+ interceptors: Object.freeze([...this.interceptors]),
60
+ commands: Object.freeze([...this.commands]),
61
+ tools: Object.freeze([...this.tools]),
62
+ };
63
+ return Object.freeze(
64
+ this.version === undefined ? base : { ...base, version: this.version },
65
+ );
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Mint a fresh {@link AddonSurface} for one addon.
71
+ */
72
+ export function createSurface(
73
+ id: AddonId,
74
+ handles: FrameworkHandles,
75
+ version?: string,
76
+ ): AddonSurface {
77
+ return new RecordingSurface(id, handles, version);
78
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Disk-backed credential vault — the boot-layer adapter that satisfies the
3
+ * launch {@link AuthVault} seam so the credential command can persist
4
+ * credentials before the concrete Phase-2 multi-account vault lands.
5
+ *
6
+ * The store is a single JSON file (`auth.json` under the profile directory),
7
+ * keyed provider → account → record. Each record is a discriminated union: it
8
+ * holds *either* a stored api key *or* a set of browser-sign-in credentials, plus
9
+ * a default marker. The adapter is deliberately small and self-contained: it
10
+ * reads / writes the whole file each time (the file is tiny and the operations
11
+ * are interactive, so atomic-rewrite simplicity beats incremental I/O).
12
+ *
13
+ * Two kinds of read are offered. The default-account lookup is plain bookkeeping;
14
+ * {@link AuthVault.readUsableKey} resolves a record to a live api-key string — for
15
+ * an api-key record it returns the stored key verbatim, and for a browser-sign-in
16
+ * record it asks the framework to refresh an expired token (persisting the
17
+ * rotated credentials back to disk) before handing back the usable key.
18
+ *
19
+ * This is *not* the Phase-2 vault — it is the minimal real persistence the
20
+ * launch wiring needs. When the concrete multi-account vault arrives it replaces
21
+ * this adapter at the one call site in {@link "./boot"}.
22
+ */
23
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
24
+ import { dirname } from "node:path";
25
+ import {
26
+ ensureFreshOAuthCredentials,
27
+ getOAuthProvider,
28
+ } from "indusagi/ai";
29
+ import type { AuthVault } from "../launch.js";
30
+
31
+ interface OAuthRecord {
32
+ access: string;
33
+ refresh: string;
34
+ expires?: number;
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ interface ApiKeyRecord {
39
+ kind: "apiKey";
40
+ key: string;
41
+ isDefault: boolean;
42
+ }
43
+
44
+ interface OAuthStoredRecord extends OAuthRecord {
45
+ kind: "oauth";
46
+ isDefault: boolean;
47
+ }
48
+
49
+ type NormalisedRecord = ApiKeyRecord | OAuthStoredRecord;
50
+
51
+ function normaliseRecord(value: unknown): NormalisedRecord | undefined {
52
+ if (value === null || typeof value !== "object") return undefined;
53
+ const raw = value as Record<string, unknown>;
54
+ const isDefault = raw.isDefault === true;
55
+ if (raw.kind === "oauth") {
56
+ if (typeof raw.access === "string" && typeof raw.refresh === "string") {
57
+ return { ...raw, kind: "oauth", isDefault } as OAuthStoredRecord;
58
+ }
59
+ return undefined;
60
+ }
61
+ if (raw.kind === "apiKey" && typeof raw.key === "string") {
62
+ return { kind: "apiKey", key: raw.key, isDefault };
63
+ }
64
+ if (typeof raw.apiKey === "string") {
65
+ return { kind: "apiKey", key: raw.apiKey, isDefault };
66
+ }
67
+ return undefined;
68
+ }
69
+
70
+ function readAuthFile(filePath: string): Record<string, Record<string, NormalisedRecord>> {
71
+ try {
72
+ const text = readFileSync(filePath, "utf8");
73
+ const parsed = JSON.parse(text);
74
+ if (parsed === null || typeof parsed !== "object") return {};
75
+ const file: Record<string, Record<string, NormalisedRecord>> = {};
76
+ for (const [provider, accounts] of Object.entries(parsed)) {
77
+ if (accounts === null || typeof accounts !== "object") continue;
78
+ const normalised: Record<string, NormalisedRecord> = {};
79
+ for (const [account, record] of Object.entries(accounts)) {
80
+ const value = normaliseRecord(record);
81
+ if (value !== undefined) normalised[account] = value;
82
+ }
83
+ file[provider] = normalised;
84
+ }
85
+ return file;
86
+ } catch {
87
+ return {};
88
+ }
89
+ }
90
+
91
+ function writeAuthFile(
92
+ filePath: string,
93
+ data: Record<string, Record<string, NormalisedRecord>>,
94
+ ): void {
95
+ mkdirSync(dirname(filePath), { recursive: true });
96
+ writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, { mode: 384 });
97
+ }
98
+
99
+ function toCredentials(record: OAuthStoredRecord): OAuthRecord {
100
+ const { kind: _kind, isDefault: _isDefault, ...credentials } = record;
101
+ return credentials as OAuthRecord;
102
+ }
103
+
104
+ function clearDefaults(accounts: Record<string, NormalisedRecord>): void {
105
+ for (const name of Object.keys(accounts)) {
106
+ accounts[name] = { ...accounts[name], isDefault: false };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Build a disk-backed {@link AuthVault} persisting to `authPath`.
112
+ *
113
+ * @param authPath absolute path of the JSON credential store (e.g. `auth.json`)
114
+ */
115
+ export function createAuthVault(authPath: string): AuthVault {
116
+ return {
117
+ async listAccounts(provider) {
118
+ const file = readAuthFile(authPath);
119
+ return Object.keys(file[provider] ?? {});
120
+ },
121
+ async defaultAccount(provider) {
122
+ const file = readAuthFile(authPath);
123
+ const accounts = file[provider] ?? {};
124
+ for (const [account, record] of Object.entries(accounts)) {
125
+ if (record.isDefault) return account;
126
+ }
127
+ return undefined;
128
+ },
129
+ async putApiKey(provider, account, apiKey, makeDefault = false) {
130
+ const file = readAuthFile(authPath);
131
+ const accounts: Record<string, NormalisedRecord> = { ...(file[provider] ?? {}) };
132
+ if (makeDefault) clearDefaults(accounts);
133
+ accounts[account] = { kind: "apiKey", key: apiKey, isDefault: makeDefault };
134
+ file[provider] = accounts;
135
+ writeAuthFile(authPath, file);
136
+ },
137
+ async putOAuth(provider, account, credentials, makeDefault = false) {
138
+ const file = readAuthFile(authPath);
139
+ const accounts: Record<string, NormalisedRecord> = { ...(file[provider] ?? {}) };
140
+ if (makeDefault) clearDefaults(accounts);
141
+ accounts[account] = { ...credentials, kind: "oauth", isDefault: makeDefault };
142
+ file[provider] = accounts;
143
+ writeAuthFile(authPath, file);
144
+ },
145
+ async authKind(provider, account) {
146
+ const file = readAuthFile(authPath);
147
+ return file[provider]?.[account]?.kind;
148
+ },
149
+ async readUsableKey(provider, account) {
150
+ const file = readAuthFile(authPath);
151
+ const record = file[provider]?.[account];
152
+ if (record === undefined) return undefined;
153
+ if (record.kind === "apiKey") return record.key;
154
+ const handler = getOAuthProvider(provider);
155
+ if (handler === undefined) return undefined;
156
+ const current = toCredentials(record);
157
+ const result = await ensureFreshOAuthCredentials(provider, current);
158
+ if (result.refreshed) {
159
+ const accounts: Record<string, NormalisedRecord> = { ...(file[provider] ?? {}) };
160
+ accounts[account] = {
161
+ ...result.credentials,
162
+ kind: "oauth",
163
+ isDefault: record.isDefault,
164
+ };
165
+ file[provider] = accounts;
166
+ writeAuthFile(authPath, file);
167
+ }
168
+ return handler.getApiKey(result.credentials);
169
+ },
170
+ async remove(provider, account) {
171
+ const file = readAuthFile(authPath);
172
+ const accounts = file[provider];
173
+ if (accounts === undefined) return false;
174
+ if (account === undefined) {
175
+ delete file[provider];
176
+ writeAuthFile(authPath, file);
177
+ return true;
178
+ }
179
+ if (accounts[account] === undefined) return false;
180
+ const wasDefault = accounts[account].isDefault;
181
+ delete accounts[account];
182
+ const survivors = Object.keys(accounts);
183
+ if (wasDefault && survivors.length > 0) {
184
+ accounts[survivors[0]] = { ...accounts[survivors[0]], isDefault: true };
185
+ }
186
+ if (survivors.length === 0) {
187
+ delete file[provider];
188
+ } else {
189
+ file[provider] = accounts;
190
+ }
191
+ writeAuthFile(authPath, file);
192
+ return true;
193
+ },
194
+ };
195
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Boot orchestrator — the single function the OS entry point calls.
3
+ *
4
+ * {@link boot} owns the whole launch arc: it assembles the seed
5
+ * {@link BootContext} (resolved workspace + brand + a placeholder invocation),
6
+ * runs the ordered {@link runStages | stage pipeline} to resolve the rest
7
+ * (parse the command line, materialise directories, apply upgrades, build the
8
+ * startup-resource graph), handles the meta requests (`--help` / `--version`)
9
+ * that short-circuit before any runner, then dispatches to the
10
+ * {@link selectRunner | selected runner} and returns its exit code.
11
+ *
12
+ * Cleanup is guaranteed: every teardown callback accumulated in
13
+ * {@link BootContext.closables} is drained in a `finally`, latest-registered
14
+ * first, regardless of how the runner resolved. {@link boot} itself never throws
15
+ * to its caller for an expected failure — it maps problems to a non-zero exit
16
+ * code so the entry point can simply set `process.exitCode`.
17
+ */
18
+ import { BRAND, VERSION } from "../workspace/brand.js";
19
+ import { formatCredentialFault, runCredentialCommand } from "../launch/credentials.js";
20
+ import { runPackageCommand } from "../launch/packages.js";
21
+ import { wantsHelp, wantsVersion } from "./invocation.js";
22
+ import { printModelCatalog } from "../launch/catalog.js";
23
+ import { renderUsage } from "../launch/invocation/usage.js";
24
+ import type { BootContext } from "./contract.js";
25
+ import { runStages } from "./stages.js";
26
+ import { selectRunner } from "./runners/registry.js";
27
+ import { createAuthVault } from "./auth-vault.js";
28
+ import { runLogin } from "../launch/login.js";
29
+ import { createWorkspace } from "../workspace/locator.js";
30
+ import { PreferenceStore } from "../settings/manager.js";
31
+ import { tokenizeInvocation } from "./invocation.js";
32
+
33
+ const EXIT_OK = 0;
34
+ const EXIT_FAILURE = 1;
35
+ const EXIT_CREDENTIAL_FAULT = 1;
36
+
37
+ function seedContext(argv: string[]): BootContext {
38
+ const workspace = createWorkspace();
39
+ const invocation = tokenizeInvocation(argv);
40
+ return {
41
+ argv,
42
+ workspace,
43
+ brand: BRAND,
44
+ invocation,
45
+ closables: [],
46
+ };
47
+ }
48
+
49
+ function renderHelp(): void {
50
+ process.stdout.write(`${renderUsage()}\n`);
51
+ }
52
+
53
+ function renderVersion(): void {
54
+ process.stdout.write(`${BRAND.name} ${VERSION}\n`);
55
+ }
56
+
57
+ async function drainClosables(ctx: BootContext): Promise<void> {
58
+ for (const close of [...ctx.closables].reverse()) {
59
+ try {
60
+ await close();
61
+ } catch {
62
+ // ignore teardown errors
63
+ }
64
+ }
65
+ }
66
+
67
+ async function runCredential(ctx: BootContext): Promise<number | undefined> {
68
+ const result = await runCredentialCommand(ctx.argv, {
69
+ vault: createAuthVault(ctx.workspace.authPath),
70
+ profileDir: ctx.workspace.profileDir,
71
+ });
72
+ if (!result.handled) {
73
+ return undefined;
74
+ }
75
+ if (result.fault !== undefined) {
76
+ process.stderr.write(`${formatCredentialFault(result.fault)}\n`);
77
+ return EXIT_CREDENTIAL_FAULT;
78
+ }
79
+ return EXIT_OK;
80
+ }
81
+
82
+ async function runPackage(ctx: BootContext): Promise<number | undefined> {
83
+ const result = await runPackageCommand(ctx.argv, {
84
+ store: PreferenceStore.fromWorkspace(ctx.workspace, ctx.invocation.cwd),
85
+ });
86
+ if (!result.handled) {
87
+ return undefined;
88
+ }
89
+ return result.code ?? EXIT_OK;
90
+ }
91
+
92
+ /**
93
+ * Boot the agent: resolve everything, dispatch to the chosen runner, and return
94
+ * the process exit code.
95
+ *
96
+ * @param argv The arguments the launch was invoked with, already sliced (no
97
+ * node/exec path).
98
+ * @returns The exit code the process should adopt.
99
+ */
100
+ export async function boot(argv: string[]): Promise<number> {
101
+ const seeded = seedContext(argv);
102
+ const credential = await runCredential(seeded);
103
+ if (credential !== undefined) {
104
+ return credential;
105
+ }
106
+ const login = await runLogin(seeded);
107
+ if (login !== undefined) {
108
+ return login;
109
+ }
110
+ const pkg = await runPackage(seeded);
111
+ if (pkg !== undefined) {
112
+ return pkg;
113
+ }
114
+ if (wantsHelp(seeded.invocation)) {
115
+ renderHelp();
116
+ return EXIT_OK;
117
+ }
118
+ if (wantsVersion(seeded.invocation)) {
119
+ renderVersion();
120
+ return EXIT_OK;
121
+ }
122
+ if (seeded.invocation.listModels) {
123
+ const filter = seeded.invocation.listModelsFilter;
124
+ printModelCatalog(undefined, filter !== undefined ? { search: filter } : {});
125
+ return EXIT_OK;
126
+ }
127
+ const ctx = await runStages(seeded);
128
+ try {
129
+ const runner = selectRunner(ctx.invocation);
130
+ return await runner.run(ctx);
131
+ } catch (error) {
132
+ const message = error instanceof Error ? error.message : String(error);
133
+ process.stderr.write(`${BRAND.name}: ${message}\n`);
134
+ return EXIT_FAILURE;
135
+ } finally {
136
+ await drainClosables(ctx);
137
+ }
138
+ }