@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1

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 (246) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
  3. package/dist/cli.js +10009 -9654
  4. package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
  5. package/dist/types/advisor/runtime.d.ts +8 -2
  6. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  7. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  8. package/dist/types/cli/args.d.ts +2 -0
  9. package/dist/types/cli/session-picker.d.ts +14 -7
  10. package/dist/types/collab/guest.d.ts +21 -2
  11. package/dist/types/commands/launch.d.ts +6 -0
  12. package/dist/types/config/model-discovery.d.ts +12 -0
  13. package/dist/types/config/settings-schema.d.ts +73 -8
  14. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  15. package/dist/types/cursor.d.ts +193 -8
  16. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  17. package/dist/types/edit/hashline/diff.d.ts +9 -1
  18. package/dist/types/edit/index.d.ts +8 -1
  19. package/dist/types/edit/streaming.d.ts +8 -1
  20. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  21. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
  23. package/dist/types/internal-urls/index.d.ts +1 -0
  24. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  25. package/dist/types/internal-urls/security-protocol.d.ts +15 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  28. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  29. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  30. package/dist/types/lsp/client.d.ts +2 -0
  31. package/dist/types/main.d.ts +2 -0
  32. package/dist/types/mcp/manager.d.ts +33 -4
  33. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  35. package/dist/types/modes/components/session-selector.d.ts +6 -0
  36. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  38. package/dist/types/modes/components/usage-row.d.ts +2 -0
  39. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  40. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  41. package/dist/types/modes/interactive-mode.d.ts +4 -2
  42. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  44. package/dist/types/modes/theme/theme.d.ts +12 -1
  45. package/dist/types/modes/types.d.ts +2 -1
  46. package/dist/types/sdk.d.ts +18 -0
  47. package/dist/types/security/auth.d.ts +30 -0
  48. package/dist/types/security/cloud.d.ts +79 -0
  49. package/dist/types/security/comparison.d.ts +49 -0
  50. package/dist/types/security/contracts/ids.d.ts +15 -0
  51. package/dist/types/security/contracts/index.d.ts +4 -0
  52. package/dist/types/security/contracts/schemas.d.ts +660 -0
  53. package/dist/types/security/contracts/types.d.ts +234 -0
  54. package/dist/types/security/contracts/validation.d.ts +5 -0
  55. package/dist/types/security/coordinator.d.ts +100 -0
  56. package/dist/types/security/importers/codex-security.d.ts +7 -0
  57. package/dist/types/security/importers/index.d.ts +2 -0
  58. package/dist/types/security/importers/sarif.d.ts +9 -0
  59. package/dist/types/security/index.d.ts +13 -0
  60. package/dist/types/security/preflight.d.ts +61 -0
  61. package/dist/types/security/provenance.d.ts +24 -0
  62. package/dist/types/security/publication.d.ts +78 -0
  63. package/dist/types/security/remediation.d.ts +27 -0
  64. package/dist/types/security/resource-output.d.ts +8 -0
  65. package/dist/types/security/sarif.d.ts +2 -0
  66. package/dist/types/security/store.d.ts +40 -0
  67. package/dist/types/session/agent-session-events.d.ts +2 -0
  68. package/dist/types/session/agent-session-types.d.ts +36 -1
  69. package/dist/types/session/agent-session.d.ts +4 -2
  70. package/dist/types/session/claude-session-store.d.ts +13 -0
  71. package/dist/types/session/codex-session-store.d.ts +14 -0
  72. package/dist/types/session/credential-pin.d.ts +58 -0
  73. package/dist/types/session/foreign-session-import.d.ts +15 -0
  74. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  75. package/dist/types/session/foreign-session-store.d.ts +23 -0
  76. package/dist/types/session/session-advisors.d.ts +47 -2
  77. package/dist/types/session/session-entries.d.ts +19 -1
  78. package/dist/types/session/session-handoff.d.ts +4 -1
  79. package/dist/types/session/session-loader.d.ts +2 -0
  80. package/dist/types/session/session-maintenance.d.ts +1 -1
  81. package/dist/types/session/session-manager.d.ts +21 -0
  82. package/dist/types/session/session-tools.d.ts +25 -1
  83. package/dist/types/session/turn-recovery.d.ts +10 -4
  84. package/dist/types/slash-commands/helpers/security.d.ts +2 -0
  85. package/dist/types/system-prompt.d.ts +4 -1
  86. package/dist/types/task/executor.d.ts +14 -2
  87. package/dist/types/task/render.d.ts +7 -0
  88. package/dist/types/thinking.d.ts +18 -6
  89. package/dist/types/tools/acp-bridge.d.ts +28 -5
  90. package/dist/types/tools/builtin-names.d.ts +1 -1
  91. package/dist/types/tools/grep.d.ts +17 -1
  92. package/dist/types/tools/hub/launch.d.ts +5 -1
  93. package/dist/types/tools/index.d.ts +10 -2
  94. package/dist/types/tools/path-utils.d.ts +19 -0
  95. package/dist/types/tools/read.d.ts +2 -0
  96. package/dist/types/tools/security-scan.d.ts +96 -0
  97. package/dist/types/tools/xdev.d.ts +2 -1
  98. package/dist/types/utils/changelog.d.ts +19 -0
  99. package/dist/types/utils/git.d.ts +10 -0
  100. package/dist/types/utils/jj.d.ts +11 -4
  101. package/dist/types/web/search/types.d.ts +2 -2
  102. package/package.json +12 -12
  103. package/scripts/security-compare.ts +40 -0
  104. package/src/advisor/runtime.ts +138 -12
  105. package/src/advisor/transcript-recorder.ts +56 -0
  106. package/src/auto-thinking/classifier.ts +48 -10
  107. package/src/cli/args.ts +6 -0
  108. package/src/cli/flag-tables.ts +2 -0
  109. package/src/cli/gallery-fixtures/fs.ts +25 -0
  110. package/src/cli/session-picker.ts +34 -17
  111. package/src/cli/ttsr-cli.ts +19 -1
  112. package/src/cli.ts +9 -0
  113. package/src/collab/guest.ts +37 -4
  114. package/src/collab/host.ts +1 -0
  115. package/src/commands/launch.ts +6 -0
  116. package/src/config/model-discovery.ts +32 -5
  117. package/src/config/model-registry.ts +107 -25
  118. package/src/config/settings-schema.ts +77 -6
  119. package/src/config/settings.ts +25 -0
  120. package/src/cursor-bridge-tools.ts +81 -0
  121. package/src/cursor.ts +463 -12
  122. package/src/edit/edit-clipboard.ts +23 -0
  123. package/src/edit/hashline/diff.ts +49 -10
  124. package/src/edit/hashline/execute.ts +38 -6
  125. package/src/edit/hashline/filesystem.ts +27 -3
  126. package/src/edit/index.ts +12 -2
  127. package/src/edit/renderer.ts +4 -4
  128. package/src/edit/streaming.ts +17 -2
  129. package/src/eval/py/prelude.py +7 -3
  130. package/src/export/html/template.js +1 -1
  131. package/src/extensibility/extensions/runner.ts +70 -0
  132. package/src/extensibility/extensions/types.ts +30 -0
  133. package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
  134. package/src/internal-urls/index.ts +1 -0
  135. package/src/internal-urls/local-protocol.ts +3 -2
  136. package/src/internal-urls/mcp-protocol.ts +4 -1
  137. package/src/internal-urls/router.ts +4 -1
  138. package/src/internal-urls/security-protocol.ts +261 -0
  139. package/src/internal-urls/types.ts +1 -1
  140. package/src/launch/terminal-output-worker-client.ts +53 -0
  141. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  142. package/src/launch/terminal-output-worker.ts +23 -0
  143. package/src/lsp/client.ts +18 -3
  144. package/src/lsp/index.ts +7 -0
  145. package/src/lsp/render.ts +1 -1
  146. package/src/main.ts +127 -56
  147. package/src/mcp/manager.ts +120 -18
  148. package/src/modes/acp/acp-agent.ts +15 -9
  149. package/src/modes/components/chat-transcript-builder.ts +30 -11
  150. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  151. package/src/modes/components/read-tool-group.ts +159 -17
  152. package/src/modes/components/session-selector.ts +19 -3
  153. package/src/modes/components/status-line/component.ts +381 -54
  154. package/src/modes/components/tool-execution.ts +78 -31
  155. package/src/modes/components/usage-row.ts +10 -5
  156. package/src/modes/controllers/event-controller.ts +26 -4
  157. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  158. package/src/modes/controllers/selector-controller.ts +94 -36
  159. package/src/modes/controllers/tan-command-controller.ts +14 -0
  160. package/src/modes/interactive-mode.ts +31 -17
  161. package/src/modes/rpc/host-uris.ts +6 -0
  162. package/src/modes/rpc/rpc-client.ts +18 -0
  163. package/src/modes/rpc/rpc-mode.ts +16 -1
  164. package/src/modes/rpc/rpc-types.ts +11 -0
  165. package/src/modes/theme/theme.ts +151 -6
  166. package/src/modes/types.ts +2 -1
  167. package/src/modes/utils/ui-helpers.ts +38 -21
  168. package/src/prompts/agents/security-reviewer.md +75 -0
  169. package/src/prompts/security/scan-coordinator.md +7 -0
  170. package/src/prompts/security/scan-request.md +21 -0
  171. package/src/prompts/security/validate-request.md +8 -0
  172. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  173. package/src/prompts/system/plan-mode-active.md +2 -2
  174. package/src/prompts/system/system-prompt.md +6 -0
  175. package/src/prompts/system/xdev-mount-notice.md +1 -1
  176. package/src/prompts/tools/bash.md +15 -17
  177. package/src/prompts/tools/checkpoint.md +1 -1
  178. package/src/prompts/tools/glob.md +7 -6
  179. package/src/prompts/tools/grep.md +7 -6
  180. package/src/prompts/tools/security-publish.md +1 -0
  181. package/src/prompts/tools/security-scan.md +1 -0
  182. package/src/sdk.ts +212 -21
  183. package/src/security/auth.ts +98 -0
  184. package/src/security/cloud.ts +686 -0
  185. package/src/security/comparison.ts +255 -0
  186. package/src/security/contracts/ids.ts +111 -0
  187. package/src/security/contracts/index.ts +4 -0
  188. package/src/security/contracts/schemas.ts +201 -0
  189. package/src/security/contracts/types.ts +254 -0
  190. package/src/security/contracts/validation.ts +65 -0
  191. package/src/security/coordinator.ts +708 -0
  192. package/src/security/importers/codex-security.ts +387 -0
  193. package/src/security/importers/index.ts +2 -0
  194. package/src/security/importers/sarif.ts +357 -0
  195. package/src/security/index.ts +13 -0
  196. package/src/security/preflight.ts +405 -0
  197. package/src/security/provenance.ts +106 -0
  198. package/src/security/publication.ts +326 -0
  199. package/src/security/remediation.ts +93 -0
  200. package/src/security/resource-output.ts +50 -0
  201. package/src/security/sarif.ts +78 -0
  202. package/src/security/store.ts +430 -0
  203. package/src/session/agent-session-events.ts +1 -0
  204. package/src/session/agent-session-types.ts +40 -1
  205. package/src/session/agent-session.ts +302 -175
  206. package/src/session/claude-session-store.ts +426 -0
  207. package/src/session/codex-session-store.ts +673 -0
  208. package/src/session/credential-pin.ts +93 -0
  209. package/src/session/foreign-session-import.ts +52 -0
  210. package/src/session/foreign-session-jsonl.ts +29 -0
  211. package/src/session/foreign-session-store.ts +26 -0
  212. package/src/session/model-controls.ts +11 -3
  213. package/src/session/session-advisors.ts +140 -16
  214. package/src/session/session-entries.ts +21 -1
  215. package/src/session/session-handoff.ts +20 -3
  216. package/src/session/session-loader.ts +25 -9
  217. package/src/session/session-maintenance.ts +59 -11
  218. package/src/session/session-manager.ts +61 -0
  219. package/src/session/session-tools.ts +107 -5
  220. package/src/session/settings-stream-fn.ts +1 -0
  221. package/src/session/turn-recovery.ts +97 -51
  222. package/src/slash-commands/builtin-registry.ts +152 -3
  223. package/src/slash-commands/helpers/security.ts +451 -0
  224. package/src/system-prompt.ts +7 -2
  225. package/src/task/agents.ts +2 -0
  226. package/src/task/executor.ts +20 -4
  227. package/src/task/render.ts +20 -4
  228. package/src/task/structured-subagent.ts +6 -4
  229. package/src/thinking.ts +29 -10
  230. package/src/tools/acp-bridge.ts +52 -8
  231. package/src/tools/browser.ts +5 -0
  232. package/src/tools/builtin-names.ts +1 -0
  233. package/src/tools/checkpoint.ts +0 -13
  234. package/src/tools/grep.ts +64 -8
  235. package/src/tools/hub/launch.ts +10 -15
  236. package/src/tools/index.ts +37 -5
  237. package/src/tools/path-utils.ts +91 -0
  238. package/src/tools/read.ts +3 -0
  239. package/src/tools/security-scan.ts +287 -0
  240. package/src/tools/write.ts +9 -3
  241. package/src/tools/xdev.ts +39 -14
  242. package/src/utils/changelog.ts +144 -4
  243. package/src/utils/git.ts +61 -14
  244. package/src/utils/jj.ts +19 -13
  245. package/src/web/search/providers/codex.ts +34 -0
  246. package/src/web/search/types.ts +1 -1
@@ -0,0 +1,426 @@
1
+ import type * as fsTypes from "node:fs";
2
+ import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import type {
6
+ AssistantMessage,
7
+ ImageContent,
8
+ TextContent,
9
+ ThinkingContent,
10
+ ToolCall,
11
+ ToolResultMessage,
12
+ Usage,
13
+ UserMessage,
14
+ } from "@oh-my-pi/pi-ai";
15
+ import { isRecord } from "@oh-my-pi/pi-utils";
16
+ import { collectForeignJsonRecords, type ForeignJsonRecord, readForeignJsonRecords } from "./foreign-session-jsonl";
17
+ import type { ForeignSessionInfo, ForeignSessionStore } from "./foreign-session-store";
18
+ import type { ModelChangeEntry, SessionMessageEntry } from "./session-entries";
19
+ import { SessionManager } from "./session-manager";
20
+
21
+ interface ClaudeHistoryMetadata {
22
+ firstMessage?: string;
23
+ cwd?: string;
24
+ created: number;
25
+ modified: number;
26
+ messageCount: number;
27
+ }
28
+
29
+ interface ConvertedMessage {
30
+ readonly message: UserMessage | AssistantMessage | ToolResultMessage;
31
+ readonly suffix: string;
32
+ }
33
+
34
+ const EMPTY_USAGE: Usage = {
35
+ input: 0,
36
+ output: 0,
37
+ cacheRead: 0,
38
+ cacheWrite: 0,
39
+ totalTokens: 0,
40
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
41
+ };
42
+
43
+ function stringField(record: Record<string, unknown>, key: string): string | undefined {
44
+ const value = record[key];
45
+ return typeof value === "string" && value.length > 0 ? value : undefined;
46
+ }
47
+
48
+ function numberField(record: Record<string, unknown>, key: string): number {
49
+ const value = record[key];
50
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
51
+ }
52
+
53
+ function timestampMs(value: unknown, fallback: number): number {
54
+ if (typeof value === "number" && Number.isFinite(value)) return value;
55
+ if (typeof value === "string") {
56
+ const parsed = Date.parse(value);
57
+ if (Number.isFinite(parsed)) return parsed;
58
+ }
59
+ return fallback;
60
+ }
61
+
62
+ function isoTimestamp(value: unknown, fallback: number): string {
63
+ return new Date(timestampMs(value, fallback)).toISOString();
64
+ }
65
+
66
+ function cleanPreview(value: string): string | undefined {
67
+ const cleaned = value.replace(/\s+/g, " ").trim();
68
+ return cleaned.length > 0 ? cleaned : undefined;
69
+ }
70
+
71
+ async function readHistoryIndex(file: string): Promise<Map<string, ClaudeHistoryMetadata>> {
72
+ const metadata = new Map<string, ClaudeHistoryMetadata>();
73
+ try {
74
+ for await (const { value } of readForeignJsonRecords(file)) {
75
+ const id = stringField(value, "sessionId") ?? stringField(value, "session_id");
76
+ const timestamp = timestampMs(value.timestamp ?? value.ts, 0);
77
+ if (!id || timestamp <= 0) continue;
78
+ const previous = metadata.get(id);
79
+ const rawText = stringField(value, "display") ?? stringField(value, "text");
80
+ const text = rawText ? cleanPreview(rawText) : undefined;
81
+ const cwd = stringField(value, "project");
82
+ if (!previous) {
83
+ metadata.set(id, { created: timestamp, modified: timestamp, firstMessage: text, cwd, messageCount: 1 });
84
+ continue;
85
+ }
86
+ if (timestamp < previous.created) {
87
+ previous.created = timestamp;
88
+ if (text) previous.firstMessage = text;
89
+ }
90
+ previous.modified = Math.max(previous.modified, timestamp);
91
+ if (!previous.firstMessage && text) previous.firstMessage = text;
92
+ previous.messageCount += 1;
93
+ if (!previous.cwd && cwd) previous.cwd = cwd;
94
+ }
95
+ } catch {
96
+ // History is an optional index; project files remain independently discoverable.
97
+ }
98
+ return metadata;
99
+ }
100
+
101
+ async function readRegisteredProjects(root: string): Promise<string[]> {
102
+ const config = path.join(path.dirname(root), ".claude.json");
103
+ try {
104
+ const parsed: unknown = await Bun.file(config).json();
105
+ if (!isRecord(parsed) || !isRecord(parsed.projects)) return [];
106
+ const projects: string[] = [];
107
+ for (const project in parsed.projects) {
108
+ if (path.isAbsolute(project)) projects.push(project);
109
+ }
110
+ return projects;
111
+ } catch {
112
+ return [];
113
+ }
114
+ }
115
+
116
+ function projectCwd(encoded: string, registered: readonly string[]): string {
117
+ const exact = registered.find(project => project.replaceAll(path.sep, "-") === encoded);
118
+ if (exact) return exact;
119
+ if (!encoded.startsWith("-")) return encoded;
120
+ return encoded.replaceAll("-", path.sep);
121
+ }
122
+
123
+ async function projectFiles(root: string): Promise<Array<{ file: string; cwd: string }>> {
124
+ const registered = await readRegisteredProjects(root);
125
+ const found: Array<{ file: string; cwd: string }> = [];
126
+ for (const containerName of ["projects", ".projects"]) {
127
+ const container = path.join(root, containerName);
128
+ const projects = await fs.readdir(container, { withFileTypes: true }).catch(() => []);
129
+ for (const project of projects) {
130
+ if (!project.isDirectory()) continue;
131
+ const directory = path.join(container, project.name);
132
+ const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
133
+ const cwd = projectCwd(project.name, registered);
134
+ for (const entry of entries) {
135
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) {
136
+ found.push({ file: path.join(directory, entry.name), cwd });
137
+ }
138
+ }
139
+ }
140
+ }
141
+ return found;
142
+ }
143
+
144
+ function imageContent(value: unknown): ImageContent | undefined {
145
+ if (!isRecord(value) || value.type !== "image" || !isRecord(value.source)) return undefined;
146
+ const data = stringField(value.source, "data");
147
+ const mimeType = stringField(value.source, "media_type");
148
+ return data && mimeType ? { type: "image", data, mimeType } : undefined;
149
+ }
150
+
151
+ function userContent(value: unknown): string | (TextContent | ImageContent)[] | undefined {
152
+ if (typeof value === "string") return value;
153
+ if (!Array.isArray(value)) return undefined;
154
+ const content: (TextContent | ImageContent)[] = [];
155
+ for (const block of value) {
156
+ if (!isRecord(block)) continue;
157
+ if (block.type === "text" && typeof block.text === "string") content.push({ type: "text", text: block.text });
158
+ else {
159
+ const image = imageContent(block);
160
+ if (image) content.push(image);
161
+ }
162
+ }
163
+ return content.length > 0 ? content : undefined;
164
+ }
165
+
166
+ function toolResultContent(value: unknown): (TextContent | ImageContent)[] {
167
+ if (typeof value === "string") return [{ type: "text", text: value }];
168
+ if (!Array.isArray(value)) return [];
169
+ const content: (TextContent | ImageContent)[] = [];
170
+ for (const block of value) {
171
+ if (!isRecord(block)) continue;
172
+ if (block.type === "text" && typeof block.text === "string") content.push({ type: "text", text: block.text });
173
+ else {
174
+ const image = imageContent(block);
175
+ if (image) content.push(image);
176
+ }
177
+ }
178
+ return content;
179
+ }
180
+
181
+ function assistantContent(
182
+ value: unknown,
183
+ toolNames: Map<string, string>,
184
+ ): (TextContent | ThinkingContent | ToolCall)[] {
185
+ if (!Array.isArray(value)) return [];
186
+ const content: (TextContent | ThinkingContent | ToolCall)[] = [];
187
+ for (const block of value) {
188
+ if (!isRecord(block)) continue;
189
+ if (block.type === "text" && typeof block.text === "string") {
190
+ content.push({ type: "text", text: block.text });
191
+ } else if (block.type === "thinking" && typeof block.thinking === "string") {
192
+ const thinking: ThinkingContent = { type: "thinking", thinking: block.thinking };
193
+ if (typeof block.signature === "string") thinking.thinkingSignature = block.signature;
194
+ content.push(thinking);
195
+ } else if (block.type === "tool_use") {
196
+ const id = stringField(block, "id");
197
+ const name = stringField(block, "name");
198
+ if (!id || !name) continue;
199
+ const argumentsValue = isRecord(block.input) ? block.input : {};
200
+ content.push({ type: "toolCall", id, name, arguments: argumentsValue });
201
+ toolNames.set(id, name);
202
+ }
203
+ }
204
+ return content;
205
+ }
206
+
207
+ function claudeUsage(value: unknown): Usage {
208
+ if (!isRecord(value)) return EMPTY_USAGE;
209
+ const input = numberField(value, "input_tokens");
210
+ const output = numberField(value, "output_tokens");
211
+ const cacheRead = numberField(value, "cache_read_input_tokens");
212
+ const cacheWrite = numberField(value, "cache_creation_input_tokens");
213
+ return {
214
+ input,
215
+ output,
216
+ cacheRead,
217
+ cacheWrite,
218
+ totalTokens: input + output + cacheRead + cacheWrite,
219
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
220
+ };
221
+ }
222
+
223
+ function stopReason(value: unknown): AssistantMessage["stopReason"] {
224
+ if (value === "tool_use") return "toolUse";
225
+ if (value === "max_tokens") return "length";
226
+ if (value === "end_turn" || value === "stop_sequence" || value === "pause_turn") return "stop";
227
+ return value == null ? "stop" : "error";
228
+ }
229
+
230
+ function convertRecord(
231
+ record: Record<string, unknown>,
232
+ fallbackTimestamp: number,
233
+ toolNames: Map<string, string>,
234
+ ): ConvertedMessage[] {
235
+ if (
236
+ (record.type !== "user" && record.type !== "assistant") ||
237
+ record.isSidechain === true ||
238
+ record.isMeta === true
239
+ ) {
240
+ return [];
241
+ }
242
+ if (!isRecord(record.message)) return [];
243
+ const timestamp = timestampMs(record.timestamp, fallbackTimestamp);
244
+ if (record.type === "assistant") {
245
+ const content = assistantContent(record.message.content, toolNames);
246
+ const errorMessage = stringField(record, "error");
247
+ if (content.length === 0 && !errorMessage) return [];
248
+ const model = stringField(record.message, "model") ?? "unknown";
249
+ const message: AssistantMessage = {
250
+ role: "assistant",
251
+ content,
252
+ api: "anthropic-messages",
253
+ provider: "anthropic",
254
+ model,
255
+ usage: claudeUsage(record.message.usage),
256
+ stopReason: stopReason(record.message.stop_reason),
257
+ timestamp,
258
+ };
259
+ const responseId = stringField(record.message, "id");
260
+ if (responseId) message.responseId = responseId;
261
+ if (errorMessage) message.errorMessage = errorMessage;
262
+ if (typeof record.apiErrorStatus === "number" && Number.isFinite(record.apiErrorStatus)) {
263
+ message.errorStatus = record.apiErrorStatus;
264
+ }
265
+ return [{ message, suffix: "message" }];
266
+ }
267
+ const rawContent = record.message.content;
268
+ if (Array.isArray(rawContent)) {
269
+ const results: ConvertedMessage[] = [];
270
+ for (let index = 0; index < rawContent.length; index += 1) {
271
+ const block = rawContent[index];
272
+ if (!isRecord(block) || block.type !== "tool_result") continue;
273
+ const toolCallId = stringField(block, "tool_use_id");
274
+ if (!toolCallId) continue;
275
+ const message: ToolResultMessage = {
276
+ role: "toolResult",
277
+ toolCallId,
278
+ toolName: toolNames.get(toolCallId) ?? "unknown",
279
+ content: toolResultContent(block.content),
280
+ isError: block.is_error === true,
281
+ timestamp,
282
+ };
283
+ results.push({ message, suffix: `tool-${index}` });
284
+ }
285
+ if (results.length > 0) return results;
286
+ }
287
+ const content = userContent(rawContent);
288
+ if (content === undefined || (typeof content === "string" && content.length === 0)) return [];
289
+ return [{ message: { role: "user", content, timestamp }, suffix: "message" }];
290
+ }
291
+
292
+ function uniqueEntryId(base: string, used: Set<string>): string {
293
+ let id = base;
294
+ let suffix = 2;
295
+ while (used.has(id)) {
296
+ id = `${base}-${suffix}`;
297
+ suffix += 1;
298
+ }
299
+ used.add(id);
300
+ return id;
301
+ }
302
+
303
+ /** Imports Claude Code JSONL sessions into non-persistent OMP session managers. */
304
+ export class ClaudeSessionStore implements ForeignSessionStore {
305
+ readonly source = "claude";
306
+ readonly #root: string;
307
+
308
+ /** Creates a store rooted at Claude's data directory, or at a fixture root when supplied. */
309
+ constructor(root: string = path.join(os.homedir(), ".claude")) {
310
+ this.#root = path.resolve(root);
311
+ }
312
+
313
+ /** Lists indexed Claude sessions without reading transcript bodies. */
314
+ async list(): Promise<ForeignSessionInfo[]> {
315
+ const [history, files] = await Promise.all([
316
+ readHistoryIndex(path.join(this.#root, "history.jsonl")),
317
+ projectFiles(this.#root),
318
+ ]);
319
+ const sessions: ForeignSessionInfo[] = [];
320
+ for (const item of files) {
321
+ try {
322
+ const stats = await fs.stat(item.file);
323
+ const id = path.basename(item.file, ".jsonl");
324
+ const indexed = history.get(id);
325
+ const createdMs = indexed?.created ?? (stats.birthtimeMs || stats.ctimeMs || stats.mtimeMs);
326
+ const modifiedMs = Math.max(indexed?.modified ?? 0, stats.mtimeMs);
327
+ sessions.push({
328
+ source: this.source,
329
+ id,
330
+ path: item.file,
331
+ cwd: indexed?.cwd ?? item.cwd,
332
+ created: new Date(createdMs),
333
+ modified: new Date(modifiedMs),
334
+ firstMessage: indexed?.firstMessage,
335
+ messageCount: indexed?.messageCount,
336
+ });
337
+ } catch {
338
+ // Files may disappear while Claude rotates its session store.
339
+ }
340
+ }
341
+ return sessions.sort(
342
+ (left, right) => right.modified.getTime() - left.modified.getTime() || left.path.localeCompare(right.path),
343
+ );
344
+ }
345
+
346
+ /** Loads and converts a Claude transcript while preserving its source tree and timestamps. */
347
+ async load(info: ForeignSessionInfo): Promise<SessionManager> {
348
+ if (info.source !== this.source) throw new Error(`Cannot load ${info.source} session with ClaudeSessionStore`);
349
+ let records: ForeignJsonRecord[];
350
+ let stats: fsTypes.Stats;
351
+ try {
352
+ [records, stats] = await Promise.all([collectForeignJsonRecords(info.path), fs.stat(info.path)]);
353
+ } catch (error) {
354
+ const detail = error instanceof Error ? error.message : String(error);
355
+ throw new Error(`Unable to read Claude session ${info.id}: ${detail}`);
356
+ }
357
+ if (records.length === 0 && stats.size > 0)
358
+ throw new Error(`Claude session ${info.id} contains no readable records`);
359
+
360
+ const sourceParents = new Map<string, string | null>();
361
+ let sourceCwd: string | undefined;
362
+ let sourceTitle: string | undefined;
363
+ let aiTitle: string | undefined;
364
+ for (const { value } of records) {
365
+ const uuid = stringField(value, "uuid");
366
+ if (uuid) sourceParents.set(uuid, stringField(value, "parentUuid") ?? null);
367
+ if (!sourceCwd) sourceCwd = stringField(value, "cwd");
368
+ if (value.type === "custom-title") sourceTitle = stringField(value, "customTitle") ?? sourceTitle;
369
+ if (value.type === "ai-title") aiTitle = stringField(value, "aiTitle") ?? aiTitle;
370
+ }
371
+
372
+ const manager = SessionManager.inMemory(sourceCwd ?? info.cwd);
373
+ const sourceTails = new Map<string, string>();
374
+ const usedIds = new Set<string>();
375
+ const toolNames = new Map<string, string>();
376
+ let lastModel: string | undefined;
377
+ let synthetic = 0;
378
+ const resolveParent = (sourceId: string | undefined): string | null => {
379
+ const seen = new Set<string>();
380
+ let cursor = sourceId;
381
+ while (cursor && !seen.has(cursor)) {
382
+ seen.add(cursor);
383
+ const retained = sourceTails.get(cursor);
384
+ if (retained) return retained;
385
+ cursor = sourceParents.get(cursor) ?? undefined;
386
+ }
387
+ return null;
388
+ };
389
+
390
+ for (const { value, line } of records) {
391
+ const converted = convertRecord(value, stats.mtimeMs, toolNames);
392
+ if (converted.length === 0) continue;
393
+ const sourceUuid = stringField(value, "uuid") ?? `line-${line}`;
394
+ let parentId = resolveParent(stringField(value, "parentUuid"));
395
+ const timestamp = isoTimestamp(value.timestamp, stats.mtimeMs);
396
+ if (value.type === "assistant" && isRecord(value.message)) {
397
+ const model = stringField(value.message, "model");
398
+ if (model && model !== lastModel) {
399
+ const id = uniqueEntryId(`claude-${sourceUuid}-model`, usedIds);
400
+ const entry: ModelChangeEntry = {
401
+ type: "model_change",
402
+ id,
403
+ parentId,
404
+ timestamp,
405
+ model: `anthropic/${model}`,
406
+ };
407
+ manager.ingestReplicatedEntry(entry);
408
+ parentId = id;
409
+ lastModel = model;
410
+ }
411
+ }
412
+ for (const item of converted) {
413
+ synthetic += 1;
414
+ const id = uniqueEntryId(`claude-${sourceUuid}-${item.suffix}-${synthetic}`, usedIds);
415
+ const entry: SessionMessageEntry = { type: "message", id, parentId, timestamp, message: item.message };
416
+ manager.ingestReplicatedEntry(entry);
417
+ parentId = id;
418
+ }
419
+ if (parentId) sourceTails.set(sourceUuid, parentId);
420
+ }
421
+
422
+ const title = sourceTitle ?? aiTitle ?? info.title;
423
+ if (title) await manager.setSessionName(title, sourceTitle ? "user" : "auto");
424
+ return manager;
425
+ }
426
+ }