@agentproto/runtime 0.5.0 → 0.6.0

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.
@@ -1,57 +1 @@
1
- /**
2
- * `~/.agentproto/providers.json` — the provider API-key store (mode 0600).
3
- *
4
- * Distinct from `credentials.json` (host-binding OAuth tokens for
5
- * `serve --connect`) and from per-adapter `setup[]` tokens. This file holds
6
- * the LLM/model **provider** keys (anthropic, openrouter, openai, …) that the
7
- * model gateways in spawned agents read from the environment.
8
- *
9
- * Why a store at all: before this, the only way to give the daemon a provider
10
- * key was an ambient `export FOO_API_KEY=…` in whatever shell launched
11
- * `serve`. That's invisible, easy to forget, and per-shell. Storing the keys
12
- * here (0600, same-user-only) lets `serve` inject them at boot — set once,
13
- * works for every daemon. Explicit env still wins (see injectProviderKeysIntoEnv).
14
- *
15
- * Keys never leave this file except into the daemon's own process env; they
16
- * are never logged. A browser-loaded localhost page can't read a 0600 file —
17
- * the same defence the runtime.json bearer token relies on.
18
- */
19
- /**
20
- * Canonical provider → environment-variable name. The spawned model gateways
21
- * (Mastra in mastra-agent, hermes/opencode's routers) read the provider key
22
- * from these env names. Aligned with the adapter manifests' `models.env`
23
- * maps + the common SDK conventions.
24
- */
25
- declare const PROVIDER_ENV_VARS: Readonly<Record<string, string>>;
26
- type KnownProvider = keyof typeof PROVIDER_ENV_VARS;
27
- interface ProviderEntry {
28
- /** The provider API key (or gateway key). */
29
- apiKey: string;
30
- /** Optional custom base URL (self-hosted / proxy). */
31
- baseUrl?: string;
32
- /** Wall-clock ISO timestamp the key was last set. */
33
- updatedAt: string;
34
- }
35
- interface ProvidersFile {
36
- version: 1;
37
- providers: Record<string, ProviderEntry>;
38
- }
39
- declare function providersPath(): string;
40
- /** Resolve the env-var name for a provider (canonical map, else
41
- * `<PROVIDER>_API_KEY` upper-snake fallback so new providers still work). */
42
- declare function providerEnvVar(provider: string): string;
43
- declare function loadProviders(): Promise<ProvidersFile>;
44
- /** Set (or replace) a provider's key. Returns the env-var it maps to. */
45
- declare function setProviderKey(provider: string, apiKey: string, baseUrl?: string): Promise<string>;
46
- /** Remove a provider's key. Returns true if it existed. */
47
- declare function removeProviderKey(provider: string): Promise<boolean>;
48
- /**
49
- * Inject stored provider keys into a target env (default `process.env`).
50
- * **Explicit env always wins** — a var already set is never overwritten, so a
51
- * one-off `FOO_API_KEY=… serve` or a CI secret takes precedence over the
52
- * store. Returns the list of provider names actually injected (for a boot log
53
- * that names providers, never values).
54
- */
55
- declare function injectProviderKeysIntoEnv(env?: NodeJS.ProcessEnv): Promise<string[]>;
56
-
57
- export { type KnownProvider, PROVIDER_ENV_VARS, type ProviderEntry, type ProvidersFile, injectProviderKeysIntoEnv, loadProviders, providerEnvVar, providersPath, removeProviderKey, setProviderKey };
1
+ export * from '@agentproto/providers-store';
@@ -1,92 +1,3 @@
1
- import { readFile, mkdir, writeFile } from 'fs/promises';
2
- import { homedir } from 'os';
3
- import { resolve, join } from 'path';
4
-
5
- /**
6
- * @agentproto/runtime v0.1.0-alpha
7
- * Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
8
- */
9
-
10
- var PROVIDER_ENV_VARS = {
11
- anthropic: "ANTHROPIC_API_KEY",
12
- openrouter: "OPENROUTER_API_KEY",
13
- openai: "OPENAI_API_KEY",
14
- google: "GOOGLE_GENERATIVE_AI_API_KEY",
15
- groq: "GROQ_API_KEY",
16
- mistral: "MISTRAL_API_KEY",
17
- // Moonshot (Kimi) — an Anthropic-compatible gateway; its key is sent as a
18
- // Bearer token (ANTHROPIC_AUTH_TOKEN) with a custom base_url by the claude-sdk
19
- // gateway resolver. Native routers (Mastra/hermes) read this env name directly.
20
- moonshot: "MOONSHOT_API_KEY",
21
- // MiniMax — direct LLM provider (also serves voice/video under the same key).
22
- minimax: "MINIMAX_API_KEY",
23
- // Vercel AI Gateway — a gateway provider, like openrouter, that fronts many
24
- // upstream model families behind one key.
25
- "vercel-ai-gateway": "AI_GATEWAY_API_KEY"
26
- };
27
- function emptyFile() {
28
- return { version: 1, providers: {} };
29
- }
30
- function providersPath() {
31
- return resolve(homedir(), ".agentproto", "providers.json");
32
- }
33
- function providerEnvVar(provider) {
34
- return PROVIDER_ENV_VARS[provider] ?? `${provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`;
35
- }
36
- async function loadProviders() {
37
- try {
38
- const raw = await readFile(providersPath(), "utf8");
39
- const parsed = JSON.parse(raw);
40
- if (!parsed || typeof parsed !== "object" || !("providers" in parsed) || typeof parsed.providers !== "object" || parsed.providers === null) {
41
- return emptyFile();
42
- }
43
- return {
44
- version: 1,
45
- providers: parsed.providers
46
- };
47
- } catch {
48
- return emptyFile();
49
- }
50
- }
51
- async function writeProviders(file) {
52
- const dir = join(homedir(), ".agentproto");
53
- await mkdir(dir, { recursive: true });
54
- await writeFile(providersPath(), JSON.stringify(file, null, 2) + "\n", {
55
- encoding: "utf8",
56
- mode: 384
57
- });
58
- }
59
- async function setProviderKey(provider, apiKey, baseUrl) {
60
- const file = await loadProviders();
61
- file.providers[provider] = {
62
- apiKey,
63
- ...baseUrl ? { baseUrl } : {},
64
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
65
- };
66
- await writeProviders(file);
67
- return providerEnvVar(provider);
68
- }
69
- async function removeProviderKey(provider) {
70
- const file = await loadProviders();
71
- if (!(provider in file.providers)) return false;
72
- delete file.providers[provider];
73
- await writeProviders(file);
74
- return true;
75
- }
76
- async function injectProviderKeysIntoEnv(env = process.env) {
77
- const file = await loadProviders();
78
- const injected = [];
79
- for (const [provider, entry] of Object.entries(file.providers)) {
80
- if (!entry?.apiKey) continue;
81
- const name = providerEnvVar(provider);
82
- if (env[name]) continue;
83
- env[name] = entry.apiKey;
84
- if (entry.baseUrl) env[`${provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_BASE_URL`] = entry.baseUrl;
85
- injected.push(provider);
86
- }
87
- return injected;
88
- }
89
-
90
- export { PROVIDER_ENV_VARS, injectProviderKeysIntoEnv, loadProviders, providerEnvVar, providersPath, removeProviderKey, setProviderKey };
1
+ export * from '@agentproto/providers-store';
91
2
  //# sourceMappingURL=providers-store.mjs.map
92
3
  //# sourceMappingURL=providers-store.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/providers-store.ts"],"names":[],"mappings":"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","file":"providers-store.mjs","sourcesContent":["/**\n * `~/.agentproto/providers.json` — the provider API-key store (mode 0600).\n *\n * Distinct from `credentials.json` (host-binding OAuth tokens for\n * `serve --connect`) and from per-adapter `setup[]` tokens. This file holds\n * the LLM/model **provider** keys (anthropic, openrouter, openai, …) that the\n * model gateways in spawned agents read from the environment.\n *\n * Why a store at all: before this, the only way to give the daemon a provider\n * key was an ambient `export FOO_API_KEY=…` in whatever shell launched\n * `serve`. That's invisible, easy to forget, and per-shell. Storing the keys\n * here (0600, same-user-only) lets `serve` inject them at boot — set once,\n * works for every daemon. Explicit env still wins (see injectProviderKeysIntoEnv).\n *\n * Keys never leave this file except into the daemon's own process env; they\n * are never logged. A browser-loaded localhost page can't read a 0600 file —\n * the same defence the runtime.json bearer token relies on.\n */\n\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\"\nimport { homedir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\n\n/**\n * Canonical provider → environment-variable name. The spawned model gateways\n * (Mastra in mastra-agent, hermes/opencode's routers) read the provider key\n * from these env names. Aligned with the adapter manifests' `models.env`\n * maps + the common SDK conventions.\n */\nexport const PROVIDER_ENV_VARS: Readonly<Record<string, string>> = {\n anthropic: \"ANTHROPIC_API_KEY\",\n openrouter: \"OPENROUTER_API_KEY\",\n openai: \"OPENAI_API_KEY\",\n google: \"GOOGLE_GENERATIVE_AI_API_KEY\",\n groq: \"GROQ_API_KEY\",\n mistral: \"MISTRAL_API_KEY\",\n // Moonshot (Kimi) — an Anthropic-compatible gateway; its key is sent as a\n // Bearer token (ANTHROPIC_AUTH_TOKEN) with a custom base_url by the claude-sdk\n // gateway resolver. Native routers (Mastra/hermes) read this env name directly.\n moonshot: \"MOONSHOT_API_KEY\",\n // MiniMax — direct LLM provider (also serves voice/video under the same key).\n minimax: \"MINIMAX_API_KEY\",\n // Vercel AI Gateway — a gateway provider, like openrouter, that fronts many\n // upstream model families behind one key.\n \"vercel-ai-gateway\": \"AI_GATEWAY_API_KEY\",\n}\n\nexport type KnownProvider = keyof typeof PROVIDER_ENV_VARS\n\nexport interface ProviderEntry {\n /** The provider API key (or gateway key). */\n apiKey: string\n /** Optional custom base URL (self-hosted / proxy). */\n baseUrl?: string\n /** Wall-clock ISO timestamp the key was last set. */\n updatedAt: string\n}\n\nexport interface ProvidersFile {\n version: 1\n providers: Record<string, ProviderEntry>\n}\n\n/** Fresh empty file each call — never share the `providers` object, or a\n * later `setProviderKey` mutation leaks into every other load in-process. */\nfunction emptyFile(): ProvidersFile {\n return { version: 1, providers: {} }\n}\n\nexport function providersPath(): string {\n return resolve(homedir(), \".agentproto\", \"providers.json\")\n}\n\n/** Resolve the env-var name for a provider (canonical map, else\n * `<PROVIDER>_API_KEY` upper-snake fallback so new providers still work). */\nexport function providerEnvVar(provider: string): string {\n return (\n PROVIDER_ENV_VARS[provider] ??\n `${provider.toUpperCase().replace(/[^A-Z0-9]+/g, \"_\")}_API_KEY`\n )\n}\n\nexport async function loadProviders(): Promise<ProvidersFile> {\n try {\n const raw = await readFile(providersPath(), \"utf8\")\n const parsed = JSON.parse(raw) as unknown\n if (\n !parsed ||\n typeof parsed !== \"object\" ||\n !(\"providers\" in parsed) ||\n typeof (parsed as Record<string, unknown>).providers !== \"object\" ||\n (parsed as Record<string, unknown>).providers === null\n ) {\n return emptyFile()\n }\n return {\n version: 1,\n providers: (parsed as ProvidersFile).providers,\n }\n } catch {\n return emptyFile() // ENOENT / malformed → empty\n }\n}\n\nasync function writeProviders(file: ProvidersFile): Promise<void> {\n const dir = join(homedir(), \".agentproto\")\n await mkdir(dir, { recursive: true })\n // mode 0600 so other local users can't read the keys.\n await writeFile(providersPath(), JSON.stringify(file, null, 2) + \"\\n\", {\n encoding: \"utf8\",\n mode: 0o600,\n })\n}\n\n/** Set (or replace) a provider's key. Returns the env-var it maps to. */\nexport async function setProviderKey(\n provider: string,\n apiKey: string,\n baseUrl?: string,\n): Promise<string> {\n const file = await loadProviders()\n file.providers[provider] = {\n apiKey,\n ...(baseUrl ? { baseUrl } : {}),\n updatedAt: new Date().toISOString(),\n }\n await writeProviders(file)\n return providerEnvVar(provider)\n}\n\n/** Remove a provider's key. Returns true if it existed. */\nexport async function removeProviderKey(provider: string): Promise<boolean> {\n const file = await loadProviders()\n if (!(provider in file.providers)) return false\n delete file.providers[provider]\n await writeProviders(file)\n return true\n}\n\n/**\n * Inject stored provider keys into a target env (default `process.env`).\n * **Explicit env always wins** — a var already set is never overwritten, so a\n * one-off `FOO_API_KEY=… serve` or a CI secret takes precedence over the\n * store. Returns the list of provider names actually injected (for a boot log\n * that names providers, never values).\n */\nexport async function injectProviderKeysIntoEnv(\n env: NodeJS.ProcessEnv = process.env,\n): Promise<string[]> {\n const file = await loadProviders()\n const injected: string[] = []\n for (const [provider, entry] of Object.entries(file.providers)) {\n if (!entry?.apiKey) continue\n const name = providerEnvVar(provider)\n if (env[name]) continue // explicit env wins\n env[name] = entry.apiKey\n if (entry.baseUrl) env[`${provider.toUpperCase().replace(/[^A-Z0-9]+/g, \"_\")}_BASE_URL`] = entry.baseUrl\n injected.push(provider)\n }\n return injected\n}\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"providers-store.mjs","sourcesContent":[]}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Export a clean, readable transcript, dispatched between two kinds of
3
+ * source:
4
+ *
5
+ * native — the adapter's OWN persistence, re-read after the fact:
6
+ * claude-code — ~/.claude/projects/<cwd-encoded>/<sessionId>.jsonl
7
+ * hermes — ~/.hermes/state.db via node:sqlite (read-only)
8
+ * daemon — agentproto's own capture, `~/.agentproto/sessions/<id>/events.jsonl`
9
+ * (written by transcript-writer.ts at the same point `projectEvent`
10
+ * flattens events into the ANSI ring buffer, ahead of that flattening).
11
+ * Available for ANY agent-cli session (acp or print protocol), not just
12
+ * the two adapters with a native store.
13
+ *
14
+ * `source: "auto"` (the default) tries native first and falls back to
15
+ * daemon events when there's no native strategy or its store can't be
16
+ * read; `"native"` / `"daemon"` force one and surface its own error.
17
+ *
18
+ * Output: Markdown (default) or JSON, built from a shared ExportedSession
19
+ * model so every source produces identical rendering logic.
20
+ */
21
+
22
+ interface ExportedMessage {
23
+ role: "user" | "assistant" | "tool" | "system";
24
+ text?: string;
25
+ reasoning?: string;
26
+ toolName?: string;
27
+ toolCalls?: {
28
+ name: string;
29
+ args: string;
30
+ }[];
31
+ ts?: number;
32
+ }
33
+ interface ExportedSessionMeta {
34
+ title?: string;
35
+ model?: string;
36
+ startedAt?: string;
37
+ endedAt?: string;
38
+ messageCount?: number;
39
+ toolCallCount?: number;
40
+ tokens?: {
41
+ input?: number;
42
+ output?: number;
43
+ cacheRead?: number;
44
+ cacheWrite?: number;
45
+ reasoning?: number;
46
+ };
47
+ costUsd?: number;
48
+ source?: string;
49
+ }
50
+ interface ExportedSession {
51
+ meta: ExportedSessionMeta;
52
+ messages: ExportedMessage[];
53
+ }
54
+
55
+ /**
56
+ * Pure heuristic module that folds an exported session's messages into a
57
+ * "story": chapters (inferred sub-tasks) + steps (one per fixed-height feed
58
+ * row). Backs the `agentproto_session_story` MCP app panel.
59
+ *
60
+ * v1 = pure heuristics, NO LLM. Everything here is deterministic and
61
+ * unit-testable so a future LLM-backed chaptering/summarizing pass can
62
+ * replace the internals behind the same `buildStory(messages) => Story`
63
+ * interface (mirrors the summarize_session heuristic → LLM swap path
64
+ * described in agents-overview-app.ts).
65
+ *
66
+ * Folding rule (mirrors transcript-export.ts's own batching): an assistant
67
+ * message plus every immediately-following `role: "tool"` message collapse
68
+ * into ONE step. A user message is always its own step, and is also a
69
+ * candidate chapter boundary.
70
+ */
71
+
72
+ type StoryStepKind = "text" | "edit" | "bash" | "read" | "user";
73
+ type ChapterStatus = "done" | "cur";
74
+ type RouteVerdict = "cont" | "newt";
75
+ interface StoryChapter {
76
+ id: string;
77
+ title: string;
78
+ status: ChapterStatus;
79
+ }
80
+ type StoryItem = {
81
+ text: string;
82
+ } | {
83
+ h: string;
84
+ r: string;
85
+ };
86
+ interface StoryStep {
87
+ chap: string;
88
+ kind: StoryStepKind;
89
+ /** HH:MM:SS (UTC) when the source message carried a `ts`, else "". */
90
+ ts: string;
91
+ sum: string;
92
+ raw1: string;
93
+ count: number;
94
+ facts: string[];
95
+ items: StoryItem[];
96
+ route?: RouteVerdict;
97
+ }
98
+ interface Story {
99
+ chapters: StoryChapter[];
100
+ steps: StoryStep[];
101
+ }
102
+ /** Dominant kind for a group of tool calls: edit/write → edit, bash/terminal/
103
+ * command → bash, read/grep/glob → read, else text (pure assistant text). */
104
+ declare function classifyKind(toolCalls?: readonly {
105
+ name: string;
106
+ }[]): StoryStepKind;
107
+ declare function classifyRoute(text: string): {
108
+ route: RouteVerdict;
109
+ title?: string;
110
+ };
111
+ declare function buildStory(messages: readonly ExportedMessage[]): Story;
112
+
113
+ export { type ChapterStatus, type ExportedMessage, type ExportedSession, type ExportedSessionMeta, type RouteVerdict, type Story, type StoryChapter, type StoryItem, type StoryStep, type StoryStepKind, buildStory, classifyKind, classifyRoute };
@@ -0,0 +1,314 @@
1
+ /**
2
+ * @agentproto/runtime v0.1.0-alpha
3
+ * Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
4
+ */
5
+
6
+ // src/tool-presenter.ts
7
+ var SALIENT_ARG_KEYS = [
8
+ "file_path",
9
+ "path",
10
+ "filePath",
11
+ "file",
12
+ "command",
13
+ "pattern",
14
+ "query",
15
+ "q",
16
+ "url",
17
+ "todos",
18
+ "description",
19
+ "prompt"
20
+ ];
21
+ var MAX_CALL_LENGTH = 120;
22
+ var MAX_RESULT_LENGTH = 160;
23
+ function isRecord(value) {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+ function truncate(value, max) {
27
+ const oneLine = value.replace(/\s+/g, " ").trim();
28
+ return oneLine.length > max ? `${oneLine.slice(0, max - 1)}\u2026` : oneLine;
29
+ }
30
+ function formatArgValue(value) {
31
+ if (typeof value === "string") return value;
32
+ if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? "" : "s"}`;
33
+ if (isRecord(value)) return JSON.stringify(value);
34
+ return String(value);
35
+ }
36
+ function pickSalientArg(args) {
37
+ for (const key of SALIENT_ARG_KEYS) {
38
+ const value = args[key];
39
+ if (value !== void 0 && value !== null && value !== "") {
40
+ return formatArgValue(value);
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ function subagentSummary(args) {
46
+ const description = typeof args.description === "string" ? args.description : typeof args.prompt === "string" ? args.prompt : "subagent";
47
+ return `\u21B3 subagent: ${truncate(description, MAX_CALL_LENGTH)}`;
48
+ }
49
+ var BESPOKE_TOOLS = {
50
+ schedulewakeup: (args) => {
51
+ const delay = args.delaySeconds ?? args.delay ?? "?";
52
+ const reason = typeof args.reason === "string" ? args.reason : "";
53
+ return reason ? `\u23F0 wake in ${delay}s \u2014 ${reason}` : `\u23F0 wake in ${delay}s`;
54
+ },
55
+ task: subagentSummary,
56
+ agent: subagentSummary,
57
+ todowrite: (args) => {
58
+ const todos = Array.isArray(args.todos) ? args.todos : [];
59
+ return `\u2611 todos (${todos.length})`;
60
+ },
61
+ exitplanmode: () => "\u{1F4CB} plan ready"
62
+ };
63
+ function formatToolCall(toolName, args) {
64
+ const name = toolName || "tool";
65
+ const argsRecord = isRecord(args) ? args : {};
66
+ const bespoke = BESPOKE_TOOLS[name.toLowerCase()];
67
+ if (bespoke) return bespoke(argsRecord);
68
+ const salient = pickSalientArg(argsRecord);
69
+ if (salient !== null) {
70
+ if (name.toLowerCase().includes(salient.toLowerCase())) {
71
+ return truncate(name, MAX_CALL_LENGTH);
72
+ }
73
+ return truncate(`${name} ${salient}`, MAX_CALL_LENGTH);
74
+ }
75
+ if (Object.keys(argsRecord).length === 0) return name;
76
+ return truncate(`${name} ${JSON.stringify(args)}`, MAX_CALL_LENGTH);
77
+ }
78
+ function extractText(value) {
79
+ if (value == null) return null;
80
+ if (typeof value === "string") return value;
81
+ if (Array.isArray(value)) {
82
+ const parts = value.map(extractText).filter((v) => v != null);
83
+ return parts.length ? parts.join("\n") : null;
84
+ }
85
+ if (isRecord(value)) {
86
+ if (typeof value.text === "string") return value.text;
87
+ if (typeof value.message === "string") return value.message;
88
+ if (Array.isArray(value.content)) return extractText(value.content);
89
+ if (typeof value.error === "string") return value.error;
90
+ if (isRecord(value.error) && typeof value.error.message === "string") {
91
+ return value.error.message;
92
+ }
93
+ return null;
94
+ }
95
+ return null;
96
+ }
97
+ function formatToolResult(toolName, result, isError) {
98
+ const text = extractText(result);
99
+ if (isError) {
100
+ const message = text ?? (result != null ? JSON.stringify(result) : "failed");
101
+ const firstLine = message.split(/\r?\n/)[0] ?? message;
102
+ return truncate(firstLine, MAX_RESULT_LENGTH);
103
+ }
104
+ if (text == null) return null;
105
+ const trimmed = text.trim();
106
+ if (!trimmed) return null;
107
+ const lines = trimmed.split(/\r?\n/);
108
+ if (lines.length > 1) {
109
+ const bytes = Buffer.byteLength(trimmed, "utf8");
110
+ return `${lines.length} lines, ${bytes}B`;
111
+ }
112
+ return truncate(lines[0], MAX_RESULT_LENGTH);
113
+ }
114
+
115
+ // src/session-story.ts
116
+ function formatTs(ts) {
117
+ if (ts === void 0 || Number.isNaN(ts)) return "";
118
+ return new Date(ts).toISOString().slice(11, 19);
119
+ }
120
+ function firstMeaningfulLine(text) {
121
+ if (!text) return void 0;
122
+ const line = text.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
123
+ return line;
124
+ }
125
+ function truncate2(text, max) {
126
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
127
+ }
128
+ function parseArgs(argsJson) {
129
+ try {
130
+ return JSON.parse(argsJson);
131
+ } catch {
132
+ return {};
133
+ }
134
+ }
135
+ function lineCount(text) {
136
+ return text.split("\n").filter((l) => l.trim().length > 0).length || 1;
137
+ }
138
+ function classifyKind(toolCalls) {
139
+ if (!toolCalls || toolCalls.length === 0) return "text";
140
+ const names = toolCalls.map((t) => t.name.toLowerCase());
141
+ if (names.some((n) => /edit|write/.test(n))) return "edit";
142
+ if (names.some((n) => /bash|terminal|command/.test(n))) return "bash";
143
+ if (names.some((n) => /read|grep|glob/.test(n))) return "read";
144
+ return "text";
145
+ }
146
+ var NEW_CHAPTER_RE = /\b(aussi|autre|ensuite|nouveau|nouvelle|plut[oô]t|maintenant|apr[eè]s ça|il faudrait|peux[- ]tu|on pourrait|ajoute|g[eè]re)\b/iu;
147
+ function classifyRoute(text) {
148
+ const newt = NEW_CHAPTER_RE.test(text);
149
+ if (!newt) return { route: "cont" };
150
+ const title = text.replace(/[.?!].*$/, "").slice(0, 42);
151
+ return { route: "newt", title };
152
+ }
153
+ function foldToolStep(assistant, toolResults) {
154
+ const toolCalls = assistant.toolCalls ?? [];
155
+ const kind = classifyKind(toolCalls);
156
+ const count = toolCalls.length || 1;
157
+ const items = [];
158
+ const facts = [];
159
+ if (assistant.text?.trim()) items.push({ text: assistant.text.trim() });
160
+ toolCalls.forEach((tc, i) => {
161
+ const args = parseArgs(tc.args);
162
+ const h = formatToolCall(tc.name, args);
163
+ const resultMsg = toolResults[i];
164
+ const resultText = resultMsg?.text ?? "";
165
+ const isError = resultText.startsWith("[error]");
166
+ const r = isError ? resultText.slice("[error]".length).trim() : resultText;
167
+ items.push({ h, r });
168
+ const fact = formatToolResult(tc.name, r, isError);
169
+ if (fact) facts.push(fact);
170
+ });
171
+ const firstLine = firstMeaningfulLine(assistant.text);
172
+ const firstToolCall = toolCalls[0];
173
+ const sum = firstLine ?? (firstToolCall ? formatToolCall(firstToolCall.name, parseArgs(firstToolCall.args)) : "\u2026");
174
+ let raw1;
175
+ if (toolCalls.length === 0) {
176
+ raw1 = `assistant \xB7 ${lineCount(assistant.text ?? "")} ligne(s)`;
177
+ } else if (toolCalls.length === 1 && firstToolCall) {
178
+ raw1 = formatToolCall(firstToolCall.name, parseArgs(firstToolCall.args));
179
+ } else {
180
+ raw1 = `${firstToolCall?.name ?? "tool"} \xD7${toolCalls.length}`;
181
+ }
182
+ return {
183
+ kind,
184
+ ts: formatTs(assistant.ts),
185
+ sum,
186
+ raw1,
187
+ count,
188
+ facts,
189
+ items
190
+ };
191
+ }
192
+ function foldUserStep(msg) {
193
+ const text = msg.text ?? "";
194
+ const sum = `\xAB ${truncate2(text, 80)} \xBB`;
195
+ return {
196
+ kind: "user",
197
+ ts: formatTs(msg.ts),
198
+ sum,
199
+ raw1: `user \xB7 ${lineCount(text)} ligne(s)`,
200
+ count: 1,
201
+ facts: [],
202
+ items: [{ text }],
203
+ userText: text
204
+ };
205
+ }
206
+ function foldOrphanToolStep(msg) {
207
+ const text = msg.text ?? "";
208
+ const isError = text.startsWith("[error]");
209
+ const r = isError ? text.slice("[error]".length).trim() : text;
210
+ const name = msg.toolName ?? "tool";
211
+ const fact = formatToolResult(name, r, isError);
212
+ return {
213
+ kind: classifyKind([{ name }]),
214
+ ts: formatTs(msg.ts),
215
+ sum: msg.toolName ? `${msg.toolName} \xB7 r\xE9sultat` : "R\xE9sultat d'outil",
216
+ raw1: msg.toolName ?? "tool",
217
+ count: 1,
218
+ facts: fact ? [fact] : [],
219
+ items: [{ h: name, r }]
220
+ };
221
+ }
222
+ function foldSystemStep(msg) {
223
+ const text = msg.text ?? "";
224
+ return {
225
+ kind: "text",
226
+ ts: formatTs(msg.ts),
227
+ sum: firstMeaningfulLine(text) ?? text,
228
+ raw1: "system",
229
+ count: 1,
230
+ facts: [],
231
+ items: text ? [{ text }] : []
232
+ };
233
+ }
234
+ function foldMessages(messages) {
235
+ const steps = [];
236
+ let i = 0;
237
+ while (i < messages.length) {
238
+ const msg = messages[i];
239
+ if (msg.role === "user") {
240
+ steps.push(foldUserStep(msg));
241
+ i += 1;
242
+ continue;
243
+ }
244
+ if (msg.role === "assistant") {
245
+ let j = i + 1;
246
+ const toolResults = [];
247
+ while (j < messages.length && messages[j].role === "tool") {
248
+ toolResults.push(messages[j]);
249
+ j += 1;
250
+ }
251
+ steps.push(foldToolStep(msg, toolResults));
252
+ i = j;
253
+ continue;
254
+ }
255
+ if (msg.role === "tool") {
256
+ steps.push(foldOrphanToolStep(msg));
257
+ i += 1;
258
+ continue;
259
+ }
260
+ steps.push(foldSystemStep(msg));
261
+ i += 1;
262
+ }
263
+ return steps;
264
+ }
265
+ function buildStory(messages) {
266
+ const folded = foldMessages(messages);
267
+ const chapters = [];
268
+ const steps = [];
269
+ let currentChapterId;
270
+ let sawFirstUser = false;
271
+ const closeCurrent = () => {
272
+ const cur = chapters.find((c) => c.id === currentChapterId);
273
+ if (cur) cur.status = "done";
274
+ };
275
+ const openChapter = (title) => {
276
+ const id = `c${chapters.length + 1}`;
277
+ chapters.push({ id, title, status: "cur" });
278
+ return id;
279
+ };
280
+ for (const step of folded) {
281
+ let route;
282
+ if (step.kind === "user" && step.userText !== void 0) {
283
+ if (!sawFirstUser) {
284
+ sawFirstUser = true;
285
+ currentChapterId = openChapter("Cadrage");
286
+ } else {
287
+ const verdict = classifyRoute(step.userText);
288
+ route = verdict.route;
289
+ if (verdict.route === "newt") {
290
+ closeCurrent();
291
+ currentChapterId = openChapter(verdict.title || "Nouvelle sous-t\xE2che");
292
+ }
293
+ }
294
+ } else if (currentChapterId === void 0) {
295
+ currentChapterId = openChapter("Cadrage");
296
+ }
297
+ steps.push({
298
+ chap: currentChapterId,
299
+ kind: step.kind,
300
+ ts: step.ts,
301
+ sum: step.sum,
302
+ raw1: step.raw1,
303
+ count: step.count,
304
+ facts: step.facts,
305
+ items: step.items,
306
+ ...route ? { route } : {}
307
+ });
308
+ }
309
+ return { chapters, steps };
310
+ }
311
+
312
+ export { buildStory, classifyKind, classifyRoute };
313
+ //# sourceMappingURL=session-story.mjs.map
314
+ //# sourceMappingURL=session-story.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tool-presenter.ts","../src/session-story.ts"],"names":["truncate"],"mappings":"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","file":"session-story.mjs","sourcesContent":["/**\n * Turns a raw tool-call name + args (and its eventual result) into a short,\n * human-readable line for ring-buffer / CLI / transcript rendering. Without\n * this, every tool call renders as a bare `[tool] view` with no args and no\n * outcome — this module is the one place that knows how to summarize both.\n */\n\nconst SALIENT_ARG_KEYS = [\n \"file_path\",\n \"path\",\n \"filePath\",\n \"file\",\n \"command\",\n \"pattern\",\n \"query\",\n \"q\",\n \"url\",\n \"todos\",\n \"description\",\n \"prompt\",\n] as const\n\nconst MAX_CALL_LENGTH = 120\nconst MAX_RESULT_LENGTH = 160\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction truncate(value: string, max: number): string {\n const oneLine = value.replace(/\\s+/g, \" \").trim()\n return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine\n}\n\nfunction formatArgValue(value: unknown): string {\n if (typeof value === \"string\") return value\n if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? \"\" : \"s\"}`\n if (isRecord(value)) return JSON.stringify(value)\n return String(value)\n}\n\nfunction pickSalientArg(args: Record<string, unknown>): string | null {\n for (const key of SALIENT_ARG_KEYS) {\n const value = args[key]\n if (value !== undefined && value !== null && value !== \"\") {\n return formatArgValue(value)\n }\n }\n return null\n}\n\ntype BespokeFormatter = (args: Record<string, unknown>) => string\n\nfunction subagentSummary(args: Record<string, unknown>): string {\n const description =\n typeof args.description === \"string\"\n ? args.description\n : typeof args.prompt === \"string\"\n ? args.prompt\n : \"subagent\"\n return `↳ subagent: ${truncate(description, MAX_CALL_LENGTH)}`\n}\n\n// Bespoke one-liners for control tools where the generic arg-sniffing\n// below would either miss the point (ScheduleWakeup's payload isn't a\n// file/command/pattern) or just repeat the tool name pointlessly\n// (TodoWrite, ExitPlanMode carry no salient single-value arg).\nconst BESPOKE_TOOLS: Record<string, BespokeFormatter> = {\n schedulewakeup: (args) => {\n const delay = args.delaySeconds ?? args.delay ?? \"?\"\n const reason = typeof args.reason === \"string\" ? args.reason : \"\"\n return reason ? `⏰ wake in ${delay}s — ${reason}` : `⏰ wake in ${delay}s`\n },\n task: subagentSummary,\n agent: subagentSummary,\n todowrite: (args) => {\n const todos = Array.isArray(args.todos) ? args.todos : []\n return `☑ todos (${todos.length})`\n },\n exitplanmode: () => \"📋 plan ready\",\n}\n\n/** A one-line human summary of a tool call, e.g. `read src/foo.ts` or `⏰ wake in 30s — checking CI`. */\nexport function formatToolCall(toolName: string, args: unknown): string {\n const name = toolName || \"tool\"\n const argsRecord = isRecord(args) ? args : {}\n\n const bespoke = BESPOKE_TOOLS[name.toLowerCase()]\n if (bespoke) return bespoke(argsRecord)\n\n const salient = pickSalientArg(argsRecord)\n if (salient !== null) {\n // Some ACP agents (e.g. claude-agent-acp) already bake the salient\n // value into a curated title — \"Read src/foo.ts\" alongside\n // arguments.path === \"src/foo.ts\". Appending it again would render\n // \"Read src/foo.ts src/foo.ts\". Only append when it adds new info.\n if (name.toLowerCase().includes(salient.toLowerCase())) {\n return truncate(name, MAX_CALL_LENGTH)\n }\n return truncate(`${name} ${salient}`, MAX_CALL_LENGTH)\n }\n\n if (Object.keys(argsRecord).length === 0) return name\n\n return truncate(`${name} ${JSON.stringify(args)}`, MAX_CALL_LENGTH)\n}\n\nfunction extractText(value: unknown): string | null {\n if (value == null) return null\n if (typeof value === \"string\") return value\n if (Array.isArray(value)) {\n const parts = value.map(extractText).filter((v): v is string => v != null)\n return parts.length ? parts.join(\"\\n\") : null\n }\n if (isRecord(value)) {\n if (typeof value.text === \"string\") return value.text\n if (typeof value.message === \"string\") return value.message\n if (Array.isArray(value.content)) return extractText(value.content)\n if (typeof value.error === \"string\") return value.error\n if (isRecord(value.error) && typeof value.error.message === \"string\") {\n return value.error.message\n }\n return null\n }\n return null\n}\n\n/**\n * A short outcome line for a completed tool call, or `null` when there's\n * nothing useful to show (e.g. an empty/void result). `toolName` is\n * accepted for parity with `formatToolCall` and future bespoke result\n * formatting, but generic text extraction covers today's cases.\n */\nexport function formatToolResult(\n toolName: string | undefined,\n result: unknown,\n isError: boolean,\n): string | null {\n void toolName\n const text = extractText(result)\n\n if (isError) {\n const message = text ?? (result != null ? JSON.stringify(result) : \"failed\")\n const firstLine = message.split(/\\r?\\n/)[0] ?? message\n return truncate(firstLine, MAX_RESULT_LENGTH)\n }\n\n if (text == null) return null\n const trimmed = text.trim()\n if (!trimmed) return null\n\n const lines = trimmed.split(/\\r?\\n/)\n if (lines.length > 1) {\n const bytes = Buffer.byteLength(trimmed, \"utf8\")\n return `${lines.length} lines, ${bytes}B`\n }\n return truncate(lines[0]!, MAX_RESULT_LENGTH)\n}\n","/**\n * Pure heuristic module that folds an exported session's messages into a\n * \"story\": chapters (inferred sub-tasks) + steps (one per fixed-height feed\n * row). Backs the `agentproto_session_story` MCP app panel.\n *\n * v1 = pure heuristics, NO LLM. Everything here is deterministic and\n * unit-testable so a future LLM-backed chaptering/summarizing pass can\n * replace the internals behind the same `buildStory(messages) => Story`\n * interface (mirrors the summarize_session heuristic → LLM swap path\n * described in agents-overview-app.ts).\n *\n * Folding rule (mirrors transcript-export.ts's own batching): an assistant\n * message plus every immediately-following `role: \"tool\"` message collapse\n * into ONE step. A user message is always its own step, and is also a\n * candidate chapter boundary.\n */\n\nimport { formatToolCall, formatToolResult } from \"./tool-presenter.js\"\nimport type { ExportedMessage } from \"./transcript-export.js\"\n\n// Re-exported so consumers of buildStory (e.g. the CLI's `sessions story`\n// command) can type the `/sessions/:id/export?format=json` payload they\n// feed into it without a second import from transcript-export.js.\nexport type { ExportedMessage, ExportedSession, ExportedSessionMeta } from \"./transcript-export.js\"\n\nexport type StoryStepKind = \"text\" | \"edit\" | \"bash\" | \"read\" | \"user\"\nexport type ChapterStatus = \"done\" | \"cur\"\nexport type RouteVerdict = \"cont\" | \"newt\"\n\nexport interface StoryChapter {\n id: string\n title: string\n status: ChapterStatus\n}\n\nexport type StoryItem = { text: string } | { h: string; r: string }\n\nexport interface StoryStep {\n chap: string\n kind: StoryStepKind\n /** HH:MM:SS (UTC) when the source message carried a `ts`, else \"\". */\n ts: string\n sum: string\n raw1: string\n count: number\n facts: string[]\n items: StoryItem[]\n route?: RouteVerdict\n}\n\nexport interface Story {\n chapters: StoryChapter[]\n steps: StoryStep[]\n}\n\n// ── formatting helpers ───────────────────────────────────────────────────\n\nfunction formatTs(ts?: number): string {\n if (ts === undefined || Number.isNaN(ts)) return \"\"\n return new Date(ts).toISOString().slice(11, 19)\n}\n\nfunction firstMeaningfulLine(text?: string): string | undefined {\n if (!text) return undefined\n const line = text\n .split(\"\\n\")\n .map(l => l.trim())\n .find(l => l.length > 0)\n return line\n}\n\nfunction truncate(text: string, max: number): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text\n}\n\nfunction parseArgs(argsJson: string): unknown {\n try {\n return JSON.parse(argsJson)\n } catch {\n return {}\n }\n}\n\nfunction lineCount(text: string): number {\n return text.split(\"\\n\").filter(l => l.trim().length > 0).length || 1\n}\n\n// ── kind classification ──────────────────────────────────────────────────\n\n/** Dominant kind for a group of tool calls: edit/write → edit, bash/terminal/\n * command → bash, read/grep/glob → read, else text (pure assistant text). */\nexport function classifyKind(toolCalls?: readonly { name: string }[]): StoryStepKind {\n if (!toolCalls || toolCalls.length === 0) return \"text\"\n const names = toolCalls.map(t => t.name.toLowerCase())\n if (names.some(n => /edit|write/.test(n))) return \"edit\"\n if (names.some(n => /bash|terminal|command/.test(n))) return \"bash\"\n if (names.some(n => /read|grep|glob/.test(n))) return \"read\"\n return \"text\"\n}\n\n// ── chapter routing (mockup's `classify()`, ported verbatim) ────────────\n\nconst NEW_CHAPTER_RE =\n /\\b(aussi|autre|ensuite|nouveau|nouvelle|plut[oô]t|maintenant|apr[eè]s ça|il faudrait|peux[- ]tu|on pourrait|ajoute|g[eè]re)\\b/iu\n\nexport function classifyRoute(text: string): { route: RouteVerdict; title?: string } {\n const newt = NEW_CHAPTER_RE.test(text)\n if (!newt) return { route: \"cont\" }\n const title = text.replace(/[.?!].*$/, \"\").slice(0, 42)\n return { route: \"newt\", title }\n}\n\n// ── folding: raw messages → steps (chapter unassigned) ───────────────────\n\ninterface FoldedStep {\n kind: StoryStepKind\n ts: string\n sum: string\n raw1: string\n count: number\n facts: string[]\n items: StoryItem[]\n /** Present only for user steps — drives chapter routing. */\n userText?: string\n}\n\nfunction foldToolStep(assistant: ExportedMessage, toolResults: ExportedMessage[]): FoldedStep {\n const toolCalls = assistant.toolCalls ?? []\n const kind = classifyKind(toolCalls)\n const count = toolCalls.length || 1\n\n const items: StoryItem[] = []\n const facts: string[] = []\n if (assistant.text?.trim()) items.push({ text: assistant.text.trim() })\n toolCalls.forEach((tc, i) => {\n const args = parseArgs(tc.args)\n const h = formatToolCall(tc.name, args)\n const resultMsg = toolResults[i]\n const resultText = resultMsg?.text ?? \"\"\n const isError = resultText.startsWith(\"[error]\")\n const r = isError ? resultText.slice(\"[error]\".length).trim() : resultText\n items.push({ h, r })\n const fact = formatToolResult(tc.name, r, isError)\n if (fact) facts.push(fact)\n })\n\n const firstLine = firstMeaningfulLine(assistant.text)\n const firstToolCall = toolCalls[0]\n const sum =\n firstLine ??\n (firstToolCall ? formatToolCall(firstToolCall.name, parseArgs(firstToolCall.args)) : \"…\")\n\n let raw1: string\n if (toolCalls.length === 0) {\n raw1 = `assistant · ${lineCount(assistant.text ?? \"\")} ligne(s)`\n } else if (toolCalls.length === 1 && firstToolCall) {\n raw1 = formatToolCall(firstToolCall.name, parseArgs(firstToolCall.args))\n } else {\n raw1 = `${firstToolCall?.name ?? \"tool\"} ×${toolCalls.length}`\n }\n\n return {\n kind,\n ts: formatTs(assistant.ts),\n sum,\n raw1,\n count,\n facts,\n items,\n }\n}\n\nfunction foldUserStep(msg: ExportedMessage): FoldedStep {\n const text = msg.text ?? \"\"\n const sum = `« ${truncate(text, 80)} »`\n return {\n kind: \"user\",\n ts: formatTs(msg.ts),\n sum,\n raw1: `user · ${lineCount(text)} ligne(s)`,\n count: 1,\n facts: [],\n items: [{ text }],\n userText: text,\n }\n}\n\nfunction foldOrphanToolStep(msg: ExportedMessage): FoldedStep {\n const text = msg.text ?? \"\"\n const isError = text.startsWith(\"[error]\")\n const r = isError ? text.slice(\"[error]\".length).trim() : text\n const name = msg.toolName ?? \"tool\"\n const fact = formatToolResult(name, r, isError)\n return {\n kind: classifyKind([{ name }]),\n ts: formatTs(msg.ts),\n sum: msg.toolName ? `${msg.toolName} · résultat` : \"Résultat d'outil\",\n raw1: msg.toolName ?? \"tool\",\n count: 1,\n facts: fact ? [fact] : [],\n items: [{ h: name, r }],\n }\n}\n\nfunction foldSystemStep(msg: ExportedMessage): FoldedStep {\n const text = msg.text ?? \"\"\n return {\n kind: \"text\",\n ts: formatTs(msg.ts),\n sum: firstMeaningfulLine(text) ?? text,\n raw1: \"system\",\n count: 1,\n facts: [],\n items: text ? [{ text }] : [],\n }\n}\n\nfunction foldMessages(messages: readonly ExportedMessage[]): FoldedStep[] {\n const steps: FoldedStep[] = []\n let i = 0\n while (i < messages.length) {\n const msg = messages[i]!\n if (msg.role === \"user\") {\n steps.push(foldUserStep(msg))\n i += 1\n continue\n }\n if (msg.role === \"assistant\") {\n let j = i + 1\n const toolResults: ExportedMessage[] = []\n while (j < messages.length && messages[j]!.role === \"tool\") {\n toolResults.push(messages[j]!)\n j += 1\n }\n steps.push(foldToolStep(msg, toolResults))\n i = j\n continue\n }\n if (msg.role === \"tool\") {\n // Orphan tool result with no preceding assistant message — rare, but\n // render it as its own read-only step rather than dropping data.\n steps.push(foldOrphanToolStep(msg))\n i += 1\n continue\n }\n // system\n steps.push(foldSystemStep(msg))\n i += 1\n }\n return steps\n}\n\n// ── chaptering: assign each folded step to a chapter ─────────────────────\n\nexport function buildStory(messages: readonly ExportedMessage[]): Story {\n const folded = foldMessages(messages)\n const chapters: StoryChapter[] = []\n const steps: StoryStep[] = []\n\n let currentChapterId: string | undefined\n let sawFirstUser = false\n\n const closeCurrent = (): void => {\n const cur = chapters.find(c => c.id === currentChapterId)\n if (cur) cur.status = \"done\"\n }\n const openChapter = (title: string): string => {\n const id = `c${chapters.length + 1}`\n chapters.push({ id, title, status: \"cur\" })\n return id\n }\n\n for (const step of folded) {\n let route: RouteVerdict | undefined\n if (step.kind === \"user\" && step.userText !== undefined) {\n if (!sawFirstUser) {\n sawFirstUser = true\n currentChapterId = openChapter(\"Cadrage\")\n } else {\n const verdict = classifyRoute(step.userText)\n route = verdict.route\n if (verdict.route === \"newt\") {\n closeCurrent()\n currentChapterId = openChapter(verdict.title || \"Nouvelle sous-tâche\")\n }\n }\n } else if (currentChapterId === undefined) {\n // Transcript opens with assistant/system output before any user\n // message (unusual, but keep every step chaptered).\n currentChapterId = openChapter(\"Cadrage\")\n }\n\n steps.push({\n chap: currentChapterId!,\n kind: step.kind,\n ts: step.ts,\n sum: step.sum,\n raw1: step.raw1,\n count: step.count,\n facts: step.facts,\n items: step.items,\n ...(route ? { route } : {}),\n })\n }\n\n return { chapters, steps }\n}\n"]}