@codemieai/code 0.11.0 → 0.13.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.
Files changed (253) hide show
  1. package/README.md +109 -56
  2. package/bin/codemie-copilot.js +224 -0
  3. package/bin/codemie-pi.js +18 -0
  4. package/dist/agents/core/AgentCLI.d.ts.map +1 -1
  5. package/dist/agents/core/AgentCLI.js +16 -10
  6. package/dist/agents/core/AgentCLI.js.map +1 -1
  7. package/dist/agents/core/BaseAgentAdapter.d.ts.map +1 -1
  8. package/dist/agents/core/BaseAgentAdapter.js +43 -19
  9. package/dist/agents/core/BaseAgentAdapter.js.map +1 -1
  10. package/dist/agents/core/agent-aliases.d.ts +17 -0
  11. package/dist/agents/core/agent-aliases.d.ts.map +1 -0
  12. package/dist/agents/core/agent-aliases.js +41 -0
  13. package/dist/agents/core/agent-aliases.js.map +1 -0
  14. package/dist/agents/core/session/BaseSessionAdapter.d.ts +19 -0
  15. package/dist/agents/core/session/BaseSessionAdapter.d.ts.map +1 -1
  16. package/dist/agents/core/session/session-origin-audit.d.ts +13 -0
  17. package/dist/agents/core/session/session-origin-audit.d.ts.map +1 -1
  18. package/dist/agents/core/session/session-origin-audit.js +34 -0
  19. package/dist/agents/core/session/session-origin-audit.js.map +1 -1
  20. package/dist/agents/core/session/stale-session-reconciliation.d.ts +42 -0
  21. package/dist/agents/core/session/stale-session-reconciliation.d.ts.map +1 -0
  22. package/dist/agents/core/session/stale-session-reconciliation.js +141 -0
  23. package/dist/agents/core/session/stale-session-reconciliation.js.map +1 -0
  24. package/dist/agents/core/session/types.d.ts +19 -0
  25. package/dist/agents/core/session/types.d.ts.map +1 -1
  26. package/dist/agents/core/session/types.js +17 -1
  27. package/dist/agents/core/session/types.js.map +1 -1
  28. package/dist/agents/core/types.d.ts +11 -0
  29. package/dist/agents/core/types.d.ts.map +1 -1
  30. package/dist/agents/plugins/codex/codex.reconciliation.d.ts +15 -35
  31. package/dist/agents/plugins/codex/codex.reconciliation.d.ts.map +1 -1
  32. package/dist/agents/plugins/codex/codex.reconciliation.js +8 -139
  33. package/dist/agents/plugins/codex/codex.reconciliation.js.map +1 -1
  34. package/dist/agents/plugins/copilot-cli/copilot-cli-event-types.d.ts +146 -0
  35. package/dist/agents/plugins/copilot-cli/copilot-cli-event-types.d.ts.map +1 -0
  36. package/dist/agents/plugins/copilot-cli/copilot-cli-event-types.js +12 -0
  37. package/dist/agents/plugins/copilot-cli/copilot-cli-event-types.js.map +1 -0
  38. package/dist/agents/plugins/copilot-cli/copilot-cli.constants.d.ts +15 -0
  39. package/dist/agents/plugins/copilot-cli/copilot-cli.constants.d.ts.map +1 -0
  40. package/dist/agents/plugins/copilot-cli/copilot-cli.constants.js +15 -0
  41. package/dist/agents/plugins/copilot-cli/copilot-cli.constants.js.map +1 -0
  42. package/dist/agents/plugins/copilot-cli/copilot-cli.models.d.ts +8 -0
  43. package/dist/agents/plugins/copilot-cli/copilot-cli.models.d.ts.map +1 -0
  44. package/dist/agents/plugins/copilot-cli/copilot-cli.models.js +195 -0
  45. package/dist/agents/plugins/copilot-cli/copilot-cli.models.js.map +1 -0
  46. package/dist/agents/plugins/copilot-cli/copilot-cli.paths.d.ts +11 -0
  47. package/dist/agents/plugins/copilot-cli/copilot-cli.paths.d.ts.map +1 -0
  48. package/dist/agents/plugins/copilot-cli/copilot-cli.paths.js +21 -0
  49. package/dist/agents/plugins/copilot-cli/copilot-cli.paths.js.map +1 -0
  50. package/dist/agents/plugins/copilot-cli/copilot-cli.plugin.d.ts +14 -0
  51. package/dist/agents/plugins/copilot-cli/copilot-cli.plugin.d.ts.map +1 -0
  52. package/dist/agents/plugins/copilot-cli/copilot-cli.plugin.js +263 -0
  53. package/dist/agents/plugins/copilot-cli/copilot-cli.plugin.js.map +1 -0
  54. package/dist/agents/plugins/copilot-cli/copilot-cli.session.d.ts +43 -0
  55. package/dist/agents/plugins/copilot-cli/copilot-cli.session.d.ts.map +1 -0
  56. package/dist/agents/plugins/copilot-cli/copilot-cli.session.js +343 -0
  57. package/dist/agents/plugins/copilot-cli/copilot-cli.session.js.map +1 -0
  58. package/dist/agents/plugins/copilot-cli/copilot-cli.storage-utils.d.ts +10 -0
  59. package/dist/agents/plugins/copilot-cli/copilot-cli.storage-utils.d.ts.map +1 -0
  60. package/dist/agents/plugins/copilot-cli/copilot-cli.storage-utils.js +41 -0
  61. package/dist/agents/plugins/copilot-cli/copilot-cli.storage-utils.js.map +1 -0
  62. package/dist/agents/plugins/copilot-cli/copilot-cli.usage.d.ts +37 -0
  63. package/dist/agents/plugins/copilot-cli/copilot-cli.usage.d.ts.map +1 -0
  64. package/dist/agents/plugins/copilot-cli/copilot-cli.usage.js +96 -0
  65. package/dist/agents/plugins/copilot-cli/copilot-cli.usage.js.map +1 -0
  66. package/dist/agents/plugins/copilot-cli/copilot-cli.workspace.d.ts +17 -0
  67. package/dist/agents/plugins/copilot-cli/copilot-cli.workspace.d.ts.map +1 -0
  68. package/dist/agents/plugins/copilot-cli/copilot-cli.workspace.js +58 -0
  69. package/dist/agents/plugins/copilot-cli/copilot-cli.workspace.js.map +1 -0
  70. package/dist/agents/plugins/copilot-cli/index.d.ts +3 -0
  71. package/dist/agents/plugins/copilot-cli/index.d.ts.map +1 -0
  72. package/dist/agents/plugins/copilot-cli/index.js +3 -0
  73. package/dist/agents/plugins/copilot-cli/index.js.map +1 -0
  74. package/dist/agents/plugins/copilot-cli/session/processors/copilot-cli.conversations-processor.d.ts +18 -0
  75. package/dist/agents/plugins/copilot-cli/session/processors/copilot-cli.conversations-processor.d.ts.map +1 -0
  76. package/dist/agents/plugins/copilot-cli/session/processors/copilot-cli.conversations-processor.js +313 -0
  77. package/dist/agents/plugins/copilot-cli/session/processors/copilot-cli.conversations-processor.js.map +1 -0
  78. package/dist/agents/plugins/copilot-cli/session/processors/copilot-cli.metrics-processor.d.ts +16 -0
  79. package/dist/agents/plugins/copilot-cli/session/processors/copilot-cli.metrics-processor.d.ts.map +1 -0
  80. package/dist/agents/plugins/copilot-cli/session/processors/copilot-cli.metrics-processor.js +22 -0
  81. package/dist/agents/plugins/copilot-cli/session/processors/copilot-cli.metrics-processor.js.map +1 -0
  82. package/dist/agents/plugins/pi/__tests__/home-redirect.d.ts +12 -0
  83. package/dist/agents/plugins/pi/__tests__/home-redirect.d.ts.map +1 -0
  84. package/dist/agents/plugins/pi/__tests__/home-redirect.js +27 -0
  85. package/dist/agents/plugins/pi/__tests__/home-redirect.js.map +1 -0
  86. package/dist/agents/plugins/pi/extension/README.md +68 -0
  87. package/dist/agents/plugins/pi/extension/index.js +161 -0
  88. package/dist/agents/plugins/pi/extension/package.json +8 -0
  89. package/dist/agents/plugins/pi/index.d.ts +3 -0
  90. package/dist/agents/plugins/pi/index.d.ts.map +1 -0
  91. package/dist/agents/plugins/pi/index.js +3 -0
  92. package/dist/agents/plugins/pi/index.js.map +1 -0
  93. package/dist/agents/plugins/pi/pi.extension.d.ts +60 -0
  94. package/dist/agents/plugins/pi/pi.extension.d.ts.map +1 -0
  95. package/dist/agents/plugins/pi/pi.extension.js +201 -0
  96. package/dist/agents/plugins/pi/pi.extension.js.map +1 -0
  97. package/dist/agents/plugins/pi/pi.incremental-sync.d.ts +63 -0
  98. package/dist/agents/plugins/pi/pi.incremental-sync.d.ts.map +1 -0
  99. package/dist/agents/plugins/pi/pi.incremental-sync.js +266 -0
  100. package/dist/agents/plugins/pi/pi.incremental-sync.js.map +1 -0
  101. package/dist/agents/plugins/pi/pi.models.d.ts +20 -0
  102. package/dist/agents/plugins/pi/pi.models.d.ts.map +1 -0
  103. package/dist/agents/plugins/pi/pi.models.js +201 -0
  104. package/dist/agents/plugins/pi/pi.models.js.map +1 -0
  105. package/dist/agents/plugins/pi/pi.packages.d.ts +11 -0
  106. package/dist/agents/plugins/pi/pi.packages.d.ts.map +1 -0
  107. package/dist/agents/plugins/pi/pi.packages.js +35 -0
  108. package/dist/agents/plugins/pi/pi.packages.js.map +1 -0
  109. package/dist/agents/plugins/pi/pi.paths.d.ts +37 -0
  110. package/dist/agents/plugins/pi/pi.paths.d.ts.map +1 -0
  111. package/dist/agents/plugins/pi/pi.paths.js +146 -0
  112. package/dist/agents/plugins/pi/pi.paths.js.map +1 -0
  113. package/dist/agents/plugins/pi/pi.plugin.d.ts +30 -0
  114. package/dist/agents/plugins/pi/pi.plugin.d.ts.map +1 -0
  115. package/dist/agents/plugins/pi/pi.plugin.js +453 -0
  116. package/dist/agents/plugins/pi/pi.plugin.js.map +1 -0
  117. package/dist/agents/plugins/pi/pi.session.d.ts +114 -0
  118. package/dist/agents/plugins/pi/pi.session.d.ts.map +1 -0
  119. package/dist/agents/plugins/pi/pi.session.js +388 -0
  120. package/dist/agents/plugins/pi/pi.session.js.map +1 -0
  121. package/dist/agents/plugins/pi/pi.setup.d.ts +2 -0
  122. package/dist/agents/plugins/pi/pi.setup.d.ts.map +1 -0
  123. package/dist/agents/plugins/pi/pi.setup.js +33 -0
  124. package/dist/agents/plugins/pi/pi.setup.js.map +1 -0
  125. package/dist/agents/plugins/pi/pi.types.d.ts +143 -0
  126. package/dist/agents/plugins/pi/pi.types.d.ts.map +1 -0
  127. package/dist/agents/plugins/pi/pi.types.js +74 -0
  128. package/dist/agents/plugins/pi/pi.types.js.map +1 -0
  129. package/dist/agents/plugins/pi/session/pi-file-operations.d.ts +17 -0
  130. package/dist/agents/plugins/pi/session/pi-file-operations.d.ts.map +1 -0
  131. package/dist/agents/plugins/pi/session/pi-file-operations.js +98 -0
  132. package/dist/agents/plugins/pi/session/pi-file-operations.js.map +1 -0
  133. package/dist/agents/plugins/pi/session/pi-named-invocations.d.ts +52 -0
  134. package/dist/agents/plugins/pi/session/pi-named-invocations.d.ts.map +1 -0
  135. package/dist/agents/plugins/pi/session/pi-named-invocations.js +189 -0
  136. package/dist/agents/plugins/pi/session/pi-named-invocations.js.map +1 -0
  137. package/dist/agents/plugins/pi/session/pi-user-prompt.d.ts +14 -0
  138. package/dist/agents/plugins/pi/session/pi-user-prompt.d.ts.map +1 -0
  139. package/dist/agents/plugins/pi/session/pi-user-prompt.js +51 -0
  140. package/dist/agents/plugins/pi/session/pi-user-prompt.js.map +1 -0
  141. package/dist/agents/plugins/pi/session/processors/pi.conversations-processor.d.ts +52 -0
  142. package/dist/agents/plugins/pi/session/processors/pi.conversations-processor.d.ts.map +1 -0
  143. package/dist/agents/plugins/pi/session/processors/pi.conversations-processor.js +716 -0
  144. package/dist/agents/plugins/pi/session/processors/pi.conversations-processor.js.map +1 -0
  145. package/dist/agents/plugins/pi/session/processors/pi.metrics-processor.d.ts +104 -0
  146. package/dist/agents/plugins/pi/session/processors/pi.metrics-processor.d.ts.map +1 -0
  147. package/dist/agents/plugins/pi/session/processors/pi.metrics-processor.js +595 -0
  148. package/dist/agents/plugins/pi/session/processors/pi.metrics-processor.js.map +1 -0
  149. package/dist/agents/registry.d.ts +20 -0
  150. package/dist/agents/registry.d.ts.map +1 -1
  151. package/dist/agents/registry.js +29 -2
  152. package/dist/agents/registry.js.map +1 -1
  153. package/dist/cli/commands/analytics/agent-labels.d.ts +3 -0
  154. package/dist/cli/commands/analytics/agent-labels.d.ts.map +1 -0
  155. package/dist/cli/commands/analytics/agent-labels.js +20 -0
  156. package/dist/cli/commands/analytics/agent-labels.js.map +1 -0
  157. package/dist/cli/commands/analytics/cost/cost-enricher.d.ts.map +1 -1
  158. package/dist/cli/commands/analytics/cost/cost-enricher.js +12 -0
  159. package/dist/cli/commands/analytics/cost/cost-enricher.js.map +1 -1
  160. package/dist/cli/commands/analytics/cost/types.d.ts +10 -0
  161. package/dist/cli/commands/analytics/cost/types.d.ts.map +1 -1
  162. package/dist/cli/commands/analytics/cost/usage-readers.d.ts +24 -0
  163. package/dist/cli/commands/analytics/cost/usage-readers.d.ts.map +1 -1
  164. package/dist/cli/commands/analytics/cost/usage-readers.js +253 -54
  165. package/dist/cli/commands/analytics/cost/usage-readers.js.map +1 -1
  166. package/dist/cli/commands/analytics/formatter.d.ts.map +1 -1
  167. package/dist/cli/commands/analytics/formatter.js +10 -2
  168. package/dist/cli/commands/analytics/formatter.js.map +1 -1
  169. package/dist/cli/commands/analytics/index.d.ts.map +1 -1
  170. package/dist/cli/commands/analytics/index.js +22 -0
  171. package/dist/cli/commands/analytics/index.js.map +1 -1
  172. package/dist/cli/commands/analytics/native-loader.d.ts +21 -4
  173. package/dist/cli/commands/analytics/native-loader.d.ts.map +1 -1
  174. package/dist/cli/commands/analytics/native-loader.js +267 -88
  175. package/dist/cli/commands/analytics/native-loader.js.map +1 -1
  176. package/dist/cli/commands/analytics/report/client/app.js +77 -15
  177. package/dist/cli/commands/analytics/report/payload-builder.d.ts.map +1 -1
  178. package/dist/cli/commands/analytics/report/payload-builder.js +32 -2
  179. package/dist/cli/commands/analytics/report/payload-builder.js.map +1 -1
  180. package/dist/cli/commands/analytics/report/types.d.ts +11 -0
  181. package/dist/cli/commands/analytics/report/types.d.ts.map +1 -1
  182. package/dist/cli/commands/hook.d.ts.map +1 -1
  183. package/dist/cli/commands/hook.js +71 -29
  184. package/dist/cli/commands/hook.js.map +1 -1
  185. package/dist/cli/commands/install.d.ts.map +1 -1
  186. package/dist/cli/commands/install.js +9 -7
  187. package/dist/cli/commands/install.js.map +1 -1
  188. package/dist/cli/commands/list.d.ts.map +1 -1
  189. package/dist/cli/commands/list.js +4 -2
  190. package/dist/cli/commands/list.js.map +1 -1
  191. package/dist/cli/commands/setup.d.ts +3 -1
  192. package/dist/cli/commands/setup.d.ts.map +1 -1
  193. package/dist/cli/commands/setup.js +87 -63
  194. package/dist/cli/commands/setup.js.map +1 -1
  195. package/dist/cli/commands/uninstall.d.ts.map +1 -1
  196. package/dist/cli/commands/uninstall.js +6 -4
  197. package/dist/cli/commands/uninstall.js.map +1 -1
  198. package/dist/cli/commands/update.d.ts.map +1 -1
  199. package/dist/cli/commands/update.js +7 -5
  200. package/dist/cli/commands/update.js.map +1 -1
  201. package/dist/cli/first-time.d.ts.map +1 -1
  202. package/dist/cli/first-time.js +10 -9
  203. package/dist/cli/first-time.js.map +1 -1
  204. package/dist/providers/core/auth-validation.d.ts +7 -2
  205. package/dist/providers/core/auth-validation.d.ts.map +1 -1
  206. package/dist/providers/core/auth-validation.js +11 -5
  207. package/dist/providers/core/auth-validation.js.map +1 -1
  208. package/dist/providers/core/types.d.ts +16 -0
  209. package/dist/providers/core/types.d.ts.map +1 -1
  210. package/dist/providers/core/types.js.map +1 -1
  211. package/dist/providers/plugins/sso/proxy/plugins/claude-request-normalizer.plugin.d.ts +8 -3
  212. package/dist/providers/plugins/sso/proxy/plugins/claude-request-normalizer.plugin.d.ts.map +1 -1
  213. package/dist/providers/plugins/sso/proxy/plugins/claude-request-normalizer.plugin.js +46 -11
  214. package/dist/providers/plugins/sso/proxy/plugins/claude-request-normalizer.plugin.js.map +1 -1
  215. package/dist/providers/plugins/sso/proxy/plugins/codex-encrypted-content-sanitizer.plugin.d.ts +23 -13
  216. package/dist/providers/plugins/sso/proxy/plugins/codex-encrypted-content-sanitizer.plugin.d.ts.map +1 -1
  217. package/dist/providers/plugins/sso/proxy/plugins/codex-encrypted-content-sanitizer.plugin.js +75 -18
  218. package/dist/providers/plugins/sso/proxy/plugins/codex-encrypted-content-sanitizer.plugin.js.map +1 -1
  219. package/dist/providers/plugins/sso/proxy/plugins/copilot-encrypted-content-sanitizer.plugin.d.ts +21 -0
  220. package/dist/providers/plugins/sso/proxy/plugins/copilot-encrypted-content-sanitizer.plugin.d.ts.map +1 -0
  221. package/dist/providers/plugins/sso/proxy/plugins/copilot-encrypted-content-sanitizer.plugin.js +133 -0
  222. package/dist/providers/plugins/sso/proxy/plugins/copilot-encrypted-content-sanitizer.plugin.js.map +1 -0
  223. package/dist/providers/plugins/sso/proxy/plugins/header-injection.plugin.js +1 -1
  224. package/dist/providers/plugins/sso/proxy/plugins/header-injection.plugin.js.map +1 -1
  225. package/dist/providers/plugins/sso/proxy/plugins/index.d.ts +2 -1
  226. package/dist/providers/plugins/sso/proxy/plugins/index.d.ts.map +1 -1
  227. package/dist/providers/plugins/sso/proxy/plugins/index.js +4 -2
  228. package/dist/providers/plugins/sso/proxy/plugins/index.js.map +1 -1
  229. package/dist/providers/plugins/sso/proxy/plugins/types.d.ts +11 -0
  230. package/dist/providers/plugins/sso/proxy/plugins/types.d.ts.map +1 -1
  231. package/dist/providers/plugins/sso/proxy/sso.proxy.d.ts +2 -0
  232. package/dist/providers/plugins/sso/proxy/sso.proxy.d.ts.map +1 -1
  233. package/dist/providers/plugins/sso/proxy/sso.proxy.js +37 -1
  234. package/dist/providers/plugins/sso/proxy/sso.proxy.js.map +1 -1
  235. package/dist/providers/plugins/sso/session/SessionSyncer.d.ts.map +1 -1
  236. package/dist/providers/plugins/sso/session/SessionSyncer.js +13 -0
  237. package/dist/providers/plugins/sso/session/SessionSyncer.js.map +1 -1
  238. package/dist/providers/plugins/sso/session/processors/conversations/syncProcessor.js +6 -0
  239. package/dist/providers/plugins/sso/session/processors/conversations/syncProcessor.js.map +1 -1
  240. package/dist/providers/plugins/sso/session/processors/metrics/metrics-post-processor.d.ts +0 -6
  241. package/dist/providers/plugins/sso/session/processors/metrics/metrics-post-processor.d.ts.map +1 -1
  242. package/dist/providers/plugins/sso/session/processors/metrics/metrics-post-processor.js +28 -19
  243. package/dist/providers/plugins/sso/session/processors/metrics/metrics-post-processor.js.map +1 -1
  244. package/dist/providers/plugins/sso/sso.setup-steps.d.ts.map +1 -1
  245. package/dist/providers/plugins/sso/sso.setup-steps.js +39 -22
  246. package/dist/providers/plugins/sso/sso.setup-steps.js.map +1 -1
  247. package/dist/utils/interactive.d.ts +14 -0
  248. package/dist/utils/interactive.d.ts.map +1 -0
  249. package/dist/utils/interactive.js +16 -0
  250. package/dist/utils/interactive.js.map +1 -0
  251. package/package.json +6 -5
  252. package/scripts/copy-plugins.js +6 -0
  253. package/scripts/release.sh +54 -0
@@ -0,0 +1,716 @@
1
+ /**
2
+ * Pi Conversations Processor
3
+ *
4
+ * Turns Pi's v3 JSONL entry tree into incremental CodeMie conversation payloads so a Pi
5
+ * run shows up in the conversations UI the same way Claude, Codex, OpenCode and Gemini
6
+ * runs do.
7
+ *
8
+ * What survives the translation:
9
+ * - user prompts (with Pi's `<skill …>` wrapper stripped, and a wordless prompt described
10
+ * by what it did carry rather than dropped — see `describeUserPrompt`)
11
+ * - the last assistant text of a turn as the visible reply; earlier ones as thoughts
12
+ * - thinking blocks as reasoning thoughts
13
+ * - toolCall/toolResult pairs as Tool thoughts
14
+ * - `!`-style bash executions, unless the user marked them private (`!!`)
15
+ * - compaction and branch summaries as summary thoughts
16
+ *
17
+ * What deliberately does not: anything on an abandoned branch. A Pi session file is an
18
+ * append-only *tree*, so a `/rewind` or an edited message leaves the discarded exchange
19
+ * in the file forever; only the active branch is uploaded.
20
+ */
21
+ import { appendFile, mkdir } from 'fs/promises';
22
+ import { dirname } from 'path';
23
+ import { CONVERSATION_SYNC_STATUS } from '../../../../../providers/plugins/sso/session/processors/conversations/types.js';
24
+ import { CODEMIE_ASSISTANT_ID } from '../../../../../providers/plugins/sso/session/processors/conversations/constants.js';
25
+ import { getSessionConversationPath } from '../../../../../agents/core/session/session-config.js';
26
+ import { readJSONL } from '../../../../../providers/plugins/sso/session/utils/jsonl-reader.js';
27
+ import { logger } from '../../../../../utils/logger.js';
28
+ import { extractPiToolCalls, isPiAssistantMessage, isPiBashExecutionMessage, isPiMessageEntry, isPiToolResultMessage, isPiUserMessage, } from '../../pi.types.js';
29
+ import { parseSkillWrapper } from '../pi-named-invocations.js';
30
+ const PI_CONVERSATIONS_PROCESSOR_NAME = 'pi-conversations';
31
+ const PI_CONVERSATION_FOLDER = 'pi';
32
+ /** Caps on what a single thought contributes to the PUT body. */
33
+ const MAX_TOOL_INPUT_CHARS = 4_000;
34
+ const MAX_TOOL_OUTPUT_CHARS = 8_000;
35
+ /**
36
+ * Stand-in prompt for a skill invocation whose wrapper `parseSkillWrapper` refused to parse.
37
+ *
38
+ * That parser is fail-closed: when Pi changes the wrapper's shape it returns no name and no
39
+ * text rather than risk handing back the skill file's body. The invocation still happened and
40
+ * still opened a turn, so it is reported without a name instead of vanishing.
41
+ */
42
+ const UNNAMED_SKILL_PROMPT = '/skill';
43
+ export class PiConversationsProcessor {
44
+ name = PI_CONVERSATIONS_PROCESSOR_NAME;
45
+ priority = 2;
46
+ shouldProcess(session) {
47
+ if (process.env.CODEMIE_CONV_SYNC_DISABLED === '1')
48
+ return false;
49
+ return session.messages.length > 0;
50
+ }
51
+ async process(session, _context) {
52
+ try {
53
+ const metadata = session.metadata;
54
+ // One conversation per transcript, keyed by the transcript's own header id. There is
55
+ // no usable fallback: the run's `agentSessionId` and the CodeMie `sessionId` are both
56
+ // shared by every transcript a run produces, so using either would merge two
57
+ // conversations into one and interleave their history indices. A transcript that
58
+ // cannot name itself is declined instead.
59
+ const conversationId = typeof metadata?.agentSessionId === 'string' ? metadata.agentSessionId.trim() : '';
60
+ if (!conversationId) {
61
+ logger.warn(`[${PI_CONVERSATIONS_PROCESSOR_NAME}] Skipping a transcript of session ${session.sessionId}: ` +
62
+ 'its header carries no Pi session id, so its turns cannot be filed under a conversation of their own');
63
+ return {
64
+ success: true,
65
+ message: 'Skipped: transcript has no Pi session id',
66
+ metadata: { recordsProcessed: 0, skipReason: 'MISSING_CONVERSATION_ID' },
67
+ };
68
+ }
69
+ const entries = session.messages;
70
+ const activeEntries = selectActiveBranch(entries);
71
+ const fallbackDate = metadata?.createdAt ?? new Date().toISOString();
72
+ const events = normalizeEntries(activeEntries, fallbackDate);
73
+ logger.debug(`[${PI_CONVERSATIONS_PROCESSOR_NAME}] Normalised ${events.length} events from ` +
74
+ `${activeEntries.length} active entries of ${entries.length}`);
75
+ if (events.length === 0) {
76
+ return {
77
+ success: true,
78
+ message: 'No conversation events generated',
79
+ metadata: { recordsProcessed: 0 },
80
+ };
81
+ }
82
+ const conversationsPath = getSessionConversationPath(session.sessionId);
83
+ const existingPayloads = await readJSONL(conversationsPath);
84
+ const syncedSourceIndex = await this.resolveSyncedSourceIndex(session.sessionId, conversationId, existingPayloads);
85
+ const queuedSentinels = new Set(existingPayloads
86
+ .map((payload) => payload.lastProcessedMessageUuid)
87
+ .filter((sentinel) => typeof sentinel === 'string'));
88
+ const llmModel = resolveModel(activeEntries);
89
+ const written = await this.appendTurnPayloads({
90
+ events,
91
+ syncedSourceIndex,
92
+ conversationId,
93
+ conversationsPath,
94
+ queuedSentinels,
95
+ llmModel,
96
+ });
97
+ if (written.payloads === 0) {
98
+ return {
99
+ success: true,
100
+ message: 'No new conversation messages',
101
+ metadata: { recordsProcessed: 0 },
102
+ };
103
+ }
104
+ return {
105
+ success: true,
106
+ message: `Generated ${written.payloads} conversation payload(s) from ${written.events} new events`,
107
+ metadata: {
108
+ recordsProcessed: written.events,
109
+ userMessages: written.userMessages,
110
+ assistantMessages: written.assistantMessages,
111
+ syncUpdates: {
112
+ conversations: {
113
+ lastSyncedMessageUuid: written.lastSentinel,
114
+ lastSyncedHistoryIndex: written.lastHistoryIndex,
115
+ conversationId,
116
+ totalMessagesSynced: written.userMessages + written.assistantMessages,
117
+ totalSyncAttempts: 1,
118
+ lastSyncAt: Date.now(),
119
+ },
120
+ },
121
+ },
122
+ };
123
+ }
124
+ catch (error) {
125
+ const message = error instanceof Error ? error.message : String(error);
126
+ logger.error(`[${PI_CONVERSATIONS_PROCESSOR_NAME}] Processing failed:`, error);
127
+ return {
128
+ success: false,
129
+ message: `Conversations processing failed: ${message}`,
130
+ metadata: { failureReason: 'PROCESSING_ERROR' },
131
+ };
132
+ }
133
+ }
134
+ /**
135
+ * The file position this conversation's sync reached.
136
+ *
137
+ * Both the persisted session state and the queued payload file are shared by every
138
+ * transcript of the run, so a checkpoint only counts when its sentinel names *this*
139
+ * conversation — otherwise a `/new` transcript would inherit the previous one's position
140
+ * and silently skip its own opening turns.
141
+ *
142
+ * Only the position is taken from state. `lastSyncedHistoryIndex` deliberately is not:
143
+ * the sync processor computes it as a maximum over the payloads of *all* conversations
144
+ * in the session and the session store then keeps it monotonic, so it says nothing about
145
+ * where one conversation is. Turn numbering comes from the transcript instead
146
+ * (see `scanNextTurn`).
147
+ */
148
+ async resolveSyncedSourceIndex(sessionId, conversationId, existingPayloads) {
149
+ let sourceIndex = -1;
150
+ const { SessionStore } = await import('../../../../../agents/core/session/SessionStore.js');
151
+ const sessionMetadata = await new SessionStore().loadSession(sessionId);
152
+ const persisted = sessionMetadata?.sync?.conversations;
153
+ if (persisted && belongsToConversation(persisted.lastSyncedMessageUuid, conversationId)) {
154
+ sourceIndex = parseSentinelIndex(persisted.lastSyncedMessageUuid);
155
+ }
156
+ for (const payload of existingPayloads) {
157
+ if (!belongsToConversation(payload.lastProcessedMessageUuid, conversationId))
158
+ continue;
159
+ sourceIndex = Math.max(sourceIndex, parseSentinelIndex(payload.lastProcessedMessageUuid));
160
+ }
161
+ return sourceIndex;
162
+ }
163
+ /**
164
+ * Queue one payload per complete turn past the checkpoint.
165
+ *
166
+ * Pi only fires SessionStart and SessionEnd, so a run's whole backlog arrives in a
167
+ * single call. Emitting one window per call — as the per-turn-hooked agents can
168
+ * afford to — would strand every turn but the first.
169
+ */
170
+ async appendTurnPayloads(options) {
171
+ const { events, conversationId, conversationsPath, queuedSentinels, llmModel } = options;
172
+ let cursorSourceIndex = options.syncedSourceIndex;
173
+ let payloads = 0;
174
+ let newEvents = 0;
175
+ let userMessages = 0;
176
+ let assistantMessages = 0;
177
+ let lastSentinel;
178
+ let lastHistoryIndex;
179
+ let directoryReady = false;
180
+ /** Summaries the cursor has passed without uploading; they ride with the next payload. */
181
+ let carriedSummaries = [];
182
+ for (;;) {
183
+ const scan = scanNextTurn(events, cursorSourceIndex);
184
+ if (scan.kind === 'done')
185
+ break;
186
+ if (scan.kind === 'skip') {
187
+ carriedSummaries = [
188
+ ...carriedSummaries,
189
+ ...summariesUpTo(events, cursorSourceIndex, scan.resumeSourceIndex),
190
+ ];
191
+ cursorSourceIndex = scan.resumeSourceIndex;
192
+ continue;
193
+ }
194
+ const leadingSummaries = [
195
+ ...carriedSummaries,
196
+ ...summariesUpTo(events, cursorSourceIndex, scan.turn.user.sourceIndex),
197
+ ];
198
+ const turn = leadingSummaries.length > 0
199
+ ? { ...scan.turn, events: [...leadingSummaries, ...scan.turn.events] }
200
+ : scan.turn;
201
+ const turnEvents = turn.events.filter((event) => event.sourceIndex > cursorSourceIndex);
202
+ if (turnEvents.length === 0)
203
+ break;
204
+ const endSourceIndex = Math.max(...turnEvents.map((event) => event.sourceIndex));
205
+ // The cursor must strictly advance, or the loop below never terminates.
206
+ if (endSourceIndex <= cursorSourceIndex)
207
+ break;
208
+ const sentinel = `${conversationId}@${endSourceIndex}`;
209
+ const history = queuedSentinels.has(sentinel) ? [] : turnToHistory(turn, cursorSourceIndex);
210
+ if (history.length > 0) {
211
+ if (!directoryReady) {
212
+ await mkdir(dirname(conversationsPath), { recursive: true });
213
+ directoryReady = true;
214
+ }
215
+ const payloadRecord = {
216
+ payloadId: sentinel,
217
+ timestamp: Date.now(),
218
+ isTurnContinuation: turn.isTurnContinuation,
219
+ historyIndices: history.map((entry) => entry.history_index),
220
+ messageCount: history.length,
221
+ lastProcessedMessageUuid: sentinel,
222
+ payload: {
223
+ conversationId,
224
+ assistantId: CODEMIE_ASSISTANT_ID,
225
+ folder: PI_CONVERSATION_FOLDER,
226
+ llmModel,
227
+ history,
228
+ },
229
+ status: CONVERSATION_SYNC_STATUS.PENDING,
230
+ };
231
+ await appendFile(conversationsPath, JSON.stringify(payloadRecord) + '\n', 'utf-8');
232
+ queuedSentinels.add(sentinel);
233
+ payloads += 1;
234
+ newEvents += turnEvents.length;
235
+ userMessages += history.filter((entry) => entry.role === 'User').length;
236
+ assistantMessages += history.filter((entry) => entry.role === 'Assistant').length;
237
+ lastSentinel = sentinel;
238
+ lastHistoryIndex = turn.historyIndex;
239
+ carriedSummaries = [];
240
+ }
241
+ cursorSourceIndex = endSourceIndex;
242
+ }
243
+ return { payloads, events: newEvents, userMessages, assistantMessages, lastSentinel, lastHistoryIndex };
244
+ }
245
+ }
246
+ /**
247
+ * The entries on the active branch, in file order, each tagged with its file position.
248
+ *
249
+ * A Pi session file is an append-only tree: `branch()` moves the leaf back without
250
+ * touching what is already written, so every `/rewind` and every edited message leaves
251
+ * its abandoned exchange in the file. Reading the file as a flat log would upload
252
+ * prompts and answers the user explicitly took back, so the active path is rebuilt the
253
+ * way Pi itself does — from the leaf up through `parentId`.
254
+ *
255
+ * `sourceIndex` stays the position in the *file*, never in the path: the file is
256
+ * append-only, so those positions are stable across runs and safe to checkpoint on.
257
+ *
258
+ * @see buildSessionPath in packages/coding-agent/src/core/session-manager.ts (Pi repo)
259
+ */
260
+ function selectActiveBranch(entries) {
261
+ const byId = new Map();
262
+ let leaf;
263
+ for (const [sourceIndex, entry] of entries.entries()) {
264
+ // The `session` header line is not part of the tree — upstream drops it too.
265
+ if (!entry || typeof entry !== 'object' || entry.type === 'session')
266
+ continue;
267
+ // Only tree nodes may become the leaf. Transcripts written before CodeMie stopped
268
+ // appending its `codemie_session_start` ownership marker still end with one, and foreign
269
+ // tooling may append too. Such a line carries no `id`, so taking it as the leaf ended the
270
+ // parent walk on its first step and normalised the whole conversation down to nothing.
271
+ if (typeof entry.id !== 'string' || !entry.id)
272
+ continue;
273
+ const node = { entry, sourceIndex };
274
+ byId.set(entry.id, node);
275
+ // Pi appends every entry as a child of the current leaf, so the last one written is it.
276
+ leaf = node;
277
+ }
278
+ const path = [];
279
+ const visited = new Set();
280
+ let current = leaf;
281
+ while (current) {
282
+ path.push(current);
283
+ const { id, parentId } = current.entry;
284
+ // Pi never writes a cycle, but this reads a third-party file: a corrupt or
285
+ // hand-edited parent chain must end the walk, not spin it.
286
+ if (typeof id === 'string')
287
+ visited.add(id);
288
+ current = typeof parentId === 'string' && !visited.has(parentId) ? byId.get(parentId) : undefined;
289
+ }
290
+ return path.reverse();
291
+ }
292
+ function normalizeEntries(activeEntries, fallbackDate) {
293
+ const events = [];
294
+ for (const { entry, sourceIndex } of activeEntries) {
295
+ const date = resolveEntryDate(entry, fallbackDate);
296
+ if (entry.type === 'compaction' || entry.type === 'branch_summary') {
297
+ const summary = entry.summary;
298
+ if (typeof summary === 'string' && summary.trim()) {
299
+ events.push({
300
+ kind: 'summary',
301
+ sourceIndex,
302
+ date,
303
+ text: truncate(summary, MAX_TOOL_OUTPUT_CHARS),
304
+ authorName: entry.type === 'compaction' ? 'Compaction Summary' : 'Branch Summary',
305
+ metadata: { entry_type: entry.type },
306
+ });
307
+ }
308
+ continue;
309
+ }
310
+ if (!isPiMessageEntry(entry))
311
+ continue;
312
+ const message = entry.message;
313
+ if (isPiUserMessage(message)) {
314
+ // Every user message opens a turn, whatever it carried. Emitting the event only for
315
+ // messages that still had words after the skill wrapper came off used to merge the
316
+ // wordless one into the turn before it, which published the next answer under the
317
+ // previous question.
318
+ events.push({ kind: 'user_prompt', sourceIndex, date, text: describeUserPrompt(message.content) });
319
+ continue;
320
+ }
321
+ if (isPiAssistantMessage(message)) {
322
+ events.push(...normalizeAssistantContent(message, sourceIndex, date));
323
+ continue;
324
+ }
325
+ if (isPiToolResultMessage(message)) {
326
+ events.push({
327
+ kind: 'tool_output',
328
+ sourceIndex,
329
+ date,
330
+ callId: typeof message.toolCallId === 'string' ? message.toolCallId : undefined,
331
+ authorName: typeof message.toolName === 'string' ? message.toolName : undefined,
332
+ text: truncate(joinTextParts(message.content), MAX_TOOL_OUTPUT_CHARS),
333
+ error: message.isError === true,
334
+ });
335
+ continue;
336
+ }
337
+ if (isPiBashExecutionMessage(message)) {
338
+ const bashEvent = normalizeBashExecution(message, sourceIndex, date);
339
+ if (bashEvent)
340
+ events.push(bashEvent);
341
+ }
342
+ }
343
+ return events;
344
+ }
345
+ function normalizeAssistantContent(message, sourceIndex, date) {
346
+ if (!Array.isArray(message.content))
347
+ return [];
348
+ // Tool calls come from the shared normalizer, so this processor accepts exactly the
349
+ // serializations the metrics processor does. It returns them in content order, so
350
+ // walking both lists in lockstep keeps thoughts in the order Pi emitted them.
351
+ const toolCalls = extractPiToolCalls(message);
352
+ let nextToolCall = 0;
353
+ const events = [];
354
+ for (const part of message.content) {
355
+ if (!part || typeof part !== 'object')
356
+ continue;
357
+ const typed = part;
358
+ const toolCall = toolCalls[nextToolCall];
359
+ if (toolCall && (toolCall === part || toolCall === typed.toolCall)) {
360
+ nextToolCall += 1;
361
+ events.push({
362
+ kind: 'tool_call',
363
+ sourceIndex,
364
+ date,
365
+ callId: toolCall.id,
366
+ authorName: toolCall.name,
367
+ inputText: truncate(stringifyToolArguments(toolCall.arguments), MAX_TOOL_INPUT_CHARS),
368
+ });
369
+ continue;
370
+ }
371
+ if (typed.type === 'text' && typeof typed.text === 'string' && typed.text.trim()) {
372
+ events.push({ kind: 'assistant_text', sourceIndex, date, text: typed.text });
373
+ continue;
374
+ }
375
+ if (typed.type === 'thinking' && typeof typed.thinking === 'string' && typed.thinking.trim()) {
376
+ events.push({ kind: 'thinking', sourceIndex, date, text: typed.thinking });
377
+ }
378
+ }
379
+ return events;
380
+ }
381
+ /**
382
+ * A `!` bash execution becomes a Tool thought. A `!!` one becomes nothing at all:
383
+ * Pi keeps that output out of the model's context precisely because the user does not
384
+ * want it shared, so it must not reach CodeMie either.
385
+ */
386
+ function normalizeBashExecution(message, sourceIndex, date) {
387
+ if (message.excludeFromContext === true) {
388
+ logger.debug(`[${PI_CONVERSATIONS_PROCESSOR_NAME}] Skipping bash execution marked excludeFromContext at entry ${sourceIndex}`);
389
+ return undefined;
390
+ }
391
+ const command = typeof message.command === 'string' ? message.command : '';
392
+ const output = typeof message.output === 'string' ? message.output : '';
393
+ if (!command && !output)
394
+ return undefined;
395
+ const exitCode = typeof message.exitCode === 'number' ? message.exitCode : undefined;
396
+ return {
397
+ kind: 'bash_execution',
398
+ sourceIndex,
399
+ date,
400
+ authorName: 'bash',
401
+ inputText: truncate(command, MAX_TOOL_INPUT_CHARS),
402
+ text: truncate(output, MAX_TOOL_OUTPUT_CHARS),
403
+ error: message.cancelled === true || (exitCode !== undefined && exitCode !== 0),
404
+ metadata: { ...(exitCode !== undefined && { exit_code: exitCode }) },
405
+ };
406
+ }
407
+ /**
408
+ * What to do at `cursorSourceIndex`: upload a turn, step over a stretch that carries
409
+ * nothing to upload, or stop.
410
+ *
411
+ * A turn is one user prompt and everything that followed it. Its `historyIndex` is the
412
+ * prompt's ordinal on the *active branch* — derived from the transcript itself so that
413
+ * numbering cannot drift with cross-conversation sync state, and so that re-processing a
414
+ * transcript always lands each turn on the same index.
415
+ *
416
+ * Because the ordinal counts the active branch, a `/rewind` makes it go backwards: the
417
+ * replacement turn takes the index of the exchange it replaced, so a later call can queue a
418
+ * second payload carrying an already-used index. That is deliberate. The uploaded
419
+ * conversation is meant to be the branch the user kept, and re-using the index is the only
420
+ * way to say "this exchange took the place of that one" through a `history_index`; a
421
+ * monotonic counter would instead publish the rewound exchange and its replacement
422
+ * side by side, a conversation that never happened. Whether CodeMie's upsert then repairs
423
+ * the row or appends a duplicate is a backend property this repo cannot see — see the
424
+ * `/rewind` test in `__tests__/pi.conversations-processor.test.ts`, which pins the choice.
425
+ * Rewinding past more turns than the replacement supplies still strands the extra rows.
426
+ */
427
+ function scanNextTurn(events, cursorSourceIndex) {
428
+ const firstNewEvent = events.find((event) => event.sourceIndex > cursorSourceIndex);
429
+ if (!firstNewEvent)
430
+ return { kind: 'done' };
431
+ const userEvents = events.filter((event) => event.kind === 'user_prompt');
432
+ const userEvent = lastUserEventUpTo(userEvents, firstNewEvent.sourceIndex) ??
433
+ // Nothing before the new events opened a turn — a transcript that starts with a
434
+ // compaction summary, say. Resume at the next prompt rather than stalling the stream.
435
+ userEvents.find((event) => event.sourceIndex > cursorSourceIndex);
436
+ if (!userEvent)
437
+ return { kind: 'done' };
438
+ const nextUserEvent = userEvents.find((event) => event.sourceIndex > userEvent.sourceIndex);
439
+ const turnEndExclusive = nextUserEvent?.sourceIndex ?? Number.MAX_SAFE_INTEGER;
440
+ const turnEvents = events.filter((event) => event.sourceIndex >= userEvent.sourceIndex && event.sourceIndex < turnEndExclusive);
441
+ const isTurnContinuation = userEvent.sourceIndex <= cursorSourceIndex;
442
+ const hasNewAssistantText = turnEvents.some((event) => event.kind === 'assistant_text' && event.sourceIndex > cursorSourceIndex);
443
+ if (isTurnContinuation && !hasNewAssistantText) {
444
+ // The tail of an already-uploaded turn brought no new answer — a model turn that only
445
+ // called tools, which is what an interrupted run usually ends on. A payload hangs its
446
+ // thoughts off an assistant message, and this stretch has none, so its events (tool
447
+ // calls, their results, any thinking) are dropped rather than published under an empty
448
+ // answer. Every later turn must still be reached, so step over it instead of ending the
449
+ // stream; summaries in the stretch are the exception and ride along with the next
450
+ // payload (see `carriedSummaries` in `appendTurnPayloads`).
451
+ const resumeSourceIndex = Math.max(...turnEvents.map((event) => event.sourceIndex));
452
+ return resumeSourceIndex > cursorSourceIndex ? { kind: 'skip', resumeSourceIndex } : { kind: 'done' };
453
+ }
454
+ return {
455
+ kind: 'turn',
456
+ turn: {
457
+ user: userEvent,
458
+ events: turnEvents,
459
+ historyIndex: userEvents.indexOf(userEvent),
460
+ isTurnContinuation,
461
+ },
462
+ };
463
+ }
464
+ /** Summary events in `(afterExclusive, throughInclusive]`, in file order. */
465
+ function summariesUpTo(events, afterExclusive, throughInclusive) {
466
+ return events.filter((event) => event.kind === 'summary' &&
467
+ event.sourceIndex > afterExclusive &&
468
+ event.sourceIndex <= throughInclusive);
469
+ }
470
+ function turnToHistory(turn, cursorSourceIndex) {
471
+ const history = [];
472
+ const finalAssistant = lastEventOfKind(turn.events, 'assistant_text');
473
+ // A prompt that describes nothing at all still opened the turn — that is why the event
474
+ // exists — but there is no message to publish for it, so the turn goes out with only its
475
+ // answer rather than with an empty User bubble.
476
+ if (turn.user.sourceIndex > cursorSourceIndex && turn.user.text) {
477
+ history.push({
478
+ role: 'User',
479
+ message: turn.user.text,
480
+ message_raw: turn.user.text,
481
+ history_index: turn.historyIndex,
482
+ date: turn.user.date,
483
+ file_names: [],
484
+ });
485
+ }
486
+ if (finalAssistant && finalAssistant.sourceIndex > cursorSourceIndex) {
487
+ const thoughts = buildThoughts(turn.events, turn.historyIndex, finalAssistant);
488
+ history.push({
489
+ role: 'Assistant',
490
+ message: finalAssistant.text,
491
+ message_raw: finalAssistant.text,
492
+ history_index: turn.historyIndex,
493
+ date: finalAssistant.date,
494
+ response_time: calculateResponseTime(turn.user.date, finalAssistant.date),
495
+ assistant_id: CODEMIE_ASSISTANT_ID,
496
+ thoughts: thoughts.length > 0 ? thoughts : undefined,
497
+ });
498
+ }
499
+ return history;
500
+ }
501
+ function buildThoughts(events, historyIndex, finalAssistant) {
502
+ const thoughts = [];
503
+ const toolThoughtByCallId = new Map();
504
+ for (const event of events) {
505
+ if (event === finalAssistant || event.kind === 'user_prompt')
506
+ continue;
507
+ if (event.kind === 'tool_output') {
508
+ const pending = event.callId ? toolThoughtByCallId.get(event.callId) : undefined;
509
+ if (pending) {
510
+ pending.message = event.text ?? '';
511
+ pending.error = event.error === true;
512
+ pending.metadata = {
513
+ ...pending.metadata,
514
+ output_source_index: event.sourceIndex,
515
+ output_timestamp: event.date,
516
+ };
517
+ if (event.callId)
518
+ toolThoughtByCallId.delete(event.callId);
519
+ continue;
520
+ }
521
+ }
522
+ const thought = createThought(event, historyIndex);
523
+ thoughts.push(thought);
524
+ if (event.kind === 'tool_call' && event.callId) {
525
+ toolThoughtByCallId.set(event.callId, thought);
526
+ }
527
+ }
528
+ return thoughts;
529
+ }
530
+ function createThought(event, historyIndex) {
531
+ const thought = {
532
+ id: `pi-${event.kind}-${historyIndex}-${event.sourceIndex}`,
533
+ parent_id: null,
534
+ metadata: {
535
+ timestamp: event.date,
536
+ source_index: event.sourceIndex,
537
+ event_kind: event.kind,
538
+ ...(event.callId && { call_id: event.callId }),
539
+ ...(event.metadata ?? {}),
540
+ },
541
+ in_progress: false,
542
+ input_text: event.inputText ?? '',
543
+ message: event.text ?? '',
544
+ author_type: 'Agent',
545
+ author_name: 'Pi',
546
+ output_format: 'text',
547
+ error: event.error === true,
548
+ interrupted: false,
549
+ aborted: false,
550
+ children: [],
551
+ };
552
+ switch (event.kind) {
553
+ case 'tool_call':
554
+ case 'tool_output':
555
+ case 'bash_execution':
556
+ thought.id = event.callId ?? thought.id;
557
+ thought.author_type = 'Tool';
558
+ thought.author_name = event.authorName ?? 'Unknown Tool';
559
+ break;
560
+ case 'thinking':
561
+ thought.author_name = 'Pi Reasoning';
562
+ break;
563
+ case 'summary':
564
+ thought.author_name = event.authorName ?? 'Summary';
565
+ thought.output_format = 'summary';
566
+ break;
567
+ default:
568
+ break;
569
+ }
570
+ return thought;
571
+ }
572
+ function lastEventOfKind(events, kind) {
573
+ for (let index = events.length - 1; index >= 0; index -= 1) {
574
+ if (events[index].kind === kind)
575
+ return events[index];
576
+ }
577
+ return undefined;
578
+ }
579
+ function lastUserEventUpTo(userEvents, sourceIndex) {
580
+ for (let index = userEvents.length - 1; index >= 0; index -= 1) {
581
+ if (userEvents[index].sourceIndex <= sourceIndex)
582
+ return userEvents[index];
583
+ }
584
+ return undefined;
585
+ }
586
+ /** The model of the most recent assistant message on the branch, which is what it ran on. */
587
+ function resolveModel(activeEntries) {
588
+ for (let index = activeEntries.length - 1; index >= 0; index -= 1) {
589
+ const { entry } = activeEntries[index];
590
+ if (!isPiMessageEntry(entry))
591
+ continue;
592
+ const message = entry.message;
593
+ if (isPiAssistantMessage(message) && typeof message.model === 'string' && message.model.trim()) {
594
+ return message.model;
595
+ }
596
+ }
597
+ return process.env.CODEMIE_MODEL || undefined;
598
+ }
599
+ /** The non-blank text blocks of a Pi message's content, in order. */
600
+ function textBlocks(content) {
601
+ if (typeof content === 'string')
602
+ return [content];
603
+ if (!Array.isArray(content))
604
+ return [];
605
+ const blocks = [];
606
+ for (const part of content) {
607
+ if (typeof part === 'string') {
608
+ if (part.trim())
609
+ blocks.push(part);
610
+ continue;
611
+ }
612
+ if (!part || typeof part !== 'object')
613
+ continue;
614
+ const typed = part;
615
+ if (typed.type === 'text' && typeof typed.text === 'string' && typed.text.trim()) {
616
+ blocks.push(typed.text);
617
+ }
618
+ }
619
+ return blocks;
620
+ }
621
+ function joinTextParts(content) {
622
+ return textBlocks(content).join('\n');
623
+ }
624
+ /** Image parts Pi appends after the text block when the user attaches or pastes a picture. */
625
+ function countImageParts(content) {
626
+ if (!Array.isArray(content))
627
+ return 0;
628
+ return content.filter((part) => !!part && typeof part === 'object' && part.type === 'image').length;
629
+ }
630
+ /**
631
+ * What to publish as the user's message, given one Pi user message's content.
632
+ *
633
+ * Parsed one text block at a time rather than over the joined content. Blocks join with a
634
+ * single `\n`, while `parseSkillWrapper`'s tail is anchored to the `\n\n` Pi writes after
635
+ * `</skill>`, so parsing the joined string makes a multi-block message miss the anchored
636
+ * match, fall through to the fail-closed opening test, and lose the whole prompt.
637
+ *
638
+ * A message with no words of its own still describes itself rather than returning empty:
639
+ * `/skill:<name>` for a bare `/skill:name` invocation (upstream `_expandSkillCommand`
640
+ * appends no tail when the command carried no arguments), an attachment count for an
641
+ * image-only message. The skill *body* never appears — only `parseSkillWrapper`'s `name`
642
+ * is read, and that parser is fail-closed by design.
643
+ *
644
+ * `piUserText` in `../pi-user-prompt.ts` does the same per-block strip for the analytics
645
+ * session title; it stops short of naming the skill because a title has nothing to anchor.
646
+ * The wrapper's shape itself is not restated here — `parseSkillWrapper` owns it.
647
+ */
648
+ function describeUserPrompt(content) {
649
+ const words = [];
650
+ const skillNames = [];
651
+ let sawUnparsableWrapper = false;
652
+ for (const block of textBlocks(content)) {
653
+ const { name, rest } = parseSkillWrapper(block);
654
+ if (name)
655
+ skillNames.push(name);
656
+ const text = rest.trim();
657
+ if (text)
658
+ words.push(text);
659
+ // A non-blank block that the parser emptied without naming a skill is the fail-closed
660
+ // path: a wrapper Pi wrote in a shape this plugin no longer recognizes.
661
+ else if (!name && block.trim())
662
+ sawUnparsableWrapper = true;
663
+ }
664
+ if (words.length > 0)
665
+ return words.join('\n');
666
+ if (skillNames.length > 0)
667
+ return skillNames.map((name) => `/skill:${name}`).join(' ');
668
+ if (sawUnparsableWrapper)
669
+ return UNNAMED_SKILL_PROMPT;
670
+ const images = countImageParts(content);
671
+ return images > 0 ? `[${images} image attachment${images === 1 ? '' : 's'}]` : '';
672
+ }
673
+ function stringifyToolArguments(args) {
674
+ if (args === undefined || args === null)
675
+ return '';
676
+ if (typeof args === 'string')
677
+ return args;
678
+ try {
679
+ return JSON.stringify(args) ?? '';
680
+ }
681
+ catch {
682
+ return '';
683
+ }
684
+ }
685
+ function truncate(text, maxChars) {
686
+ if (text.length <= maxChars)
687
+ return text;
688
+ return `${text.slice(0, maxChars)}\n... [truncated ${text.length - maxChars} characters]`;
689
+ }
690
+ function resolveEntryDate(entry, fallback) {
691
+ const entryTimestamp = entry.timestamp;
692
+ if (typeof entryTimestamp === 'string' && entryTimestamp.trim())
693
+ return entryTimestamp;
694
+ const messageTimestamp = entry.message?.timestamp;
695
+ if (typeof messageTimestamp === 'number' && Number.isFinite(messageTimestamp)) {
696
+ return new Date(messageTimestamp).toISOString();
697
+ }
698
+ return fallback;
699
+ }
700
+ function calculateResponseTime(start, end) {
701
+ const startMs = Date.parse(start);
702
+ const endMs = Date.parse(end);
703
+ if (Number.isNaN(startMs) || Number.isNaN(endMs))
704
+ return undefined;
705
+ return Math.max(0, Math.round(((endMs - startMs) / 1000) * 100) / 100);
706
+ }
707
+ function belongsToConversation(sentinel, conversationId) {
708
+ return typeof sentinel === 'string' && sentinel.slice(0, sentinel.lastIndexOf('@')) === conversationId;
709
+ }
710
+ function parseSentinelIndex(sentinel) {
711
+ if (typeof sentinel !== 'string')
712
+ return -1;
713
+ const index = Number.parseInt(sentinel.slice(sentinel.lastIndexOf('@') + 1), 10);
714
+ return Number.isFinite(index) ? index : -1;
715
+ }
716
+ //# sourceMappingURL=pi.conversations-processor.js.map