wind-agent-cli 1.38.2 → 1.39.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 (185) hide show
  1. package/README.md +8 -1
  2. package/dist/api-docs.html +4 -3
  3. package/dist/assets/{CodeEditor-C6BtLdoC.js → CodeEditor-C8eMIGen.js} +1 -1
  4. package/dist/assets/{FileTree-CGxNFZda.js → FileTree-D52nAPO4.js} +1 -1
  5. package/dist/assets/{GitPanel-BV-IAyEC.js → GitPanel-DgeKbZsW.js} +1 -1
  6. package/dist/assets/{MarkdownPreview-DVoEzpoc.js → MarkdownPreview-B00VQTx6.js} +1 -1
  7. package/dist/assets/{Shell-CUTO-THd.js → Shell-twzi9t5B.js} +7 -7
  8. package/dist/assets/{StandaloneShell-BKM1qxUc.js → StandaloneShell-DH_GBJMj.js} +1 -1
  9. package/dist/assets/{TaskMasterPanel-DDDCVF6M.js → TaskMasterPanel-BY-pMPMl.js} +2 -2
  10. package/dist/assets/{abnfDiagram-N423BO3Z-Crubtav6.js → abnfDiagram-N423BO3Z-BwBdstkH.js} +1 -1
  11. package/dist/assets/{arc-h9xb7EWA.js → arc-C0IOpbFP.js} +1 -1
  12. package/dist/assets/{architectureDiagram-T3A2C74G-BmMQp4MC.js → architectureDiagram-T3A2C74G-C-Wkj_AY.js} +1 -1
  13. package/dist/assets/{blockDiagram-VBNYF7ZC-BtD-zDnJ.js → blockDiagram-VBNYF7ZC-_sXAZKku.js} +1 -1
  14. package/dist/assets/{c4Diagram-5PPSVZJV-D18s6J-v.js → c4Diagram-5PPSVZJV-C0DUsZv8.js} +1 -1
  15. package/dist/assets/channel-B6xiB58V.js +1 -0
  16. package/dist/assets/{chunk-2GRJ4B5K-BHIcCvB_.js → chunk-2GRJ4B5K-D2TFB_6A.js} +1 -1
  17. package/dist/assets/{chunk-2Q5K7J3B-DcX4AeRe.js → chunk-2Q5K7J3B-B_co_2bI.js} +1 -1
  18. package/dist/assets/{chunk-5RXB4S5H-CZwYDv9K.js → chunk-5RXB4S5H-BcUSLq4H.js} +1 -1
  19. package/dist/assets/{chunk-5VM5RSS4-CR-F7R13.js → chunk-5VM5RSS4-0mQFBKSp.js} +1 -1
  20. package/dist/assets/{chunk-6Q2QTUOP-BPP216SB.js → chunk-6Q2QTUOP-BAkqmW1M.js} +1 -1
  21. package/dist/assets/{chunk-GF5L2VYU-CcFeSmHe.js → chunk-GF5L2VYU-Cyz0Xl7f.js} +1 -1
  22. package/dist/assets/{chunk-JWPE2WC7-C5T_9-OI.js → chunk-JWPE2WC7-DYO47aT-.js} +1 -1
  23. package/dist/assets/{chunk-KBJHAD2P-g5eR5X5h.js → chunk-KBJHAD2P-DvXEzV8J.js} +1 -1
  24. package/dist/assets/{chunk-RYQCIY6F-76WBDsoZ.js → chunk-RYQCIY6F-Beut8dYt.js} +1 -1
  25. package/dist/assets/{chunk-XXDRQBXY-DGuy3LLn.js → chunk-XXDRQBXY-DLIy1GFl.js} +1 -1
  26. package/dist/assets/classDiagram-JCYQIIEL-C7xIPTll.js +1 -0
  27. package/dist/assets/classDiagram-v2-OCEON4UE-C7xIPTll.js +1 -0
  28. package/dist/assets/{cose-bilkent-JH36ORCC-Cg4R1M0H.js → cose-bilkent-JH36ORCC-DynbO4dj.js} +1 -1
  29. package/dist/assets/{cynefin-VYW2F7L2-Cbmals-o.js → cynefin-VYW2F7L2-CYTJsH3o.js} +1 -1
  30. package/dist/assets/{cynefinDiagram-MW4NZA55-CI79hpHc.js → cynefinDiagram-MW4NZA55-ftj-lAeE.js} +1 -1
  31. package/dist/assets/{dagre-VZM6K2ZE-r3Uz8Fhv.js → dagre-VZM6K2ZE-C1kSFboe.js} +1 -1
  32. package/dist/assets/{diagram-7IWD3JNH-ClRIkmQw.js → diagram-7IWD3JNH-CqXHTFrY.js} +1 -1
  33. package/dist/assets/{diagram-B4RE2ZJO-C71qkNlS.js → diagram-B4RE2ZJO-B84n3Zko.js} +1 -1
  34. package/dist/assets/{diagram-LBJQPF4R-CC7ShhCC.js → diagram-LBJQPF4R-B710SefS.js} +1 -1
  35. package/dist/assets/{diagram-Q27KOJAE-DPHL4vnP.js → diagram-Q27KOJAE-CmVPl9pq.js} +1 -1
  36. package/dist/assets/{diagram-UB23O5K3-CP_My5E9.js → diagram-UB23O5K3-LozbeaFL.js} +1 -1
  37. package/dist/assets/{ebnfDiagram-BXEA7PRR-BQe4AU9Z.js → ebnfDiagram-BXEA7PRR-B321UBUA.js} +1 -1
  38. package/dist/assets/{erDiagram-JOGREHBK-CFidQ-d5.js → erDiagram-JOGREHBK-CTbw676k.js} +1 -1
  39. package/dist/assets/feature-chat-pY0kOrz2.js +88 -0
  40. package/dist/assets/feature-workspace-FLomYkPq.js +39 -0
  41. package/dist/assets/{flowDiagram-UKHOOZJN-X5fPFxbp.js → flowDiagram-UKHOOZJN-BWVKTWlw.js} +1 -1
  42. package/dist/assets/{ganttDiagram-PKOTCBZU-CILFHjP8.js → ganttDiagram-PKOTCBZU-BAj9eeeX.js} +1 -1
  43. package/dist/assets/{gitGraphDiagram-DS77QQ5N-DCQCtC_g.js → gitGraphDiagram-DS77QQ5N-DcVMRU7N.js} +1 -1
  44. package/dist/assets/{index-cNARKAN2.js → index-BYFstHFT.js} +1 -1
  45. package/dist/assets/index-BbBSplOU.js +35 -0
  46. package/dist/assets/{index-D0vbG70l.js → index-BrWTp075.js} +1 -1
  47. package/dist/assets/index-CHukVVB2.css +1 -0
  48. package/dist/assets/{index-Bo9nFrtn.js → index-k9M0VaFg.js} +1 -1
  49. package/dist/assets/{infoDiagram-6WML65LV-Da145n1G.js → infoDiagram-6WML65LV-V6wz33B4.js} +1 -1
  50. package/dist/assets/{ishikawaDiagram-WSZJBQD7-DGUP61eq.js → ishikawaDiagram-WSZJBQD7-Dd-WNPyK.js} +1 -1
  51. package/dist/assets/{journeyDiagram-NVQOT4AX-BqfnKWDJ.js → journeyDiagram-NVQOT4AX-D9VCuCeA.js} +1 -1
  52. package/dist/assets/{kanban-definition-27J2QSJJ-US6pQfNy.js → kanban-definition-27J2QSJJ-CUtwDOl-.js} +1 -1
  53. package/dist/assets/{linear-d91wK5kZ.js → linear-D_XGf1jO.js} +1 -1
  54. package/dist/assets/{mermaid.core-BsWqrIaW.js → mermaid.core-DOnsXioB.js} +4 -4
  55. package/dist/assets/{mindmap-definition-FAOFIHXS-CXWD2UbS.js → mindmap-definition-FAOFIHXS-DAYNL5ZV.js} +1 -1
  56. package/dist/assets/{pegDiagram-VL7TDLO6-BVzfDR-8.js → pegDiagram-VL7TDLO6-B5FX3-E6.js} +1 -1
  57. package/dist/assets/{pieDiagram-7S7Q4E2Y-PNpHZnJ9.js → pieDiagram-7S7Q4E2Y-EbnDBVd0.js} +1 -1
  58. package/dist/assets/{quadrantDiagram-CIZ2JOQS-DxCI4i3K.js → quadrantDiagram-CIZ2JOQS-C1koOtwT.js} +1 -1
  59. package/dist/assets/{railroadDiagram-AXF67PYL-BYkvo3om.js → railroadDiagram-AXF67PYL-D52xmshj.js} +1 -1
  60. package/dist/assets/{requirementDiagram-LRYGKXZP-LUtvOYtV.js → requirementDiagram-LRYGKXZP-B7VbfNnW.js} +1 -1
  61. package/dist/assets/{sankeyDiagram-W5VNT64P-CVyI8YeM.js → sankeyDiagram-W5VNT64P-CeRG1vHN.js} +1 -1
  62. package/dist/assets/{sequenceDiagram-SI44F4Z6-1vuRFolM.js → sequenceDiagram-SI44F4Z6-CfKHnmSE.js} +1 -1
  63. package/dist/assets/{sizeCapture-X5ZJPWSS-o4oVZXe8.js → sizeCapture-X5ZJPWSS-DJTzyd0S.js} +1 -1
  64. package/dist/assets/{stateDiagram-OKZ733FA-WedLIlXF.js → stateDiagram-OKZ733FA-DN0c7IYD.js} +1 -1
  65. package/dist/assets/stateDiagram-v2-UEYNNEHI-DbihlVOQ.js +1 -0
  66. package/dist/assets/{swimlanes-SLNWSIFB-CpeXuk67.js → swimlanes-SLNWSIFB-CaaJpBDA.js} +2 -2
  67. package/dist/assets/swimlanesDiagram-ULZ7WXOC-BBRevuGP.js +8 -0
  68. package/dist/assets/{timeline-definition-Z64GVDOM-DFcPWe47.js → timeline-definition-Z64GVDOM-CrzHN7d2.js} +1 -1
  69. package/dist/assets/{vennDiagram-T6HMQDX7-BH8C1C49.js → vennDiagram-T6HMQDX7-BTldHIjo.js} +1 -1
  70. package/dist/assets/{wardleyDiagram-T6FBY63Y-K1Ew0uit.js → wardleyDiagram-T6FBY63Y-D9Y50OsZ.js} +1 -1
  71. package/dist/assets/{xychartDiagram-ELKLHX3M-CQHlqWY2.js → xychartDiagram-ELKLHX3M-DjxSJ3aF.js} +1 -1
  72. package/dist/favicon.png +0 -0
  73. package/dist/icons/icon-128x128.png +0 -0
  74. package/dist/icons/icon-144x144.png +0 -0
  75. package/dist/icons/icon-152x152.png +0 -0
  76. package/dist/icons/icon-180x180.png +0 -0
  77. package/dist/icons/icon-192x192.png +0 -0
  78. package/dist/icons/icon-384x384.png +0 -0
  79. package/dist/icons/icon-512x512.png +0 -0
  80. package/dist/icons/icon-72x72.png +0 -0
  81. package/dist/icons/icon-96x96.png +0 -0
  82. package/dist/icons/icon-maskable-192x192.png +0 -0
  83. package/dist/icons/icon-maskable-512x512.png +0 -0
  84. package/dist/icons/new.png +0 -0
  85. package/dist/index.html +4 -4
  86. package/dist/logo-128.png +0 -0
  87. package/dist/logo-256.png +0 -0
  88. package/dist/sw.js +1 -1
  89. package/dist-server/server/index.js +2 -0
  90. package/dist-server/server/index.js.map +1 -1
  91. package/dist-server/server/modules/agent/agent.routes.js +14 -2
  92. package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
  93. package/dist-server/server/modules/agent/tests/agent.routes.test.js +1 -0
  94. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
  95. package/dist-server/server/modules/commands/commands.routes.js +2 -1
  96. package/dist-server/server/modules/commands/commands.routes.js.map +1 -1
  97. package/dist-server/server/modules/database/migrations.js +43 -0
  98. package/dist-server/server/modules/database/migrations.js.map +1 -1
  99. package/dist-server/server/modules/database/tests/projects.db.integration.test.js +24 -1
  100. package/dist-server/server/modules/database/tests/projects.db.integration.test.js.map +1 -1
  101. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +2 -0
  102. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
  103. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +10 -1
  104. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  105. package/dist-server/server/modules/providers/list/pi/index.js +5 -0
  106. package/dist-server/server/modules/providers/list/pi/index.js.map +1 -0
  107. package/dist-server/server/modules/providers/list/pi/pi-auth.provider.js +55 -0
  108. package/dist-server/server/modules/providers/list/pi/pi-auth.provider.js.map +1 -0
  109. package/dist-server/server/modules/providers/list/pi/pi-mcp.provider.js +23 -0
  110. package/dist-server/server/modules/providers/list/pi/pi-mcp.provider.js.map +1 -0
  111. package/dist-server/server/modules/providers/list/pi/pi-models.provider.js +99 -0
  112. package/dist-server/server/modules/providers/list/pi/pi-models.provider.js.map +1 -0
  113. package/dist-server/server/modules/providers/list/pi/pi-runtime.provider.js +262 -0
  114. package/dist-server/server/modules/providers/list/pi/pi-runtime.provider.js.map +1 -0
  115. package/dist-server/server/modules/providers/list/pi/pi-session-synchronizer.provider.js +136 -0
  116. package/dist-server/server/modules/providers/list/pi/pi-session-synchronizer.provider.js.map +1 -0
  117. package/dist-server/server/modules/providers/list/pi/pi-sessions.provider.js +225 -0
  118. package/dist-server/server/modules/providers/list/pi/pi-sessions.provider.js.map +1 -0
  119. package/dist-server/server/modules/providers/list/pi/pi-skills.provider.js +31 -0
  120. package/dist-server/server/modules/providers/list/pi/pi-skills.provider.js.map +1 -0
  121. package/dist-server/server/modules/providers/list/pi/pi.provider.js +22 -0
  122. package/dist-server/server/modules/providers/list/pi/pi.provider.js.map +1 -0
  123. package/dist-server/server/modules/providers/provider.registry.js +2 -0
  124. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  125. package/dist-server/server/modules/providers/provider.routes.js +2 -1
  126. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  127. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +26 -0
  128. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
  129. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
  130. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  131. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +5 -0
  132. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  133. package/dist-server/server/modules/providers/tests/mcp.test.js +4 -2
  134. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  135. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +56 -5
  136. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  137. package/dist-server/server/modules/providers/tests/pi-provider.test.js +244 -0
  138. package/dist-server/server/modules/providers/tests/pi-provider.test.js.map +1 -0
  139. package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js +9 -0
  140. package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js.map +1 -1
  141. package/dist-server/server/shared/utils.js +19 -0
  142. package/dist-server/server/shared/utils.js.map +1 -1
  143. package/package.json +1 -1
  144. package/public/api-docs.html +4 -3
  145. package/scripts/generate-pwa-icons.ps1 +1 -1
  146. package/server/index.ts +2 -0
  147. package/server/modules/agent/agent.module.ts +1 -1
  148. package/server/modules/agent/agent.routes.ts +15 -2
  149. package/server/modules/agent/tests/agent.routes.test.ts +1 -0
  150. package/server/modules/commands/commands.routes.ts +2 -1
  151. package/server/modules/database/migrations.ts +52 -0
  152. package/server/modules/database/tests/projects.db.integration.test.ts +28 -1
  153. package/server/modules/notifications/services/notification-orchestrator.service.js +2 -0
  154. package/server/modules/providers/README.md +5 -1
  155. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +18 -0
  156. package/server/modules/providers/list/pi/index.ts +4 -0
  157. package/server/modules/providers/list/pi/pi-auth.provider.ts +58 -0
  158. package/server/modules/providers/list/pi/pi-mcp.provider.ts +33 -0
  159. package/server/modules/providers/list/pi/pi-models.provider.ts +114 -0
  160. package/server/modules/providers/list/pi/pi-runtime.provider.ts +271 -0
  161. package/server/modules/providers/list/pi/pi-session-synchronizer.provider.ts +134 -0
  162. package/server/modules/providers/list/pi/pi-sessions.provider.ts +206 -0
  163. package/server/modules/providers/list/pi/pi-skills.provider.ts +37 -0
  164. package/server/modules/providers/list/pi/pi.provider.ts +33 -0
  165. package/server/modules/providers/provider.registry.ts +2 -0
  166. package/server/modules/providers/provider.routes.ts +1 -0
  167. package/server/modules/providers/services/provider-capabilities.service.ts +32 -0
  168. package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
  169. package/server/modules/providers/services/sessions-watcher.service.ts +5 -0
  170. package/server/modules/providers/tests/mcp.test.ts +4 -2
  171. package/server/modules/providers/tests/opencode-sessions.test.ts +81 -6
  172. package/server/modules/providers/tests/pi-provider.test.ts +244 -0
  173. package/server/modules/providers/tests/provider-runtime.service.test.ts +10 -0
  174. package/server/shared/types.ts +1 -1
  175. package/server/shared/utils.ts +23 -0
  176. package/dist/assets/channel-CuR3zGvy.js +0 -1
  177. package/dist/assets/classDiagram-JCYQIIEL-BWpl-4NG.js +0 -1
  178. package/dist/assets/classDiagram-v2-OCEON4UE-BWpl-4NG.js +0 -1
  179. package/dist/assets/feature-chat-CHKsDTs-.js +0 -88
  180. package/dist/assets/feature-workspace-BodovOw6.js +0 -39
  181. package/dist/assets/index-Bn37f1jp.js +0 -35
  182. package/dist/assets/index-Cr5MbHuj.css +0 -1
  183. package/dist/assets/stateDiagram-v2-UEYNNEHI-BqzvLVVB.js +0 -1
  184. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CeI6fByR.js +0 -8
  185. package/dist/icons/new-icon.png +0 -0
@@ -0,0 +1,206 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ import { sessionsDb } from '@/modules/database/index.js';
4
+ import { parseFilesInputTag } from '@/shared/image-attachments.js';
5
+ import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
6
+ import type { IProviderSessions } from '@/shared/interfaces.js';
7
+ import {
8
+ createNormalizedMessage,
9
+ generateMessageId,
10
+ normalizeProviderTimestamp,
11
+ readObjectRecord,
12
+ readOptionalString,
13
+ sliceTailPage,
14
+ } from '@/shared/utils.js';
15
+
16
+ const textFromContent = (content: unknown, type = 'text'): string => {
17
+ if (typeof content === 'string') return content;
18
+ if (!Array.isArray(content)) return '';
19
+ return content.flatMap((part) => {
20
+ const record = readObjectRecord(part);
21
+ return record?.type === type ? [readOptionalString(record.text) ?? ''] : [];
22
+ }).join('');
23
+ };
24
+
25
+ const formatToolResult = (result: unknown): string => {
26
+ if (typeof result === 'string') return result;
27
+ const record = readObjectRecord(result);
28
+ const content = record?.content;
29
+ const text = textFromContent(content);
30
+ if (text) return text;
31
+ try { return JSON.stringify(content ?? result, null, 2); } catch { return String(result ?? ''); }
32
+ };
33
+
34
+ const tokenBudgetFromEntries = (entries: AnyRecord[]): AnyRecord | undefined => {
35
+ let input = 0;
36
+ let output = 0;
37
+ let cacheRead = 0;
38
+ let cacheWrite = 0;
39
+ for (const entry of entries) {
40
+ const usage = readObjectRecord(readObjectRecord(entry.message)?.usage) ?? readObjectRecord(entry.usage);
41
+ if (!usage) continue;
42
+ input += Number(usage.input ?? 0);
43
+ output += Number(usage.output ?? 0);
44
+ cacheRead += Number(usage.cacheRead ?? 0);
45
+ cacheWrite += Number(usage.cacheWrite ?? 0);
46
+ }
47
+ const used = input + output + cacheRead + cacheWrite;
48
+ return used > 0 ? {
49
+ used,
50
+ inputTokens: input + cacheRead,
51
+ outputTokens: output,
52
+ breakdown: { input: input + cacheRead, output },
53
+ } : undefined;
54
+ };
55
+
56
+ const activeBranch = (entries: AnyRecord[]): AnyRecord[] => {
57
+ const withIds = entries.filter((entry) => readOptionalString(entry.id));
58
+ if (withIds.length === 0 || !withIds.some((entry) => readOptionalString(entry.parentId))) return entries;
59
+ const byId = new Map(withIds.map((entry) => [String(entry.id), entry]));
60
+ const branch: AnyRecord[] = [];
61
+ let current: AnyRecord | undefined = withIds[withIds.length - 1];
62
+ const seen = new Set<string>();
63
+ while (current) {
64
+ const id = readOptionalString(current.id);
65
+ if (!id || seen.has(id)) break;
66
+ seen.add(id);
67
+ branch.push(current);
68
+ const parentId = readOptionalString(current.parentId);
69
+ current = parentId ? byId.get(parentId) : undefined;
70
+ }
71
+ return branch.reverse();
72
+ };
73
+
74
+ /** Normalizes Pi RPC events and JSONL history into WindCli messages. */
75
+ export class PiSessionsProvider implements IProviderSessions {
76
+ normalizeMessage(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] {
77
+ const raw = readObjectRecord(rawMessage);
78
+ if (!raw) return [];
79
+ const type = readOptionalString(raw.type);
80
+ const base = { provider: 'pi' as const, sessionId, timestamp: normalizeProviderTimestamp(raw.timestamp) };
81
+ if (type === 'message_update') {
82
+ const update = readObjectRecord(raw.assistantMessageEvent) ?? readObjectRecord(raw.event);
83
+ const delta = readOptionalString(update?.delta);
84
+ if (!delta) return [];
85
+ if (update?.type === 'text_delta') return [createNormalizedMessage({ ...base, kind: 'stream_delta', content: delta })];
86
+ if (update?.type === 'thinking_delta') return [createNormalizedMessage({ ...base, kind: 'thinking', content: delta })];
87
+ }
88
+ if (type === 'tool_execution_start') {
89
+ return [createNormalizedMessage({
90
+ ...base,
91
+ kind: 'tool_use',
92
+ toolId: readOptionalString(raw.toolCallId) ?? generateMessageId('pi-tool'),
93
+ toolName: readOptionalString(raw.toolName) ?? 'Tool',
94
+ toolInput: raw.args ?? {},
95
+ })];
96
+ }
97
+ if (type === 'tool_execution_end') {
98
+ const toolId = readOptionalString(raw.toolCallId) ?? generateMessageId('pi-tool');
99
+ return [createNormalizedMessage({
100
+ ...base,
101
+ kind: 'tool_result',
102
+ toolId,
103
+ toolName: readOptionalString(raw.toolName) ?? 'Tool',
104
+ toolResult: { content: formatToolResult(raw.result), isError: Boolean(raw.isError) },
105
+ })];
106
+ }
107
+ if (type === 'agent_end' && raw.willRetry !== true) {
108
+ return [createNormalizedMessage({ ...base, kind: 'stream_end' })];
109
+ }
110
+ if (type === 'error' || type === 'fault') {
111
+ return [createNormalizedMessage({
112
+ ...base,
113
+ kind: 'error',
114
+ content: readOptionalString(raw.message) ?? readOptionalString(raw.error) ?? 'Unknown Pi error',
115
+ })];
116
+ }
117
+ return [];
118
+ }
119
+
120
+ async fetchHistory(sessionId: string, options: FetchHistoryOptions = {}): Promise<FetchHistoryResult> {
121
+ const row = sessionsDb.getSessionById(sessionId);
122
+ if (!row?.jsonl_path) return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
123
+ try {
124
+ const entries = (await readFile(row.jsonl_path, 'utf8')).split(/\r?\n/).flatMap((line) => {
125
+ if (!line.trim()) return [];
126
+ try { return [JSON.parse(line) as AnyRecord]; } catch { return []; }
127
+ });
128
+ const branch = activeBranch(entries.filter((entry) => entry.type !== 'session'));
129
+ const messages = this.normalizeHistory(branch, sessionId);
130
+ const offset = Math.max(0, options.offset ?? 0);
131
+ const limit = options.limit === undefined ? null : options.limit === null ? null : Math.max(0, options.limit);
132
+ const { page, hasMore } = sliceTailPage(messages, limit, offset);
133
+ return {
134
+ messages: page,
135
+ total: messages.length,
136
+ hasMore,
137
+ offset,
138
+ limit,
139
+ tokenUsage: tokenBudgetFromEntries(branch),
140
+ };
141
+ } catch {
142
+ return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
143
+ }
144
+ }
145
+
146
+ private normalizeHistory(entries: AnyRecord[], sessionId: string): NormalizedMessage[] {
147
+ const messages: NormalizedMessage[] = [];
148
+ const tools = new Map<string, NormalizedMessage>();
149
+ for (const entry of entries) {
150
+ const id = readOptionalString(entry.id) ?? generateMessageId('pi-history');
151
+ const timestamp = normalizeProviderTimestamp(entry.timestamp);
152
+ if (entry.type === 'message') {
153
+ const message = readObjectRecord(entry.message);
154
+ const role = readOptionalString(message?.role);
155
+ if (role === 'user') {
156
+ const parsed = parseFilesInputTag(textFromContent(message?.content));
157
+ messages.push(createNormalizedMessage({
158
+ id, provider: 'pi', sessionId, timestamp, kind: 'text', role: 'user',
159
+ content: parsed.text, ...(parsed.attachments.length ? { files: parsed.attachments } : {}),
160
+ }));
161
+ } else if (role === 'assistant') {
162
+ const content = Array.isArray(message?.content) ? message.content : [];
163
+ content.forEach((part, index) => {
164
+ const block = readObjectRecord(part);
165
+ if (!block) return;
166
+ const blockId = `${id}:${index}`;
167
+ if (block.type === 'text' || block.type === 'thinking') {
168
+ const text = readOptionalString(block.text);
169
+ if (text) messages.push(createNormalizedMessage({
170
+ id: blockId, provider: 'pi', sessionId, timestamp,
171
+ kind: block.type === 'thinking' ? 'thinking' : 'text', role: 'assistant', content: text,
172
+ }));
173
+ } else if (block.type === 'toolCall' || block.type === 'tool_use') {
174
+ const toolId = readOptionalString(block.id) ?? blockId;
175
+ const normalized = createNormalizedMessage({
176
+ id: blockId, provider: 'pi', sessionId, timestamp, kind: 'tool_use',
177
+ toolId, toolName: readOptionalString(block.name) ?? 'Tool', toolInput: block.arguments ?? block.input ?? {},
178
+ });
179
+ tools.set(toolId, normalized);
180
+ messages.push(normalized);
181
+ }
182
+ });
183
+ } else if (role === 'toolResult' || role === 'tool_result') {
184
+ const toolId = readOptionalString(message?.toolCallId) ?? readOptionalString(message?.tool_use_id);
185
+ const result = { content: formatToolResult(message), isError: Boolean(message?.isError ?? message?.is_error) };
186
+ const tool = toolId ? tools.get(toolId) : undefined;
187
+ if (tool) tool.toolResult = result;
188
+ else messages.push(createNormalizedMessage({
189
+ id, provider: 'pi', sessionId, timestamp, kind: 'tool_result', toolId, toolResult: result,
190
+ }));
191
+ }
192
+ } else if (entry.type === 'compaction') {
193
+ const content = readOptionalString(entry.summary);
194
+ if (content) messages.push(createNormalizedMessage({
195
+ id, provider: 'pi', sessionId, timestamp, kind: 'text', role: 'assistant', content, isCompactSummary: true,
196
+ }));
197
+ } else if (entry.type === 'custom_message' && entry.display !== false) {
198
+ const content = textFromContent(entry.content);
199
+ if (content) messages.push(createNormalizedMessage({
200
+ id, provider: 'pi', sessionId, timestamp, kind: 'text', role: 'assistant', content,
201
+ }));
202
+ }
203
+ }
204
+ return messages;
205
+ }
206
+ }
@@ -0,0 +1,37 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ import { SkillsProvider } from '@/modules/providers/shared/skills/skills.provider.js';
5
+ import type { ProviderSkillSource } from '@/shared/types.js';
6
+ import { addUniqueProviderSkillSource, findTopmostGitRoot } from '@/shared/utils.js';
7
+
8
+ /** Discovers Pi-native and cross-agent skills using Pi's `/skill:<name>` syntax. */
9
+ export class PiSkillsProvider extends SkillsProvider {
10
+ constructor() {
11
+ super('pi');
12
+ }
13
+
14
+ protected async getSkillSources(workspacePath: string): Promise<ProviderSkillSource[]> {
15
+ const sources: ProviderSkillSource[] = [];
16
+ const seen = new Set<string>();
17
+ const add = (scope: 'user' | 'project', rootDir: string) => addUniqueProviderSkillSource(
18
+ sources,
19
+ seen,
20
+ { scope, rootDir, commandForSkill: (skillName) => `/skill:${skillName}` },
21
+ );
22
+ add('user', path.join(os.homedir(), '.pi', 'agent', 'skills'));
23
+ add('user', path.join(os.homedir(), '.agents', 'skills'));
24
+ add('project', path.join(workspacePath, '.pi', 'skills'));
25
+
26
+ const repoRoot = await findTopmostGitRoot(workspacePath);
27
+ let current = path.resolve(workspacePath);
28
+ while (true) {
29
+ add('project', path.join(current, '.agents', 'skills'));
30
+ if (!repoRoot || current === path.resolve(repoRoot)) break;
31
+ const parent = path.dirname(current);
32
+ if (parent === current) break;
33
+ current = parent;
34
+ }
35
+ return sources;
36
+ }
37
+ }
@@ -0,0 +1,33 @@
1
+ import type {
2
+ IProviderAuth,
3
+ IProviderMcp,
4
+ IProviderModels,
5
+ IProviderRuntime,
6
+ IProviderSessionSynchronizer,
7
+ IProviderSessions,
8
+ IProviderSkills,
9
+ } from '@/shared/interfaces.js';
10
+ import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
11
+
12
+ import { PiProviderAuth } from './pi-auth.provider.js';
13
+ import { PiMcpProvider } from './pi-mcp.provider.js';
14
+ import { PiProviderModels } from './pi-models.provider.js';
15
+ import { piRuntime } from './pi-runtime.provider.js';
16
+ import { PiSessionSynchronizer } from './pi-session-synchronizer.provider.js';
17
+ import { PiSessionsProvider } from './pi-sessions.provider.js';
18
+ import { PiSkillsProvider } from './pi-skills.provider.js';
19
+
20
+ /** Registry wrapper that composes all seven Pi provider facets. */
21
+ export class PiProvider extends AbstractProvider {
22
+ readonly runtime: IProviderRuntime = piRuntime;
23
+ readonly models: IProviderModels = new PiProviderModels();
24
+ readonly mcp: IProviderMcp = new PiMcpProvider();
25
+ readonly auth: IProviderAuth = new PiProviderAuth();
26
+ readonly skills: IProviderSkills = new PiSkillsProvider();
27
+ readonly sessions: IProviderSessions = new PiSessionsProvider();
28
+ readonly sessionSynchronizer: IProviderSessionSynchronizer = new PiSessionSynchronizer();
29
+
30
+ constructor() {
31
+ super('pi');
32
+ }
33
+ }
@@ -2,6 +2,7 @@ import { ClaudeProvider } from '@/modules/providers/list/claude/claude.provider.
2
2
  import { CodexProvider } from '@/modules/providers/list/codex/codex.provider.js';
3
3
  import { CursorProvider } from '@/modules/providers/list/cursor/cursor.provider.js';
4
4
  import { OpenCodeProvider } from '@/modules/providers/list/opencode/opencode.provider.js';
5
+ import { PiProvider } from '@/modules/providers/list/pi/index.js';
5
6
  import type { IProvider } from '@/shared/interfaces.js';
6
7
  import type { LLMProvider } from '@/shared/types.js';
7
8
  import { AppError } from '@/shared/utils.js';
@@ -11,6 +12,7 @@ const providers: Record<LLMProvider, IProvider> = {
11
12
  codex: new CodexProvider(),
12
13
  cursor: new CursorProvider(),
13
14
  opencode: new OpenCodeProvider(),
15
+ pi: new PiProvider(),
14
16
  };
15
17
 
16
18
  /**
@@ -287,6 +287,7 @@ const parseProvider = (value: unknown): LLMProvider => {
287
287
  || normalized === 'codex'
288
288
  || normalized === 'cursor'
289
289
  || normalized === 'opencode'
290
+ || normalized === 'pi'
290
291
  ) {
291
292
  return normalized;
292
293
  }
@@ -25,6 +25,12 @@ type ProviderCapabilities = {
25
25
  supportsTokenUsage: boolean;
26
26
  /** Whether the provider runtime can accept model-level reasoning effort. */
27
27
  supportsEffort: boolean;
28
+ /** Whether the provider exposes configurable MCP servers. */
29
+ supportsMcp: boolean;
30
+ /** Whether the provider exposes discoverable/installable skills. */
31
+ supportsSkills: boolean;
32
+ /** Whether the Settings UI has provider permission controls. */
33
+ supportsPermissionSettings: boolean;
28
34
  };
29
35
 
30
36
  /**
@@ -44,6 +50,9 @@ const PROVIDER_CAPABILITIES: Record<LLMProvider, ProviderCapabilities> = {
44
50
  supportsPermissionRequests: true,
45
51
  supportsTokenUsage: true,
46
52
  supportsEffort: true,
53
+ supportsMcp: true,
54
+ supportsSkills: true,
55
+ supportsPermissionSettings: true,
47
56
  },
48
57
  cursor: {
49
58
  provider: 'cursor',
@@ -55,6 +64,9 @@ const PROVIDER_CAPABILITIES: Record<LLMProvider, ProviderCapabilities> = {
55
64
  supportsPermissionRequests: false,
56
65
  supportsTokenUsage: false,
57
66
  supportsEffort: false,
67
+ supportsMcp: true,
68
+ supportsSkills: true,
69
+ supportsPermissionSettings: true,
58
70
  },
59
71
  codex: {
60
72
  provider: 'codex',
@@ -66,6 +78,9 @@ const PROVIDER_CAPABILITIES: Record<LLMProvider, ProviderCapabilities> = {
66
78
  supportsPermissionRequests: false,
67
79
  supportsTokenUsage: true,
68
80
  supportsEffort: true,
81
+ supportsMcp: true,
82
+ supportsSkills: true,
83
+ supportsPermissionSettings: true,
69
84
  },
70
85
  opencode: {
71
86
  provider: 'opencode',
@@ -80,6 +95,23 @@ const PROVIDER_CAPABILITIES: Record<LLMProvider, ProviderCapabilities> = {
80
95
  supportsPermissionRequests: false,
81
96
  supportsTokenUsage: true,
82
97
  supportsEffort: true,
98
+ supportsMcp: true,
99
+ supportsSkills: false,
100
+ supportsPermissionSettings: false,
101
+ },
102
+ pi: {
103
+ provider: 'pi',
104
+ permissionModes: ['default'],
105
+ defaultPermissionMode: 'default',
106
+ supportsImages: true,
107
+ supportsFiles: true,
108
+ supportsAbort: true,
109
+ supportsPermissionRequests: false,
110
+ supportsTokenUsage: true,
111
+ supportsEffort: true,
112
+ supportsMcp: false,
113
+ supportsSkills: true,
114
+ supportsPermissionSettings: false,
83
115
  },
84
116
  };
85
117
 
@@ -22,6 +22,7 @@ export const sessionSynchronizerService = {
22
22
  codex: 0,
23
23
  cursor: 0,
24
24
  opencode: 0,
25
+ pi: 0,
25
26
  };
26
27
  const failures: string[] = [];
27
28
 
@@ -10,6 +10,7 @@ import { WS_OPEN_STATE, connectedClients } from '@/modules/websocket/index.js';
10
10
  import type { LLMProvider } from '@/shared/types.js';
11
11
  import { generateDisplayName } from '@/modules/projects/index.js';
12
12
  import { SessionWatchScheduler } from '@/modules/providers/services/session-watch-scheduler.js';
13
+ import { getPiSessionDirectory } from '@/modules/providers/list/pi/index.js';
13
14
 
14
15
  const PROVIDER_WATCH_PATHS: Array<{ provider: LLMProvider; rootPath: string }> = [
15
16
  {
@@ -28,6 +29,10 @@ const PROVIDER_WATCH_PATHS: Array<{ provider: LLMProvider; rootPath: string }> =
28
29
  provider: 'opencode',
29
30
  rootPath: path.join(os.homedir(), '.local', 'share', 'opencode'),
30
31
  },
32
+ {
33
+ provider: 'pi',
34
+ rootPath: getPiSessionDirectory(),
35
+ },
31
36
  ];
32
37
 
33
38
  const WATCHER_IGNORED_DIRECTORY_NAMES = new Set([
@@ -313,8 +313,10 @@ test('providerMcpService global adder writes to all providers and rejects unsupp
313
313
  workspacePath,
314
314
  });
315
315
 
316
- assert.equal(globalResult.length, 4);
317
- assert.ok(globalResult.every((entry) => entry.created === true));
316
+ assert.equal(globalResult.length, 5);
317
+ assert.ok(globalResult.filter((entry) => entry.provider !== 'pi').every((entry) => entry.created === true));
318
+ assert.equal(globalResult.find((entry) => entry.provider === 'pi')?.created, false);
319
+ assert.match(globalResult.find((entry) => entry.provider === 'pi')?.error ?? '', /does not provide native MCP/);
318
320
 
319
321
  const claudeProject = await readJson(path.join(workspacePath, '.mcp.json'));
320
322
  assert.ok((claudeProject.mcpServers as Record<string, unknown>)['global-http']);
@@ -6,7 +6,7 @@ import test from 'node:test';
6
6
 
7
7
  import Database from 'better-sqlite3';
8
8
 
9
- import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
9
+ import { closeConnection, initializeDatabase, projectsDb, sessionsDb } from '@/modules/database/index.js';
10
10
  import { OpenCodeSessionSynchronizer } from '@/modules/providers/list/opencode/opencode-session-synchronizer.provider.js';
11
11
  import { OpenCodeSessionsProvider } from '@/modules/providers/list/opencode/opencode-sessions.provider.js';
12
12
  import { appendImagesInputTag } from '@/shared/image-attachments.js';
@@ -41,9 +41,15 @@ async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promis
41
41
  }
42
42
  }
43
43
 
44
- const createOpenCodeDatabase = async (homeDir: string, workspacePath: string): Promise<void> => {
44
+ const createOpenCodeDatabase = async (
45
+ homeDir: string,
46
+ workspacePath: string,
47
+ options: { projectId?: string; worktreePath?: string } = {},
48
+ ): Promise<void> => {
45
49
  const dataDir = path.join(homeDir, '.local', 'share', 'opencode');
46
50
  await mkdir(dataDir, { recursive: true });
51
+ const projectId = options.projectId ?? 'project-1';
52
+ const worktreePath = options.worktreePath ?? workspacePath;
47
53
 
48
54
  const db = new Database(path.join(dataDir, 'opencode.db'));
49
55
  try {
@@ -123,8 +129,8 @@ const createOpenCodeDatabase = async (homeDir: string, workspacePath: string): P
123
129
  db.prepare(
124
130
  'INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES (?, ?, ?, ?, ?)',
125
131
  ).run(
126
- 'project-1',
127
- workspacePath,
132
+ projectId,
133
+ worktreePath,
128
134
  1_700_000_000_000,
129
135
  1_700_000_001_000,
130
136
  '[]',
@@ -137,7 +143,7 @@ const createOpenCodeDatabase = async (homeDir: string, workspacePath: string): P
137
143
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
138
144
  `).run(
139
145
  'open-session-1',
140
- 'project-1',
146
+ projectId,
141
147
  'open-session-1',
142
148
  workspacePath,
143
149
  'OpenCode indexed title',
@@ -253,7 +259,10 @@ test('OpenCode session synchronizer indexes sqlite sessions without deletable tr
253
259
  const restoreHomeDir = patchHomeDir(tempRoot);
254
260
 
255
261
  try {
256
- await createOpenCodeDatabase(tempRoot, workspacePath);
262
+ await createOpenCodeDatabase(tempRoot, workspacePath, {
263
+ projectId: 'global',
264
+ worktreePath: '/',
265
+ });
257
266
  await withIsolatedDatabase(() => {
258
267
  const synchronizer = new OpenCodeSessionSynchronizer();
259
268
  const processed = synchronizer.synchronize();
@@ -273,6 +282,72 @@ test('OpenCode session synchronizer indexes sqlite sessions without deletable tr
273
282
  }
274
283
  });
275
284
 
285
+ test('OpenCode session synchronizer skips external Multica global workdirs', { concurrency: false }, async () => {
286
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-sync-multica-'));
287
+ const workspacePath = path.join(
288
+ tempRoot,
289
+ 'multica_workspaces_desktop-api.example.test',
290
+ 'workspace-id',
291
+ 'deadbeef',
292
+ 'workdir',
293
+ );
294
+ await mkdir(workspacePath, { recursive: true });
295
+ const restoreHomeDir = patchHomeDir(tempRoot);
296
+
297
+ try {
298
+ await createOpenCodeDatabase(tempRoot, workspacePath, {
299
+ projectId: 'global',
300
+ worktreePath: '/',
301
+ });
302
+ await withIsolatedDatabase(async () => {
303
+ const count = await new OpenCodeSessionSynchronizer().synchronize();
304
+
305
+ assert.equal(count, 0);
306
+ assert.equal(sessionsDb.getSessionById('open-session-1'), null);
307
+ assert.equal(projectsDb.getProjectPath(workspacePath), null);
308
+ });
309
+ } finally {
310
+ restoreHomeDir();
311
+ await rm(tempRoot, { recursive: true, force: true });
312
+ }
313
+ });
314
+
315
+ test('OpenCode session synchronizer keeps WindCli sessions in Multica global workdirs', { concurrency: false }, async () => {
316
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-sync-multica-app-'));
317
+ const workspacePath = path.join(
318
+ tempRoot,
319
+ 'multica_workspaces_desktop-api.example.test',
320
+ 'workspace-id',
321
+ 'deadbeef',
322
+ 'workdir',
323
+ );
324
+ await mkdir(workspacePath, { recursive: true });
325
+ const restoreHomeDir = patchHomeDir(tempRoot);
326
+
327
+ try {
328
+ await createOpenCodeDatabase(tempRoot, workspacePath, {
329
+ projectId: 'global',
330
+ worktreePath: '/',
331
+ });
332
+ await withIsolatedDatabase(async () => {
333
+ sessionsDb.createAppSession('app-session-multica', 'opencode', workspacePath);
334
+ sessionsDb.assignProviderSessionId('app-session-multica', 'open-session-1');
335
+
336
+ const count = await new OpenCodeSessionSynchronizer().synchronize();
337
+
338
+ assert.equal(count, 1);
339
+ assert.equal(sessionsDb.getAllSessions().length, 1);
340
+ assert.equal(
341
+ sessionsDb.getSessionById('app-session-multica')?.provider_session_id,
342
+ 'open-session-1',
343
+ );
344
+ });
345
+ } finally {
346
+ restoreHomeDir();
347
+ await rm(tempRoot, { recursive: true, force: true });
348
+ }
349
+ });
350
+
276
351
  test('OpenCode session synchronizer returns the app session id once provider mapping exists', { concurrency: false }, async () => {
277
352
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-session-sync-mapped-'));
278
353
  const workspacePath = path.join(tempRoot, 'workspace');