indusagi-coding-agent 0.2.8 → 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 (219) hide show
  1. package/package.json +2 -1
  2. package/src/_decls/entry.ts +18 -0
  3. package/src/_decls/guardrails.ts +35 -0
  4. package/src/_decls/index.ts +26 -0
  5. package/src/addons/contract.ts +236 -0
  6. package/src/addons/dispatch/event-dispatcher.ts +164 -0
  7. package/src/addons/dispatch/index.ts +25 -0
  8. package/src/addons/dispatch/tool-interceptor.ts +208 -0
  9. package/src/addons/host.ts +225 -0
  10. package/src/addons/index.ts +112 -0
  11. package/src/addons/manifest.ts +158 -0
  12. package/src/addons/sandbox.ts +170 -0
  13. package/src/addons/surface.ts +78 -0
  14. package/src/boot/auth-vault.ts +195 -0
  15. package/src/boot/boot.ts +138 -0
  16. package/src/boot/contract.ts +238 -0
  17. package/src/boot/heap.ts +59 -0
  18. package/src/boot/index.ts +28 -0
  19. package/src/boot/invocation.ts +93 -0
  20. package/src/boot/runners/addon-wiring.ts +153 -0
  21. package/src/boot/runners/checkpoint.ts +169 -0
  22. package/src/boot/runners/delegate-runner.ts +294 -0
  23. package/src/boot/runners/index.ts +13 -0
  24. package/src/boot/runners/link-runner.ts +45 -0
  25. package/src/boot/runners/memdir.ts +168 -0
  26. package/src/boot/runners/oneshot-runner.ts +58 -0
  27. package/src/boot/runners/read-state.ts +90 -0
  28. package/src/boot/runners/registry.ts +42 -0
  29. package/src/boot/runners/repl-runner.ts +143 -0
  30. package/src/boot/runners/server-mode.ts +121 -0
  31. package/src/boot/runners/session.ts +641 -0
  32. package/src/boot/server-token.ts +148 -0
  33. package/src/boot/stages.ts +167 -0
  34. package/src/boot/upgrade/apply.ts +94 -0
  35. package/src/boot/upgrade/index.ts +13 -0
  36. package/src/boot/upgrade/upgrades.ts +289 -0
  37. package/src/briefing/compose.ts +150 -0
  38. package/src/briefing/context-docs.ts +19 -0
  39. package/src/briefing/contract.ts +717 -0
  40. package/src/briefing/index.ts +31 -0
  41. package/src/briefing/macros.ts +97 -0
  42. package/src/briefing/skills.ts +47 -0
  43. package/src/capability-deck/bridge-ledger/index.ts +27 -0
  44. package/src/capability-deck/bridge-ledger/key.ts +67 -0
  45. package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
  46. package/src/capability-deck/bridge-ledger/network.ts +117 -0
  47. package/src/capability-deck/builtin-bridge.ts +312 -0
  48. package/src/capability-deck/cards/bg-process-card.ts +335 -0
  49. package/src/capability-deck/cards/index.ts +115 -0
  50. package/src/capability-deck/cards/memory-card.ts +146 -0
  51. package/src/capability-deck/cards/plan-file.ts +97 -0
  52. package/src/capability-deck/cards/plan-tools.ts +185 -0
  53. package/src/capability-deck/cards/saas-card.ts +183 -0
  54. package/src/capability-deck/cards/task-card.ts +207 -0
  55. package/src/capability-deck/cards/todo-card.ts +168 -0
  56. package/src/capability-deck/cards/workflow-card.ts +247 -0
  57. package/src/capability-deck/contract.ts +388 -0
  58. package/src/capability-deck/index.ts +48 -0
  59. package/src/capability-deck/manifest.ts +109 -0
  60. package/src/capability-deck/provision.ts +169 -0
  61. package/src/channels/contract.ts +191 -0
  62. package/src/channels/framer.ts +50 -0
  63. package/src/channels/index.ts +101 -0
  64. package/src/channels/link/dialog.ts +129 -0
  65. package/src/channels/link/driver.ts +190 -0
  66. package/src/channels/link/index.ts +34 -0
  67. package/src/channels/link/server.ts +134 -0
  68. package/src/channels/oneshot.ts +90 -0
  69. package/src/channels/ops.ts +65 -0
  70. package/src/channels/session-ops.ts +81 -0
  71. package/src/conductor/bash-guard.ts +599 -0
  72. package/src/conductor/catalog/catalog.ts +116 -0
  73. package/src/conductor/catalog/index.ts +8 -0
  74. package/src/conductor/catalog/matcher.ts +134 -0
  75. package/src/conductor/conductor.ts +234 -0
  76. package/src/conductor/contract.ts +842 -0
  77. package/src/conductor/diagnostics.ts +227 -0
  78. package/src/conductor/index.ts +33 -0
  79. package/src/conductor/permissions.ts +588 -0
  80. package/src/conductor/quota-error.ts +49 -0
  81. package/src/conductor/signal-hub/hub.ts +46 -0
  82. package/src/conductor/signal-hub/index.ts +2 -0
  83. package/src/conductor/signal-hub/translate.test.ts +81 -0
  84. package/src/conductor/signal-hub/translate.ts +74 -0
  85. package/src/conductor/skill-parse/index.ts +2 -0
  86. package/src/conductor/skill-parse/parse.ts +108 -0
  87. package/src/conductor/transcript-store/index.ts +22 -0
  88. package/src/conductor/transcript-store/serialize.ts +116 -0
  89. package/src/conductor/transcript-store/store.ts +205 -0
  90. package/src/console/auth-status.ts +56 -0
  91. package/src/console/components/AgentsView.ts +165 -0
  92. package/src/console/components/BackgroundAgents.ts +155 -0
  93. package/src/console/components/Banner.ts +334 -0
  94. package/src/console/components/Composer.ts +94 -0
  95. package/src/console/components/StatusBar.ts +49 -0
  96. package/src/console/components/TerminalConsole.ts +1090 -0
  97. package/src/console/components/WorkingIndicator.ts +98 -0
  98. package/src/console/components/banner-sweep.ts +24 -0
  99. package/src/console/components/welcome.ts +74 -0
  100. package/src/console/contract.ts +630 -0
  101. package/src/console/index.ts +34 -0
  102. package/src/console/input/complete.ts +127 -0
  103. package/src/console/input/dir-reader.ts +34 -0
  104. package/src/console/input/index.ts +23 -0
  105. package/src/console/input/keymap.ts +159 -0
  106. package/src/console/input/paste.ts +104 -0
  107. package/src/console/mount.ts +56 -0
  108. package/src/console/overlays/approval-queue.ts +57 -0
  109. package/src/console/overlays/approval.ts +130 -0
  110. package/src/console/overlays/auth.ts +342 -0
  111. package/src/console/overlays/boards.ts +308 -0
  112. package/src/console/overlays/host.ts +36 -0
  113. package/src/console/overlays/index.ts +26 -0
  114. package/src/console/overlays/pickers.ts +258 -0
  115. package/src/console/overlays/sessions.ts +190 -0
  116. package/src/console/reducer.ts +182 -0
  117. package/src/console/slash/builtins.ts +81 -0
  118. package/src/console/slash/commands/dynamic.ts +83 -0
  119. package/src/console/slash/commands/integrations.ts +695 -0
  120. package/src/console/slash/commands/shared.ts +75 -0
  121. package/src/console/slash/commands/transcript.ts +263 -0
  122. package/src/console/slash/commands/workbench.ts +246 -0
  123. package/src/console/slash/index.ts +15 -0
  124. package/src/console/slash/registry.ts +70 -0
  125. package/src/console/slash/resolve.ts +63 -0
  126. package/src/console/startup.ts +209 -0
  127. package/src/console/theme/adapter.ts +45 -0
  128. package/src/console/theme/index.ts +7 -0
  129. package/src/console/theme/palette.ts +68 -0
  130. package/src/console/theme/resolve.ts +39 -0
  131. package/src/console/theme/tokens.ts +71 -0
  132. package/src/entry.ts +55 -0
  133. package/src/guardrails.ts +37 -0
  134. package/src/index.ts +18 -0
  135. package/src/insight/channel.ts +88 -0
  136. package/src/insight/contract.ts +185 -0
  137. package/src/insight/index.ts +110 -0
  138. package/src/insight/recorder.ts +213 -0
  139. package/src/insight/redaction.ts +157 -0
  140. package/src/insight/replay.ts +158 -0
  141. package/src/insight/sampling.ts +70 -0
  142. package/src/insight/serialize.ts +50 -0
  143. package/src/insight/sinks/console.ts +64 -0
  144. package/src/insight/sinks/file.ts +40 -0
  145. package/src/insight/sinks/index.ts +24 -0
  146. package/src/insight/sinks/stream.ts +54 -0
  147. package/src/integrations/sarvam/attach.ts +239 -0
  148. package/src/integrations/sarvam/config.ts +156 -0
  149. package/src/integrations/sarvam/index.ts +25 -0
  150. package/src/integrations/sarvam/sarvam.test.ts +60 -0
  151. package/src/integrations/sarvam/types.ts +27 -0
  152. package/src/integrations/zoho/attach.ts +342 -0
  153. package/src/integrations/zoho/config.ts +125 -0
  154. package/src/integrations/zoho/index.ts +27 -0
  155. package/src/integrations/zoho/types.ts +21 -0
  156. package/src/integrations/zoho/zoho.test.ts +50 -0
  157. package/src/kit/clipboard-image.ts +107 -0
  158. package/src/kit/external-editor.ts +48 -0
  159. package/src/kit/image.ts +59 -0
  160. package/src/kit/index.ts +51 -0
  161. package/src/kit/shell.ts +19 -0
  162. package/src/kit/tool-fetch.ts +85 -0
  163. package/src/launch/catalog.ts +148 -0
  164. package/src/launch/contract.ts +187 -0
  165. package/src/launch/credentials.ts +625 -0
  166. package/src/launch/index.ts +98 -0
  167. package/src/launch/invocation/attachments.ts +179 -0
  168. package/src/launch/invocation/flags.ts +196 -0
  169. package/src/launch/invocation/index.ts +25 -0
  170. package/src/launch/invocation/read.ts +260 -0
  171. package/src/launch/invocation/usage.ts +67 -0
  172. package/src/launch/login.ts +324 -0
  173. package/src/launch/oauth.test.ts +18 -0
  174. package/src/launch/oauth.ts +203 -0
  175. package/src/launch/packages.ts +194 -0
  176. package/src/launch/pickers.ts +189 -0
  177. package/src/runtime-bridge/bridges/_drive.ts +96 -0
  178. package/src/runtime-bridge/bridges/builtins.ts +68 -0
  179. package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
  180. package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
  181. package/src/runtime-bridge/bridges/index.ts +33 -0
  182. package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
  183. package/src/runtime-bridge/broker.ts +227 -0
  184. package/src/runtime-bridge/contract.ts +122 -0
  185. package/src/runtime-bridge/index.ts +79 -0
  186. package/src/runtime-bridge/sink.ts +180 -0
  187. package/src/sessions/contract.ts +81 -0
  188. package/src/sessions/index.ts +13 -0
  189. package/src/sessions/library.ts +229 -0
  190. package/src/settings/contract.ts +114 -0
  191. package/src/settings/index.ts +32 -0
  192. package/src/settings/manager.ts +117 -0
  193. package/src/transcript-export/index.ts +45 -0
  194. package/src/transcript-export/publish.ts +260 -0
  195. package/src/transcript-export/sgr.ts +315 -0
  196. package/src/transcript-export/template.ts +272 -0
  197. package/src/transcript-export/theme-bridge.ts +150 -0
  198. package/src/window-budget/budget/estimate.ts +135 -0
  199. package/src/window-budget/budget/gate.ts +33 -0
  200. package/src/window-budget/budget/index.ts +16 -0
  201. package/src/window-budget/budget/slice.ts +56 -0
  202. package/src/window-budget/condenser.ts +58 -0
  203. package/src/window-budget/contract.ts +184 -0
  204. package/src/window-budget/index.ts +19 -0
  205. package/src/window-budget/microcompact.ts +95 -0
  206. package/src/window-budget/rehydrate.ts +136 -0
  207. package/src/window-budget/summarize/condense.ts +103 -0
  208. package/src/window-budget/summarize/index.ts +14 -0
  209. package/src/window-budget/summarize/prompt.ts +149 -0
  210. package/src/workflow-engine/agent-runner.ts +181 -0
  211. package/src/workflow-engine/display.ts +224 -0
  212. package/src/workflow-engine/engine.ts +294 -0
  213. package/src/workflow-engine/index.ts +23 -0
  214. package/src/workflow-engine/parse.ts +172 -0
  215. package/src/workflow-engine/structured-output.ts +35 -0
  216. package/src/workspace/brand.ts +29 -0
  217. package/src/workspace/index.ts +18 -0
  218. package/src/workspace/locator.ts +103 -0
  219. package/src/workspace/runtime-detect.ts +64 -0
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Server-token store — the boot-layer persistence for the indus device-login
3
+ * session token. This is the app side of the login + model-gateway contract: a
4
+ * successful `indus login` device flow writes the better-auth session token
5
+ * here, and the key resolver later reads it back to drive "server mode" (Case B
6
+ * — no local provider key, but a valid server session, so requests are routed
7
+ * through the indus-server gateway with the session token as the api key).
8
+ *
9
+ * The token lives in a single JSON file under the app's profile directory
10
+ * (`~/.indusagi/agent/server-token.json`) — deliberately NOT the framework's
11
+ * `~/.better-auth` location, so the CLI session is isolated from any other
12
+ * tooling. As with the credential vault, the file is tiny and access is
13
+ * interactive, so we read / write the whole file each time and tolerate a
14
+ * missing or malformed file as "no token".
15
+ *
16
+ * Ported from supercli `server/src/lib/token.ts`; the load-bearing detail is the
17
+ * `expires_in` (seconds, from the device-flow response) → `expires_at` (absolute
18
+ * ISO timestamp, on disk) conversion, which lets {@link isServerTokenExpired}
19
+ * decide freshness without re-deriving the clock offset.
20
+ */
21
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
22
+ import os from "node:os";
23
+ import path from "node:path";
24
+
25
+ /** Root of the indus-server. Used to build the gateway base URL and the device-login endpoints. Any trailing slash(es) are stripped so callers can append paths with a single separator. */
26
+ export const INDUS_SERVER_URL: string = process.env.INDUS_SERVER_URL?.replace(/\/+$/, "") ?? "https://indus-server.vercel.app";
27
+ /** The app profile directory that holds the server session token. */
28
+ export const CONFIG_DIR: string = path.join(os.homedir(), ".indusagi", "agent");
29
+ /** Absolute path of the JSON file the session token is persisted to. */
30
+ export const TOKEN_FILE: string = path.join(CONFIG_DIR, "server-token.json");
31
+
32
+ /** On-disk token shape (what {@link storeServerToken} writes / {@link getServerToken} returns). */
33
+ export interface TokenData {
34
+ /** The better-auth session token, used as the gateway api key in server mode. */
35
+ access_token: string;
36
+ /** Optional refresh token, when the device flow returns one. */
37
+ refresh_token?: string;
38
+ /** Token scheme; defaults to `"Bearer"`. */
39
+ token_type: string;
40
+ /** Optional granted scope string. */
41
+ scope?: string;
42
+ /** Absolute expiry as an ISO string, or null when the token has no expiry. */
43
+ expires_at: string | null;
44
+ /** When this record was written, as an ISO string. */
45
+ created_at: string;
46
+ }
47
+
48
+ /** Device-flow response shape accepted by {@link storeServerToken} (`expires_in` → `expires_at`). */
49
+ export interface ServerTokenInput {
50
+ /** The better-auth session token. */
51
+ access_token: string;
52
+ /** Optional refresh token. */
53
+ refresh_token?: string;
54
+ /** Token scheme; defaults to `"Bearer"` when omitted. */
55
+ token_type?: string;
56
+ /** Optional granted scope string. */
57
+ scope?: string;
58
+ /** Lifetime in seconds, relative to now; converted to an absolute `expires_at`. */
59
+ expires_in?: number;
60
+ }
61
+
62
+ /**
63
+ * Read the stored session token, tolerating a missing or malformed file as
64
+ * "no token".
65
+ *
66
+ * @returns the parsed {@link TokenData}, or `null` when nothing is stored.
67
+ */
68
+ export async function getServerToken(): Promise<TokenData | null> {
69
+ try {
70
+ const data = await readFile(TOKEN_FILE, "utf8");
71
+ return JSON.parse(data);
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Persist a session token from a device-flow response, converting the relative
79
+ * `expires_in` (seconds) into an absolute `expires_at` ISO timestamp and
80
+ * defaulting `token_type` to `"Bearer"`. Creates the profile directory if
81
+ * needed and writes the file with owner-only permissions.
82
+ *
83
+ * @param token the device-flow token payload.
84
+ * @returns `true` on success, `false` if the write failed.
85
+ */
86
+ export async function storeServerToken(token: ServerTokenInput): Promise<boolean> {
87
+ try {
88
+ await mkdir(CONFIG_DIR, { recursive: true });
89
+ const tokenData = {
90
+ access_token: token.access_token,
91
+ refresh_token: token.refresh_token,
92
+ token_type: token.token_type ?? "Bearer",
93
+ scope: token.scope,
94
+ expires_at: token.expires_in !== undefined ? new Date(Date.now() + token.expires_in * 1000).toISOString() : null,
95
+ created_at: new Date().toISOString(),
96
+ };
97
+ await writeFile(TOKEN_FILE, `${JSON.stringify(tokenData, null, 2)}\n`, {
98
+ encoding: "utf8",
99
+ mode: 384,
100
+ });
101
+ return true;
102
+ } catch (error) {
103
+ console.error("Failed to store server token:", (error as Error).message);
104
+ return false;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Remove the stored session token (used by `indus logout`). A missing file is
110
+ * treated as nothing-to-do.
111
+ *
112
+ * @returns `true` when a file was deleted, `false` when none existed / removal failed.
113
+ */
114
+ export async function clearServerToken(): Promise<boolean> {
115
+ try {
116
+ await unlink(TOKEN_FILE);
117
+ return true;
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Decide whether a token is expired. A token counts as expired when it is
125
+ * absent, has no `expires_at`, or has under 5 minutes of life remaining (the
126
+ * margin keeps a long turn from racing the expiry boundary).
127
+ *
128
+ * @param token the token to inspect (or `null`).
129
+ * @returns `true` when the token is missing or stale.
130
+ */
131
+ export function isServerTokenExpired(token: TokenData | null): boolean {
132
+ if (!token || !token.expires_at) {
133
+ return true;
134
+ }
135
+ const expiresAt = new Date(token.expires_at);
136
+ const now = new Date();
137
+ return expiresAt.getTime() - now.getTime() < 5 * 60 * 1000;
138
+ }
139
+
140
+ /**
141
+ * Convenience predicate: there is a stored token and it is not expired.
142
+ *
143
+ * @returns `true` when a usable (fresh) session token is on disk.
144
+ */
145
+ export async function hasValidServerToken(): Promise<boolean> {
146
+ const token = await getServerToken();
147
+ return token !== null && !isServerTokenExpired(token);
148
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Boot stage pipeline — the ordered list of {@link Stage} transforms that turn a
3
+ * bare {@link BootContext} (argv + workspace + brand) into a fully-resolved one
4
+ * (parsed invocation, materialised directories, applied upgrades, resolved
5
+ * startup resources, selected runner).
6
+ *
7
+ * The pipeline is data, not control flow: {@link STAGES} lists each step in order,
8
+ * and {@link runStages} folds an *immutable* context through them — every stage
9
+ * receives a context and returns its successor (built by spreading, never by
10
+ * mutating). A stage may be async; the fold awaits each in turn so ordering and
11
+ * side-effect sequencing are deterministic.
12
+ *
13
+ * Stage order and intent:
14
+ * 1. `locate-workspace` — materialise the resolved {@link Workspace} on disk
15
+ * (`createWorkspace` + `ensureDirs`). Pure path computation already happened
16
+ * when the initial context was built; this stage only `mkdir`s.
17
+ * 2. `apply-upgrades` — fold the idempotent {@link applyUpgrades} registry
18
+ * over the workspace (a no-op on an already-current profile).
19
+ * 3. `build-invocation` — parse `argv` into the typed {@link Invocation}.
20
+ * 4. `resolve-resources` — best-effort construct the framework
21
+ * settings/auth/model graph; degrade to a minimal object if the framework
22
+ * pieces are unavailable.
23
+ * 5. `select-runner` — no-op transform that exists for symmetry and tracing
24
+ * (the actual dispatch happens in {@link "./boot"} after the pipeline, so the
25
+ * selected runner can own the exit code). Kept in the list so the ordered
26
+ * pipeline reads as the full launch sequence.
27
+ */
28
+ import { tokenizeInvocation } from "./invocation.js";
29
+ import { applyUpgrades } from "./upgrade/apply.js";
30
+ import { ensureDirs } from "../workspace/locator.js";
31
+ import type { BootContext, Stage } from "./contract.js";
32
+
33
+ /**
34
+ * Materialise the resolved workspace directories on disk.
35
+ *
36
+ * The {@link BootContext.workspace} was already computed (pure) when the initial
37
+ * context was assembled; this stage only ensures the directory subset exists so
38
+ * later stages and runners can write into it. Returns the same context (the
39
+ * workspace record is unchanged — only the filesystem side-effects happen here).
40
+ */
41
+ const locateWorkspace: Stage = {
42
+ name: "locate-workspace",
43
+ apply(ctx) {
44
+ ensureDirs(ctx.workspace);
45
+ return ctx;
46
+ },
47
+ };
48
+
49
+ /**
50
+ * Run any pending one-time profile upgrades, idempotently.
51
+ *
52
+ * Folds the upgrade registry over the workspace. The driver is non-fatal: a step
53
+ * that fails is reported and retried on a later launch, never aborting boot. The
54
+ * context is returned unchanged (upgrades touch the filesystem, not the context).
55
+ */
56
+ const upgrade: Stage = {
57
+ name: "apply-upgrades",
58
+ async apply(ctx) {
59
+ await applyUpgrades(ctx.workspace);
60
+ return ctx;
61
+ },
62
+ };
63
+
64
+ /**
65
+ * Parse `argv` into the typed {@link Invocation} and thread it onto the context.
66
+ *
67
+ * The initial context carries a placeholder invocation (the bootstrapper cannot
68
+ * know the mode before parsing); this stage replaces it with the real parse.
69
+ */
70
+ const buildInvocation: Stage = {
71
+ name: "build-invocation",
72
+ apply(ctx) {
73
+ const invocation = tokenizeInvocation(ctx.argv);
74
+ return { ...ctx, invocation };
75
+ },
76
+ };
77
+
78
+ /**
79
+ * Best-effort assembly of the startup resource graph.
80
+ *
81
+ * Where the rebuilt framework owns a concept, the field is constructed from it:
82
+ * {@link StartupResources.settings} from the framework's `DEFAULT_SETTINGS`, and
83
+ * {@link StartupResources.models} from the framework's `ModelRegistry`. The
84
+ * credential graph has no framework type yet, so it is an empty placeholder until
85
+ * Phase 2. If a framework piece cannot be loaded, the stage degrades to a minimal
86
+ * resources object built from empty literals rather than failing the boot — the
87
+ * shape is what later phases depend on, and they fill it in.
88
+ */
89
+ const resolveResources: Stage = {
90
+ name: "resolve-resources",
91
+ async apply(ctx) {
92
+ const resources = await buildStartupResources();
93
+ return { ...ctx, resources };
94
+ },
95
+ };
96
+
97
+ async function buildStartupResources(): Promise<{ settings: unknown; auth: Record<string, unknown>; models: unknown }> {
98
+ const settings = await loadDefaultSettings();
99
+ const models = await loadModelRegistry();
100
+ return { settings, auth: {}, models };
101
+ }
102
+
103
+ async function loadDefaultSettings(): Promise<unknown> {
104
+ try {
105
+ const mod = await import("indusagi/shell-app");
106
+ return (mod as { DEFAULT_SETTINGS?: unknown }).DEFAULT_SETTINGS ?? {};
107
+ } catch {
108
+ return {};
109
+ }
110
+ }
111
+
112
+ async function loadModelRegistry(): Promise<unknown> {
113
+ const mod = (await import("indusagi/ai")) as { ModelRegistry?: unknown; modelRegistry?: unknown };
114
+ if (typeof mod.ModelRegistry === "function") {
115
+ return new (mod.ModelRegistry as new () => unknown)();
116
+ }
117
+ return mod.modelRegistry;
118
+ }
119
+
120
+ /**
121
+ * Marker / tracing stage for runner selection.
122
+ *
123
+ * The real dispatch is performed by {@link "./boot"} after the pipeline so the
124
+ * chosen runner can own the process exit code; this stage exists to keep the
125
+ * ordered pipeline a faithful description of the launch sequence and to give the
126
+ * selection step a name for tracing. It returns the context unchanged.
127
+ */
128
+ const selectRunnerStage: Stage = {
129
+ name: "select-runner",
130
+ apply(ctx) {
131
+ return ctx;
132
+ },
133
+ };
134
+
135
+ /**
136
+ * The launch pipeline, in execution order. Folded by {@link runStages}.
137
+ */
138
+ export const STAGES: readonly Stage[] = [
139
+ locateWorkspace,
140
+ upgrade,
141
+ buildInvocation,
142
+ resolveResources,
143
+ selectRunnerStage,
144
+ ];
145
+
146
+ /**
147
+ * Fold an ordered list of {@link Stage} transforms over an immutable
148
+ * {@link BootContext}.
149
+ *
150
+ * Each stage receives the current context and returns its successor; the result
151
+ * of one stage is the input to the next. Awaits every stage so async ordering is
152
+ * deterministic. The input `initial` is never mutated — stages produce new
153
+ * contexts by spreading.
154
+ *
155
+ * @param initial The seed context (argv + workspace + brand + placeholder fields).
156
+ * @param stages The ordered transforms to apply; defaults to {@link STAGES}.
157
+ * @returns The fully-resolved context after every stage has run.
158
+ */
159
+ export async function runStages(initial: BootContext, stages: readonly Stage[] = STAGES): Promise<BootContext> {
160
+ let ctx: BootContext = initial;
161
+ for (const stage of stages) {
162
+ ctx = await stage.apply(ctx);
163
+ }
164
+ return ctx;
165
+ }
166
+
167
+ export { locateWorkspace, upgrade, buildInvocation, resolveResources, selectRunnerStage };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Upgrade driver — folds the ordered {@link UPGRADES} registry over a
3
+ * {@link Workspace}, exactly once per step.
4
+ *
5
+ * The driver is the only place that knows about the *marker file*: a small JSON
6
+ * record under the profile directory listing the ids of upgrades that have
7
+ * already run. Each registry step is skipped if its id is present in the marker;
8
+ * otherwise it is applied and, on success, its id is appended and the marker is
9
+ * persisted. A step that throws is *not* recorded (so it is retried next launch)
10
+ * and is reported as a warning rather than aborting the remaining steps — one
11
+ * bad migration must never wedge startup.
12
+ *
13
+ * The result is purely informational: {@link UpgradeReport.applied} lists ids run
14
+ * *this* invocation (empty on an already-current profile), and
15
+ * {@link UpgradeReport.warnings} carries human-readable notes for any step that
16
+ * failed. Callers typically log both and continue.
17
+ */
18
+ import { promises as fs } from "node:fs";
19
+ import { join } from "node:path";
20
+ import type { Workspace } from "../contract.js";
21
+ import { UPGRADES } from "./upgrades.js";
22
+
23
+ const MARKER_FILENAME = ".upgrade-state.json";
24
+
25
+ function markerPath(ws: Workspace): string {
26
+ return join(ws.profileDir, MARKER_FILENAME);
27
+ }
28
+
29
+ async function readAppliedIds(ws: Workspace): Promise<Set<string>> {
30
+ try {
31
+ const raw = await fs.readFile(markerPath(ws), "utf8");
32
+ const parsed = JSON.parse(raw) as { appliedIds?: unknown };
33
+ const ids = Array.isArray(parsed.appliedIds) ? parsed.appliedIds : [];
34
+ return new Set(ids.filter((id): id is string => typeof id === "string"));
35
+ } catch {
36
+ return new Set();
37
+ }
38
+ }
39
+
40
+ async function writeAppliedIds(ws: Workspace, appliedIds: Set<string>): Promise<void> {
41
+ const state = { appliedIds: [...appliedIds] };
42
+ await fs.mkdir(ws.profileDir, { recursive: true });
43
+ await fs.writeFile(markerPath(ws), `${JSON.stringify(state, null, 2)}\n`);
44
+ }
45
+
46
+ function describeError(error: unknown): string {
47
+ if (error instanceof Error) return error.message;
48
+ return String(error);
49
+ }
50
+
51
+ /**
52
+ * The outcome of an {@link applyUpgrades} pass.
53
+ *
54
+ * - `applied` — ids of upgrades that ran successfully *this* invocation, in
55
+ * apply order. Empty when the profile was already current.
56
+ * - `warnings` — one entry per step that threw, naming the step and its error.
57
+ * Non-fatal: applying continues past a failed step.
58
+ */
59
+ export interface UpgradeReport {
60
+ /** Ids successfully applied during this call, in order. */
61
+ readonly applied: string[];
62
+ /** Human-readable notes for steps that failed (non-fatal). */
63
+ readonly warnings: string[];
64
+ }
65
+
66
+ /**
67
+ * Apply every not-yet-applied upgrade in {@link UPGRADES}, in registry order,
68
+ * recording each success in the marker file so it never runs again.
69
+ *
70
+ * Steps already named in the marker are skipped. A step that throws is recorded
71
+ * as a warning, left unmarked (so it is retried on a later launch), and does not
72
+ * block the remaining steps. The marker is rewritten after each success, so a
73
+ * crash mid-pass still preserves the progress made so far.
74
+ *
75
+ * @param ws The resolved, absolute on-disk layout to upgrade.
76
+ * @returns The ids applied this pass and any non-fatal warnings.
77
+ */
78
+ export async function applyUpgrades(ws: Workspace): Promise<UpgradeReport> {
79
+ const alreadyApplied = await readAppliedIds(ws);
80
+ const applied: string[] = [];
81
+ const warnings: string[] = [];
82
+ for (const upgrade of UPGRADES) {
83
+ if (alreadyApplied.has(upgrade.id)) continue;
84
+ try {
85
+ await upgrade.apply(ws);
86
+ alreadyApplied.add(upgrade.id);
87
+ applied.push(upgrade.id);
88
+ await writeAppliedIds(ws, alreadyApplied);
89
+ } catch (error) {
90
+ warnings.push(`upgrade "${upgrade.id}" failed: ${describeError(error)}`);
91
+ }
92
+ }
93
+ return { applied, warnings };
94
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Upgrade subsystem — public barrel.
3
+ *
4
+ * Surfaces the ordered, idempotent profile-upgrade registry and its driver.
5
+ * Boot consumers import {@link applyUpgrades} to run pending one-time migrations
6
+ * and the {@link UPGRADES} registry / {@link Upgrade} type for inspection and
7
+ * testing. The marker-file bookkeeping is an internal detail of the driver and
8
+ * is not re-exported.
9
+ */
10
+ export type { Upgrade } from "./upgrades.js";
11
+ export { UPGRADES, foldCredentials, reshelveTranscripts, relocateBinaries, renamePromptDir, projectTranscriptDirName } from "./upgrades.js";
12
+ export type { UpgradeReport } from "./apply.js";
13
+ export { applyUpgrades } from "./apply.js";