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,81 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { translateAgentEvent } from "./translate.js";
3
+
4
+ type InputEvent = Parameters<typeof translateAgentEvent>[0];
5
+
6
+ describe("translateAgentEvent tool signals", () => {
7
+ it("includes tool arguments in tool_start", () => {
8
+ const event: InputEvent = {
9
+ type: "tool_execution_start",
10
+ toolCallId: "call-1",
11
+ toolName: "read",
12
+ args: { path: "README.md" },
13
+ };
14
+
15
+ expect(translateAgentEvent(event)).toEqual([{
16
+ kind: "tool_start",
17
+ id: "call-1",
18
+ name: "read",
19
+ args: { path: "README.md" },
20
+ }]);
21
+ });
22
+
23
+ it("includes tool results and error status in tool_end", () => {
24
+ const event: InputEvent = {
25
+ type: "tool_execution_end",
26
+ toolCallId: "call-2",
27
+ toolName: "write",
28
+ result: { content: "permission denied" },
29
+ isError: true,
30
+ };
31
+
32
+ expect(translateAgentEvent(event)).toEqual([{
33
+ kind: "tool_end",
34
+ id: "call-2",
35
+ ok: false,
36
+ result: { content: "permission denied" },
37
+ isError: true,
38
+ }]);
39
+ });
40
+ });
41
+
42
+ describe("translateAgentEvent preserved signals", () => {
43
+ it("preserves prompt extraction", () => {
44
+ const event = {
45
+ type: "message_end",
46
+ message: {
47
+ role: "user",
48
+ content: [{ type: "text", text: "hello " }, { type: "text", text: "world" }],
49
+ timestamp: 1,
50
+ },
51
+ } as unknown as InputEvent;
52
+
53
+ expect(translateAgentEvent(event)).toEqual([{ kind: "prompt", text: "hello world" }]);
54
+ });
55
+
56
+ it("preserves text and thinking deltas", () => {
57
+ const message = { role: "assistant", content: [], timestamp: 1 };
58
+
59
+ expect(translateAgentEvent({
60
+ type: "message_update",
61
+ message,
62
+ assistantMessageEvent: { type: "text_delta", delta: "answer" },
63
+ } as unknown as InputEvent)).toEqual([{ kind: "text", delta: "answer" }]);
64
+ expect(translateAgentEvent({
65
+ type: "message_update",
66
+ message,
67
+ assistantMessageEvent: { type: "thinking_delta", delta: "reason" },
68
+ } as unknown as InputEvent)).toEqual([{ kind: "thinking", delta: "reason" }]);
69
+ });
70
+
71
+ it("preserves turn_end usage", () => {
72
+ const usage = { input: 1, output: 2, cacheRead: 3, cacheWrite: 4, totalTokens: 10, cost: { total: 0.5 } };
73
+ const event = {
74
+ type: "turn_end",
75
+ message: { role: "assistant", content: [], usage, timestamp: 1 },
76
+ toolResults: [],
77
+ } as unknown as InputEvent;
78
+
79
+ expect(translateAgentEvent(event)).toEqual([{ kind: "turn_end", usage }]);
80
+ });
81
+ });
@@ -0,0 +1,74 @@
1
+ import type { AgentEvent, AgentEventType, ExtractAgentEvent } from "indusagi/agent";
2
+ import { createZeroUsage } from "indusagi/ai";
3
+ import { conductorFault } from "../contract.js";
4
+ import type { SessionSignal } from "../contract.js";
5
+
6
+ type TranslatorRule<TType extends AgentEventType> = (event: ExtractAgentEvent<TType>) => SessionSignal[];
7
+ type TranslatorTable = { [K in AgentEventType]: TranslatorRule<K> };
8
+
9
+ const none = (): SessionSignal[] => [];
10
+
11
+ function usageOf(message: unknown) {
12
+ if (typeof message === "object" && message !== null && "usage" in message && message.usage != null) return message.usage;
13
+ return createZeroUsage();
14
+ }
15
+
16
+ function faultMessageOf(error: unknown): string {
17
+ if (typeof error === "object" && error !== null && "errorMessage" in error && typeof error.errorMessage === "string") {
18
+ return error.errorMessage;
19
+ }
20
+ return "model stream error";
21
+ }
22
+
23
+ function translateStreamEvent(sub: any): SessionSignal[] {
24
+ switch (sub.type) {
25
+ case "text_delta": return [{ kind: "text", delta: sub.delta }];
26
+ case "thinking_delta": return [{ kind: "thinking", delta: sub.delta }];
27
+ case "error": return [{ kind: "fault", fault: conductorFault("model", faultMessageOf(sub.error), sub.error) }];
28
+ default: return [];
29
+ }
30
+ }
31
+
32
+ function promptTextOf(content: unknown): string {
33
+ if (typeof content === "string") return content;
34
+ if (Array.isArray(content)) {
35
+ return content.map((block) => typeof block === "object" && block !== null && "text" in block && typeof block.text === "string" ? block.text : "").join("");
36
+ }
37
+ return "";
38
+ }
39
+
40
+ export const TRANSLATOR_TABLE: TranslatorTable = {
41
+ agent_start: none,
42
+ agent_end: none,
43
+ turn_start: none,
44
+ message_start: none,
45
+ message_end: (event) => {
46
+ const message = event.message;
47
+ if (message?.role === "user") return [{ kind: "prompt", text: promptTextOf(message.content) }];
48
+ if (message?.role === "assistant" && (message.stopReason === "error" || typeof message.errorMessage === "string")) {
49
+ return [{ kind: "fault", fault: conductorFault("model", message.errorMessage ?? "the model call failed") }];
50
+ }
51
+ return [];
52
+ },
53
+ turn_end: (event) => [{ kind: "turn_end", usage: usageOf(event.message) }],
54
+ message_update: (event) => translateStreamEvent(event.assistantMessageEvent),
55
+ tool_execution_start: (event) => [{ kind: "tool_start", id: event.toolCallId, name: event.toolName, args: event.args } as SessionSignal],
56
+ tool_execution_update: (event) => [{
57
+ kind: "tool_update",
58
+ id: event.toolCallId,
59
+ name: event.toolName,
60
+ details: event.partialResult?.details,
61
+ }],
62
+ tool_execution_end: (event) => [{
63
+ kind: "tool_end",
64
+ id: event.toolCallId,
65
+ ok: !event.isError,
66
+ result: event.result,
67
+ isError: event.isError,
68
+ } as SessionSignal],
69
+ };
70
+
71
+ export function translateAgentEvent(event: AgentEvent): SessionSignal[] {
72
+ const rule = TRANSLATOR_TABLE[event.type] as (event: AgentEvent) => SessionSignal[];
73
+ return rule(event);
74
+ }
@@ -0,0 +1,2 @@
1
+ export { parseSkillInvocation } from "./parse.js";
2
+ export type { SkillInvocation } from "./parse.js";
@@ -0,0 +1,108 @@
1
+ export interface SkillInvocation {
2
+ readonly name: string;
3
+ readonly args: Record<string, string>;
4
+ readonly body: string;
5
+ }
6
+
7
+ const SKILL_TAG = "skill";
8
+ const NAME_ATTR = "name";
9
+
10
+ function isSpace(ch: string | undefined): boolean {
11
+ return ch === " " || ch === "\t" || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
12
+ }
13
+ function isKeyStart(ch: string | undefined): boolean {
14
+ return ch !== undefined && ((ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z") || ch === "_");
15
+ }
16
+ function isKeyChar(ch: string | undefined): boolean {
17
+ return isKeyStart(ch) || (ch !== undefined && ch >= "0" && ch <= "9") || ch === "-" || ch === ":" || ch === ".";
18
+ }
19
+ function skipSpace(text: string, index: number): number {
20
+ while (index < text.length && isSpace(text[index])) index++;
21
+ return index;
22
+ }
23
+ function decodeEntities(raw: string): string {
24
+ if (!raw.includes("&")) return raw;
25
+ return raw.replace(/&(#39|#x27|#34|#x22|amp|lt|gt|quot|apos);/gi, (match, code: string) => {
26
+ switch (code.toLowerCase()) {
27
+ case "amp": return "&";
28
+ case "lt": return "<";
29
+ case "gt": return ">";
30
+ case "quot": case "#34": case "#x22": return '"';
31
+ case "apos": case "#39": case "#x27": return "'";
32
+ default: return match;
33
+ }
34
+ });
35
+ }
36
+ function scanAttribute(text: string, index: number): { key: string; value: string; next: number } | null {
37
+ let cursor = index;
38
+ if (!isKeyStart(text[cursor])) return null;
39
+ cursor++;
40
+ while (cursor < text.length && isKeyChar(text[cursor])) cursor++;
41
+ const key = text.slice(index, cursor).toLowerCase();
42
+ const afterKey = skipSpace(text, cursor);
43
+ if (text[afterKey] !== "=") return { key, value: "", next: cursor };
44
+ cursor = skipSpace(text, afterKey + 1);
45
+ const quote = text[cursor];
46
+ if (quote === '"' || quote === "'") {
47
+ const start = cursor + 1;
48
+ let end = start;
49
+ while (end < text.length && text[end] !== quote) end++;
50
+ if (end >= text.length) return { key, value: decodeEntities(text.slice(start)), next: text.length };
51
+ return { key, value: decodeEntities(text.slice(start, end)), next: end + 1 };
52
+ }
53
+ const start = cursor;
54
+ while (cursor < text.length && !isSpace(text[cursor]) && text[cursor] !== ">" && text[cursor] !== "/") cursor++;
55
+ return { key, value: decodeEntities(text.slice(start, cursor)), next: cursor };
56
+ }
57
+ function trimBodyEdges(body: string): string {
58
+ let result = body;
59
+ if (result.startsWith("\r\n")) result = result.slice(2);
60
+ else if (result.startsWith("\n") || result.startsWith("\r")) result = result.slice(1);
61
+ if (result.endsWith("\r\n")) result = result.slice(0, -2);
62
+ else if (result.endsWith("\n") || result.endsWith("\r")) result = result.slice(0, -1);
63
+ return result;
64
+ }
65
+ function findCloseTag(text: string, index: number): number | null {
66
+ let cursor = index;
67
+ while (cursor < text.length) {
68
+ const left = text.indexOf("<", cursor);
69
+ if (left === -1) return null;
70
+ let scan = left + 1;
71
+ if (text[scan] !== "/") { cursor = left + 1; continue; }
72
+ scan = skipSpace(text, scan + 1);
73
+ const start = scan;
74
+ while (scan < text.length && isKeyChar(text[scan])) scan++;
75
+ if (text.slice(start, scan).toLowerCase() !== SKILL_TAG) { cursor = left + 1; continue; }
76
+ scan = skipSpace(text, scan);
77
+ if (text[scan] === ">") return left;
78
+ cursor = left + 1;
79
+ }
80
+ return null;
81
+ }
82
+
83
+ export function parseSkillInvocation(text: string): SkillInvocation | null {
84
+ let index = skipSpace(text, 0);
85
+ if (text[index] !== "<") return null;
86
+ index++;
87
+ const tagStart = index;
88
+ while (index < text.length && isKeyChar(text[index])) index++;
89
+ if (text.slice(tagStart, index).toLowerCase() !== SKILL_TAG) return null;
90
+ const afterTag = text[index];
91
+ if (afterTag !== undefined && !isSpace(afterTag) && afterTag !== ">" && afterTag !== "/") return null;
92
+ const args: Record<string, string> = {};
93
+ for (;;) {
94
+ index = skipSpace(text, index);
95
+ const ch = text[index];
96
+ if (ch === undefined || ch === "/") return null;
97
+ if (ch === ">") { index++; break; }
98
+ const scan = scanAttribute(text, index);
99
+ if (!scan) return null;
100
+ if (!(scan.key in args)) args[scan.key] = scan.value;
101
+ index = scan.next;
102
+ }
103
+ const name = args[NAME_ATTR];
104
+ if (!name) return null;
105
+ const close = findCloseTag(text, index);
106
+ if (close === null) return null;
107
+ return { name, args, body: trimBodyEdges(decodeEntities(text.slice(index, close))) };
108
+ }
@@ -0,0 +1,22 @@
1
+ export {
2
+ TranscriptStore,
3
+ memoryBackend,
4
+ fsBackend,
5
+ replay,
6
+ type TranscriptBackend,
7
+ type TranscriptState,
8
+ type TranscriptClock,
9
+ type TranscriptStoreOptions,
10
+ } from "./store.js";
11
+ export {
12
+ roleForMessage,
13
+ encodeEntry,
14
+ encodeHead,
15
+ parseSessionText,
16
+ branchToMessages,
17
+ branchToLlmMessages,
18
+ importLegacyFile,
19
+ importLegacyText,
20
+ resolveLegacyMessages,
21
+ type ParsedSessionFile,
22
+ } from "./serialize.js";
@@ -0,0 +1,116 @@
1
+ import { buildSessionContext, convertToLlm, loadEntriesFromFile, parseSessionEntries } from "indusagi/agent";
2
+ import type { AgentMessage } from "indusagi/agent";
3
+ import type { Message } from "indusagi/ai";
4
+ import { TRANSCRIPT_SCHEMA } from "../contract.js";
5
+ import type { SessionHead, TranscriptEntry, TranscriptRole } from "../contract.js";
6
+
7
+ export interface ParsedSessionFile {
8
+ readonly head: SessionHead | null;
9
+ readonly entries: TranscriptEntry[];
10
+ }
11
+
12
+ export function roleForMessage(message: AgentMessage): TranscriptRole {
13
+ switch (message.role) {
14
+ case "user": return "user";
15
+ case "assistant": return "assistant";
16
+ case "toolResult": return "tool";
17
+ case "compactionSummary":
18
+ case "branchSummary": return "condense";
19
+ default: return "note";
20
+ }
21
+ }
22
+
23
+ export function encodeEntry(entry: TranscriptEntry): string {
24
+ return JSON.stringify({
25
+ schema: TRANSCRIPT_SCHEMA,
26
+ kind: "entry",
27
+ id: entry.id,
28
+ prev: entry.parent,
29
+ role: entry.role,
30
+ message: entry.content,
31
+ at: entry.createdAt,
32
+ ...(entry.meta !== undefined ? { meta: entry.meta } : {}),
33
+ });
34
+ }
35
+
36
+ export function encodeHead(head: SessionHead): string {
37
+ return JSON.stringify({
38
+ schema: TRANSCRIPT_SCHEMA,
39
+ kind: "head",
40
+ sessionId: head.sessionId,
41
+ leaf: head.leaf,
42
+ ...(head.usage !== undefined ? { usage: head.usage } : {}),
43
+ });
44
+ }
45
+
46
+ function isFileLine(value: unknown): value is any {
47
+ return typeof value === "object" && value !== null && (value as any).schema === TRANSCRIPT_SCHEMA
48
+ && ((value as any).kind === "head" || (value as any).kind === "entry");
49
+ }
50
+
51
+ export function parseSessionText(sessionId: string, text: string): ParsedSessionFile {
52
+ const entries: TranscriptEntry[] = [];
53
+ let leaf: string | null | undefined;
54
+ let usage: SessionHead["usage"];
55
+ for (const raw of text.split("\n")) {
56
+ const trimmed = raw.trim();
57
+ if (!trimmed) continue;
58
+ let parsed: unknown;
59
+ try { parsed = JSON.parse(trimmed); } catch { continue; }
60
+ if (!isFileLine(parsed)) continue;
61
+ if (parsed.kind === "head") {
62
+ leaf = parsed.leaf;
63
+ usage = parsed.usage;
64
+ } else {
65
+ entries.push({
66
+ id: parsed.id,
67
+ parent: parsed.prev,
68
+ role: parsed.role,
69
+ content: parsed.message,
70
+ createdAt: parsed.at,
71
+ ...(parsed.meta !== undefined ? { meta: parsed.meta } : {}),
72
+ });
73
+ }
74
+ }
75
+ return {
76
+ head: leaf === undefined ? null : { sessionId, leaf, ...(usage !== undefined ? { usage } : {}) },
77
+ entries,
78
+ };
79
+ }
80
+
81
+ export function branchToMessages(branch: readonly TranscriptEntry[]): AgentMessage[] {
82
+ return branch.map((entry) => entry.content);
83
+ }
84
+
85
+ export function branchToLlmMessages(branch: readonly TranscriptEntry[]): Message[] {
86
+ return convertToLlm(branchToMessages(branch)) as Message[];
87
+ }
88
+
89
+ function legacyEntriesToTranscript(entries: any[]): TranscriptEntry[] {
90
+ const messages = buildSessionContext(entries).messages as AgentMessage[];
91
+ let parent: string | null = null;
92
+ return messages.map((content, index) => {
93
+ const id = `legacy_${index.toString(36)}`;
94
+ const entry: TranscriptEntry = {
95
+ id,
96
+ parent,
97
+ role: roleForMessage(content),
98
+ content,
99
+ createdAt: new Date(0).toISOString(),
100
+ };
101
+ parent = id;
102
+ return entry;
103
+ });
104
+ }
105
+
106
+ export function importLegacyFile(filePath: string): TranscriptEntry[] {
107
+ return legacyEntriesToTranscript(loadEntriesFromFile(filePath) as any[]);
108
+ }
109
+
110
+ export function importLegacyText(text: string): TranscriptEntry[] {
111
+ return legacyEntriesToTranscript(parseSessionEntries(text) as any[]);
112
+ }
113
+
114
+ export function resolveLegacyMessages(filePath: string): AgentMessage[] {
115
+ return buildSessionContext(loadEntriesFromFile(filePath) as any[]).messages as AgentMessage[];
116
+ }
@@ -0,0 +1,205 @@
1
+ import { ulid } from "ulid";
2
+ import type { AgentMessage } from "indusagi/agent";
3
+ import type { Usage } from "indusagi/ai";
4
+ import { TRANSCRIPT_SCHEMA } from "../contract.js";
5
+ import type { SessionHead, TranscriptEntry, TranscriptRole } from "../contract.js";
6
+ import { encodeEntry, encodeHead, parseSessionText, roleForMessage } from "./serialize.js";
7
+
8
+ export interface TranscriptBackend {
9
+ locate(sessionId: string): string;
10
+ read(sessionId: string): Promise<string | null>;
11
+ append(sessionId: string, line: string): Promise<void>;
12
+ rewrite(sessionId: string, text: string): Promise<void>;
13
+ }
14
+
15
+ export interface TranscriptState {
16
+ readonly nodes: ReadonlyMap<string, TranscriptEntry>;
17
+ readonly head: SessionHead;
18
+ }
19
+
20
+ export interface TranscriptClock {
21
+ readonly id: () => string;
22
+ readonly now: () => string;
23
+ }
24
+
25
+ export interface TranscriptStoreOptions {
26
+ readonly backend?: TranscriptBackend;
27
+ readonly clock?: TranscriptClock;
28
+ }
29
+
30
+ export function memoryBackend(): TranscriptBackend {
31
+ const files = new Map<string, string[]>();
32
+ return {
33
+ locate: (sessionId) => `mem:${sessionId}`,
34
+ read: async (sessionId) => {
35
+ const lines = files.get(sessionId);
36
+ return lines ? lines.join("\n") : null;
37
+ },
38
+ append: async (sessionId, line) => {
39
+ const lines = files.get(sessionId) ?? [];
40
+ lines.push(line);
41
+ files.set(sessionId, lines);
42
+ },
43
+ rewrite: async (sessionId, text) => {
44
+ files.set(sessionId, text.length === 0 ? [] : text.split("\n"));
45
+ },
46
+ };
47
+ }
48
+
49
+ export function fsBackend(dir: string): TranscriptBackend {
50
+ const fileOf = async (sessionId: string) => {
51
+ const path = await import("node:path");
52
+ return path.join(dir, `${sessionId}.ndjson`);
53
+ };
54
+ return {
55
+ locate: (sessionId) => `${dir}/${sessionId}.ndjson`,
56
+ read: async (sessionId) => {
57
+ const fs = await import("node:fs/promises");
58
+ try { return await fs.readFile(await fileOf(sessionId), "utf8"); } catch { return null; }
59
+ },
60
+ append: async (sessionId, line) => {
61
+ const fs = await import("node:fs/promises");
62
+ await fs.mkdir(dir, { recursive: true });
63
+ await fs.appendFile(await fileOf(sessionId), `${line}\n`, "utf8");
64
+ },
65
+ rewrite: async (sessionId, text) => {
66
+ const fs = await import("node:fs/promises");
67
+ await fs.mkdir(dir, { recursive: true });
68
+ await fs.writeFile(await fileOf(sessionId), text.length === 0 ? "" : `${text}\n`, "utf8");
69
+ },
70
+ };
71
+ }
72
+
73
+ export function replay(sessionId: string, entries: readonly TranscriptEntry[], leaf: string | null): TranscriptState {
74
+ const nodes = new Map<string, TranscriptEntry>();
75
+ for (const entry of entries) nodes.set(entry.id, entry);
76
+ return { nodes, head: { sessionId, leaf } };
77
+ }
78
+
79
+ function deepestLeaf(entries: readonly TranscriptEntry[]): string | null {
80
+ if (entries.length === 0) return null;
81
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
82
+ const depthOf = (id: string) => {
83
+ let depth = 0;
84
+ let cursor: string | null = id;
85
+ const seen = new Set<string>();
86
+ while (cursor !== null && !seen.has(cursor)) {
87
+ seen.add(cursor);
88
+ const node = byId.get(cursor);
89
+ if (!node) break;
90
+ depth++;
91
+ cursor = node.parent;
92
+ }
93
+ return depth;
94
+ };
95
+ let best = entries[0];
96
+ let bestDepth = depthOf(best.id);
97
+ for (const entry of entries) {
98
+ const depth = depthOf(entry.id);
99
+ if (depth > bestDepth || (depth === bestDepth && entry.id > best.id)) {
100
+ best = entry;
101
+ bestDepth = depth;
102
+ }
103
+ }
104
+ return best.id;
105
+ }
106
+
107
+ const liveClock: TranscriptClock = { id: () => ulid(), now: () => new Date().toISOString() };
108
+
109
+ export class TranscriptStore {
110
+ #state: TranscriptState;
111
+ readonly #backend: TranscriptBackend;
112
+ readonly #clock: TranscriptClock;
113
+
114
+ constructor(sessionId: string, options: TranscriptStoreOptions = {}) {
115
+ this.#backend = options.backend ?? memoryBackend();
116
+ this.#clock = options.clock ?? liveClock;
117
+ this.#state = replay(sessionId, [], null);
118
+ }
119
+
120
+ get sessionId(): string { return this.#state.head.sessionId; }
121
+ get head(): SessionHead { return this.#state.head; }
122
+ get usage(): Usage | undefined { return this.#state.head.usage; }
123
+ get size(): number { return this.#state.nodes.size; }
124
+ locate(sessionId = this.sessionId): string { return this.#backend.locate(sessionId); }
125
+ get(id: string): TranscriptEntry | undefined { return this.#state.nodes.get(id); }
126
+
127
+ async append(content: AgentMessage, role?: TranscriptRole, meta?: Readonly<Record<string, unknown>>): Promise<TranscriptEntry> {
128
+ const entry: TranscriptEntry = {
129
+ id: this.#clock.id(),
130
+ parent: this.#state.head.leaf,
131
+ role: role ?? roleForMessage(content),
132
+ content,
133
+ createdAt: this.#clock.now(),
134
+ ...(meta !== undefined ? { meta } : {}),
135
+ };
136
+ await this.#backend.append(this.sessionId, encodeEntry(entry));
137
+ this.#commit(entry);
138
+ return entry;
139
+ }
140
+
141
+ async persistUsage(usage: Usage): Promise<void> {
142
+ this.#state = { nodes: this.#state.nodes, head: { ...this.#state.head, usage } };
143
+ await this.#flush();
144
+ }
145
+
146
+ async branchAt(id: string): Promise<void> {
147
+ if (!this.#state.nodes.has(id)) throw new Error(`TranscriptStore.branchAt: unknown node "${id}"`);
148
+ this.#state = { nodes: this.#state.nodes, head: { sessionId: this.sessionId, leaf: id } };
149
+ await this.#flush();
150
+ }
151
+
152
+ async reset(): Promise<void> {
153
+ this.#state = { nodes: this.#state.nodes, head: { sessionId: this.sessionId, leaf: null } };
154
+ await this.#flush();
155
+ }
156
+
157
+ startNewSession(sessionId: string): void { this.#state = replay(sessionId, [], null); }
158
+
159
+ pathTo(from?: string): TranscriptEntry[] {
160
+ const chain: TranscriptEntry[] = [];
161
+ let cursor = from ?? this.#state.head.leaf;
162
+ const seen = new Set<string>();
163
+ while (cursor !== null) {
164
+ if (seen.has(cursor)) break;
165
+ seen.add(cursor);
166
+ const node = this.#state.nodes.get(cursor);
167
+ if (!node) break;
168
+ chain.push(node);
169
+ cursor = node.parent;
170
+ }
171
+ return chain.reverse();
172
+ }
173
+
174
+ messages(from?: string): AgentMessage[] { return this.pathTo(from).map((entry) => entry.content); }
175
+
176
+ async load(sessionId = this.sessionId): Promise<boolean> {
177
+ const text = await this.#backend.read(sessionId);
178
+ if (text === null) return false;
179
+ const parsed = parseSessionText(sessionId, text);
180
+ const leaf = parsed.head?.leaf ?? deepestLeaf(parsed.entries);
181
+ const base = replay(sessionId, parsed.entries, leaf);
182
+ this.#state = parsed.head?.usage === undefined ? base : { nodes: base.nodes, head: { ...base.head, usage: parsed.head.usage } };
183
+ return true;
184
+ }
185
+
186
+ static async open(sessionId: string, options: TranscriptStoreOptions = {}): Promise<TranscriptStore | null> {
187
+ const store = new TranscriptStore(sessionId, options);
188
+ return await store.load(sessionId) ? store : null;
189
+ }
190
+
191
+ state(): TranscriptState { return this.#state; }
192
+
193
+ #commit(entry: TranscriptEntry): void {
194
+ const nodes = new Map(this.#state.nodes);
195
+ nodes.set(entry.id, entry);
196
+ this.#state = { nodes, head: { sessionId: this.sessionId, leaf: entry.id } };
197
+ }
198
+
199
+ async #flush(): Promise<void> {
200
+ const lines = [encodeHead(this.#state.head), ...[...this.#state.nodes.values()].map(encodeEntry)];
201
+ await this.#backend.rewrite(this.sessionId, lines.join("\n"));
202
+ }
203
+ }
204
+
205
+ export { TRANSCRIPT_SCHEMA };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Shared "does the user have a usable credential for this provider" probe.
3
+ *
4
+ * Used anywhere the console needs to know which providers are actually
5
+ * callable — the `/model` picker (only list authenticated providers) and the
6
+ * banner's Session panel (don't display an unauthenticated model as if it were
7
+ * ready to use) both need the exact same answer, so it lives here once.
8
+ */
9
+ import { useEffect, useState } from "indusagi/react-host";
10
+ import { hasValidServerToken } from "../boot/server-token.js";
11
+ import { serverTierProviders } from "../boot/runners/server-mode.js";
12
+ import type { OverlayServices } from "./contract.js";
13
+
14
+ /**
15
+ * Load the set of provider ids the user has USABLE credentials for, live.
16
+ *
17
+ * Two independent sources count: (1) any saved account in the local vault for a
18
+ * provider `listLoginProviders()` names, and (2) a valid server session token,
19
+ * which makes every gateway-eligible provider usable even though the server
20
+ * tier never writes into the vault. Returns `null` until the probe settles.
21
+ */
22
+ export function useAuthenticatedProviders(services: OverlayServices | undefined, refreshKey?: unknown): Set<string> | null {
23
+ const [authed, setAuthed] = useState<Set<string> | null>(null);
24
+ useEffect(() => {
25
+ if (!services) return;
26
+ let live = true;
27
+ void (async () => {
28
+ const found = new Set<string>();
29
+ try {
30
+ for (const provider of services.listLoginProviders()) {
31
+ try {
32
+ if ((await services.vault.listAccounts(provider.id)).length > 0) {
33
+ found.add(provider.id);
34
+ }
35
+ } catch {
36
+ // ignore
37
+ }
38
+ }
39
+ } catch {
40
+ // ignore
41
+ }
42
+ try {
43
+ if (await hasValidServerToken()) {
44
+ for (const provider of serverTierProviders()) found.add(provider);
45
+ }
46
+ } catch {
47
+ // ignore
48
+ }
49
+ if (live) setAuthed(found);
50
+ })();
51
+ return () => {
52
+ live = false;
53
+ };
54
+ }, [refreshKey]);
55
+ return authed;
56
+ }