@hienlh/ppm 0.9.0-beta.3 → 0.9.0-beta.4

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 (186) hide show
  1. package/CHANGELOG.md +9 -44
  2. package/dist/web/assets/{_basePickBy-CZovQgWd.js → _basePickBy-COwDPZl_.js} +1 -1
  3. package/dist/web/assets/{_baseUniq-ClnvscgW.js → _baseUniq-DCb0mkTp.js} +1 -1
  4. package/dist/web/assets/{api-settings--eVrUeZM.js → api-settings-CuUkz5gb.js} +1 -1
  5. package/dist/web/assets/{arc-C2Qaz-ch.js → arc-D0bJaFyD.js} +1 -1
  6. package/dist/web/assets/architecture-PBZL5I3N-281eTKQ3.js +1 -0
  7. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-Jq91S_rs.js → architectureDiagram-2XIMDMQ5-BVEUkQYB.js} +1 -1
  8. package/dist/web/assets/arrow-left-C_j9Ki73.js +1 -0
  9. package/dist/web/assets/{blockDiagram-WCTKOSBZ-CKGufRTy.js → blockDiagram-WCTKOSBZ-CU2t4NHJ.js} +1 -1
  10. package/dist/web/assets/browser-tab-BhTdeeZd.js +1 -0
  11. package/dist/web/assets/{c4Diagram-IC4MRINW-BNP2L9r_.js → c4Diagram-IC4MRINW-DzjR91sM.js} +1 -1
  12. package/dist/web/assets/channel-CKNZAqoN.js +1 -0
  13. package/dist/web/assets/chat-tab-ZiiUVOxM.js +7 -0
  14. package/dist/web/assets/{chunk-4BX2VUAB-BptTlTyl.js → chunk-4BX2VUAB-0YMkpW2S.js} +1 -1
  15. package/dist/web/assets/{chunk-55IACEB6-C4mUdyio.js → chunk-55IACEB6-Dp0pTM5r.js} +1 -1
  16. package/dist/web/assets/{chunk-7E7YKBS2-6xAQfBwa.js → chunk-7E7YKBS2-CuYKSUgJ.js} +1 -1
  17. package/dist/web/assets/{chunk-7R4GIKGN-DXaGAn_K.js → chunk-7R4GIKGN-DvbvLUIN.js} +2 -2
  18. package/dist/web/assets/{chunk-C72U2L5F-DOtEiN5f.js → chunk-C72U2L5F-CcEW1AMZ.js} +1 -1
  19. package/dist/web/assets/{chunk-EGIJ26TM-D0KJTa_T.js → chunk-EGIJ26TM-Cgt-qg75.js} +1 -1
  20. package/dist/web/assets/{chunk-FMBD7UC4-C_1aG0eb.js → chunk-FMBD7UC4-JCLgVcaC.js} +1 -1
  21. package/dist/web/assets/{chunk-GEFDOKGD-DwVPiYfW.js → chunk-GEFDOKGD-B82RP9ow.js} +1 -1
  22. package/dist/web/assets/chunk-GLR3WWYH-Bx2UL5jF.js +2 -0
  23. package/dist/web/assets/chunk-HHEYEP7N-BnRVfNc5.js +1 -0
  24. package/dist/web/assets/{chunk-JSJVCQXG-BSrqCL_3.js → chunk-JSJVCQXG-Pb-JMOgO.js} +1 -1
  25. package/dist/web/assets/{chunk-KX2RTZJC-BCxGmbzy.js → chunk-KX2RTZJC-BRj-ZEvL.js} +1 -1
  26. package/dist/web/assets/{chunk-KYZI473N-BKO5gMeU.js → chunk-KYZI473N-CBRPKraG.js} +1 -1
  27. package/dist/web/assets/{chunk-L3YUKLVL-3wBgkSvL.js → chunk-L3YUKLVL-DNFj84V6.js} +1 -1
  28. package/dist/web/assets/{chunk-MX3YWQON-BgjSEzus.js → chunk-MX3YWQON-BnPzQK-O.js} +1 -1
  29. package/dist/web/assets/{chunk-NQ4KR5QH-DLrZwBEm.js → chunk-NQ4KR5QH-BRj25yO7.js} +1 -1
  30. package/dist/web/assets/{chunk-O4XLMI2P-BurQy8tt.js → chunk-O4XLMI2P-BdXwVXjJ.js} +1 -1
  31. package/dist/web/assets/{chunk-OZEHJAEY-YTn24bGg.js → chunk-OZEHJAEY-LfXT4p8B.js} +1 -1
  32. package/dist/web/assets/{chunk-PQ6SQG4A-BxtUGYhW.js → chunk-PQ6SQG4A-EdgQyTqa.js} +1 -1
  33. package/dist/web/assets/{chunk-PU5JKC2W-B66ELkQm.js → chunk-PU5JKC2W-D3thuSok.js} +1 -1
  34. package/dist/web/assets/chunk-QZHKN3VN-gaBt0Rbd.js +1 -0
  35. package/dist/web/assets/{chunk-R5LLSJPH-euR2RxLN.js → chunk-R5LLSJPH-LdG7RqsM.js} +1 -1
  36. package/dist/web/assets/{chunk-WL4C6EOR-_2CBOJdI.js → chunk-WL4C6EOR-BHFnnXOt.js} +1 -1
  37. package/dist/web/assets/{chunk-XIRO2GV7-kqQ0g6wW.js → chunk-XIRO2GV7-DUmQrLsF.js} +1 -1
  38. package/dist/web/assets/{chunk-XPW4576I-CtcaMb09.js → chunk-XPW4576I-CsGTseUr.js} +1 -1
  39. package/dist/web/assets/{chunk-XZSTWKYB-BYxFzZwS.js → chunk-XZSTWKYB-5W2emiq4.js} +1 -1
  40. package/dist/web/assets/{chunk-YBOYWFTD-Dx_fX35n.js → chunk-YBOYWFTD-COdZIaX4.js} +1 -1
  41. package/dist/web/assets/classDiagram-VBA2DB6C-CqaIqYPn.js +1 -0
  42. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bo5WN2ok.js +1 -0
  43. package/dist/web/assets/clone-DNDy9Sms.js +1 -0
  44. package/dist/web/assets/{code-editor-D3VJc1tY.js → code-editor-BRMOypkX.js} +1 -1
  45. package/dist/web/assets/{cose-bilkent-S5V4N54A-CHHjH2dV.js → cose-bilkent-S5V4N54A-C1QJ6GPW.js} +1 -1
  46. package/dist/web/assets/{dagre-CNtSxiE_.js → dagre-CWo8w9wK.js} +1 -1
  47. package/dist/web/assets/{dagre-KLK3FWXG-ChenfPp1.js → dagre-KLK3FWXG-Br4t5TRV.js} +1 -1
  48. package/dist/web/assets/database-viewer-CEoDpzPz.js +1 -0
  49. package/dist/web/assets/{diagram-E7M64L7V-CzKYZM0Y.js → diagram-E7M64L7V-CkDC2uAj.js} +1 -1
  50. package/dist/web/assets/{diagram-IFDJBPK2-ChB_paPo.js → diagram-IFDJBPK2-NvhckwcA.js} +1 -1
  51. package/dist/web/assets/{diagram-P4PSJMXO-D1eW1dkL.js → diagram-P4PSJMXO--nUaNiyB.js} +1 -1
  52. package/dist/web/assets/{diff-viewer-D5vGZJnH.js → diff-viewer-jDU2bcGj.js} +1 -1
  53. package/dist/web/assets/{erDiagram-INFDFZHY-mCvUFSn6.js → erDiagram-INFDFZHY-DK4QEZYh.js} +1 -1
  54. package/dist/web/assets/{flowDiagram-PKNHOUZH-14ohZ1M1.js → flowDiagram-PKNHOUZH-B9h_Ba-v.js} +1 -1
  55. package/dist/web/assets/{ganttDiagram-A5KZAMGK-DIX0pLbk.js → ganttDiagram-A5KZAMGK-BVlftqyZ.js} +1 -1
  56. package/dist/web/assets/git-graph-DMQzw4Sp.js +1 -0
  57. package/dist/web/assets/gitGraph-HDMCJU4V-D5qEPjgs.js +1 -0
  58. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js → gitGraphDiagram-K3NZZRJ6-L7sj3Bs-.js} +1 -1
  59. package/dist/web/assets/{graphlib-DhOZxqsh.js → graphlib-BbbiUImY.js} +1 -1
  60. package/dist/web/assets/index-B4Iz1Wbi.css +2 -0
  61. package/dist/web/assets/index-QiSWS6f-.js +37 -0
  62. package/dist/web/assets/info-3K5VOQVL-CbpovIYU.js +1 -0
  63. package/dist/web/assets/infoDiagram-LFFYTUFH-DFh9c-S2.js +2 -0
  64. package/dist/web/assets/input-DGlv6gt_.js +41 -0
  65. package/dist/web/assets/{isEmpty-C0YYdhYj.js → isEmpty-DXomfd7J.js} +1 -1
  66. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-olazD6dZ.js → ishikawaDiagram-PHBUUO56-cW7SMLa_.js} +1 -1
  67. package/dist/web/assets/{journeyDiagram-4ABVD52K-CttDH9bb.js → journeyDiagram-4ABVD52K-DFQXUZsc.js} +1 -1
  68. package/dist/web/assets/{kanban-definition-K7BYSVSG-BBXbI37U.js → kanban-definition-K7BYSVSG-BMUhjxqj.js} +1 -1
  69. package/dist/web/assets/keybindings-store-BplH-yiN.js +1 -0
  70. package/dist/web/assets/{line-DBLLF7lH.js → line--xyfYP3x.js} +1 -1
  71. package/dist/web/assets/{linear-BLFWatDe.js → linear-BdqW7iQu.js} +1 -1
  72. package/dist/web/assets/{markdown-renderer-DcGMlbRm.js → markdown-renderer-BCjJbGP8.js} +5 -5
  73. package/dist/web/assets/{mermaid-parser.core-BKiGOTjR.js → mermaid-parser.core-BY8JfkE_.js} +2 -2
  74. package/dist/web/assets/{mindmap-definition-YRQLILUH-DoT7m4Sz.js → mindmap-definition-YRQLILUH-DIv-LMXG.js} +1 -1
  75. package/dist/web/assets/{ordinal-CCj7PWgZ.js → ordinal-CIoJK3nc.js} +1 -1
  76. package/dist/web/assets/packet-RMMSAZCW-BbzPU9BK.js +1 -0
  77. package/dist/web/assets/pie-UPGHQEXC-B0h6hM1j.js +1 -0
  78. package/dist/web/assets/{pieDiagram-SKSYHLDU-Bkh2E4zE.js → pieDiagram-SKSYHLDU-seSK40d1.js} +1 -1
  79. package/dist/web/assets/postgres-viewer-s0snZ9CL.js +1 -0
  80. package/dist/web/assets/{quadrantDiagram-337W2JSQ-B7zgALOL.js → quadrantDiagram-337W2JSQ-BaRFqlsA.js} +1 -1
  81. package/dist/web/assets/radar-KQ55EAFF-CHptMqVT.js +1 -0
  82. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-D_5GXNRo.js → requirementDiagram-Z7DCOOCP-1WWjMQB_.js} +1 -1
  83. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-BA9EFAAe.js → sankeyDiagram-WA2Y5GQK-DEGGYsk7.js} +1 -1
  84. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-fyWIrHiG.js → sequenceDiagram-2WXFIKYE-BtRvoUTC.js} +1 -1
  85. package/dist/web/assets/{settings-store-Bbhg_ptG.js → settings-store-D3dJqGhB.js} +2 -2
  86. package/dist/web/assets/settings-tab-2YkgmrY0.js +1 -0
  87. package/dist/web/assets/sqlite-viewer-B5GNwXaG.js +1 -0
  88. package/dist/web/assets/{stateDiagram-RAJIS63D-DfRBcaBu.js → stateDiagram-RAJIS63D-C16aO8tn.js} +1 -1
  89. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-D7qSAjnK.js +1 -0
  90. package/dist/web/assets/switch-mjGtIVDJ.js +1 -0
  91. package/dist/web/assets/{tab-store-dpsCvqhH.js → tab-store-DSz5PQI0.js} +1 -1
  92. package/dist/web/assets/{terminal-tab-DHMITI3S.js → terminal-tab-MRg8y1xF.js} +1 -1
  93. package/dist/web/assets/{timeline-definition-YZTLITO2-DYfwJ1jM.js → timeline-definition-YZTLITO2-DrjxCpEM.js} +1 -1
  94. package/dist/web/assets/treemap-KZPCXAKY-BL9OJq3X.js +1 -0
  95. package/dist/web/assets/{use-monaco-theme-DHbyUrzJ.js → use-monaco-theme-BQzvItNE.js} +1 -1
  96. package/dist/web/assets/{vennDiagram-LZ73GAT5-DqbKNRD9.js → vennDiagram-LZ73GAT5-DfYFnniI.js} +1 -1
  97. package/dist/web/assets/{xychartDiagram-JWTSCODW-DhUL86qT.js → xychartDiagram-JWTSCODW-BRvXOVlG.js} +1 -1
  98. package/dist/web/index.html +12 -10
  99. package/dist/web/sw.js +1 -1
  100. package/docs/code-standards.md +260 -7
  101. package/docs/codebase-summary.md +255 -95
  102. package/docs/project-changelog.md +88 -1
  103. package/docs/system-architecture.md +177 -12
  104. package/package.json +1 -1
  105. package/src/providers/claude-agent-sdk.ts +85 -212
  106. package/src/providers/cli-provider-base.ts +238 -0
  107. package/src/providers/cursor-cli/cursor-event-mapper.ts +85 -0
  108. package/src/providers/cursor-cli/cursor-history.ts +207 -0
  109. package/src/providers/cursor-cli/cursor-provider.ts +146 -0
  110. package/src/providers/mock-provider.ts +1 -1
  111. package/src/providers/provider.interface.ts +1 -0
  112. package/src/providers/registry.ts +43 -4
  113. package/src/server/index.ts +8 -0
  114. package/src/server/routes/browser-preview.ts +89 -0
  115. package/src/server/routes/chat.ts +14 -3
  116. package/src/server/routes/settings.ts +14 -0
  117. package/src/server/ws/chat.ts +91 -106
  118. package/src/services/chat.service.ts +10 -15
  119. package/src/types/api.ts +1 -1
  120. package/src/types/chat.ts +21 -4
  121. package/src/types/config.ts +33 -11
  122. package/src/utils/ndjson-line-parser.ts +36 -0
  123. package/src/web/components/browser/browser-tab.tsx +269 -0
  124. package/src/web/components/chat/chat-history-bar.tsx +49 -29
  125. package/src/web/components/chat/chat-tab.tsx +17 -5
  126. package/src/web/components/chat/message-input.tsx +94 -43
  127. package/src/web/components/chat/provider-selector.tsx +150 -0
  128. package/src/web/components/chat/session-picker.tsx +3 -1
  129. package/src/web/components/layout/command-palette.tsx +4 -0
  130. package/src/web/components/layout/editor-panel.tsx +1 -0
  131. package/src/web/components/layout/mobile-nav.tsx +2 -2
  132. package/src/web/components/layout/panel-layout.tsx +17 -1
  133. package/src/web/components/layout/tab-bar.tsx +2 -0
  134. package/src/web/components/layout/tab-content.tsx +5 -0
  135. package/src/web/components/settings/ai-settings-section.tsx +196 -137
  136. package/src/web/hooks/use-chat.ts +20 -21
  137. package/src/web/hooks/use-global-keybindings.ts +7 -0
  138. package/src/web/hooks/use-voice-input.ts +111 -0
  139. package/src/web/stores/keybindings-store.ts +1 -0
  140. package/src/web/stores/panel-store.ts +10 -10
  141. package/src/web/stores/tab-store.ts +2 -1
  142. package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +0 -1
  143. package/dist/web/assets/channel-w7yboq56.js +0 -1
  144. package/dist/web/assets/chat-tab-DxkvWelV.js +0 -7
  145. package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +0 -2
  146. package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +0 -1
  147. package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +0 -1
  148. package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +0 -1
  149. package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +0 -1
  150. package/dist/web/assets/clone-BSi6cgDh.js +0 -1
  151. package/dist/web/assets/database-viewer-qlwORhh0.js +0 -1
  152. package/dist/web/assets/git-graph-B2fHtKEc.js +0 -1
  153. package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +0 -1
  154. package/dist/web/assets/index-BAioKo_2.css +0 -2
  155. package/dist/web/assets/index-Ccq6zi2E.js +0 -37
  156. package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +0 -1
  157. package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +0 -2
  158. package/dist/web/assets/input-Brjz2Vv-.js +0 -41
  159. package/dist/web/assets/keybindings-store-e3pqlQbf.js +0 -1
  160. package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +0 -1
  161. package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +0 -1
  162. package/dist/web/assets/postgres-viewer-CZzbMFtb.js +0 -1
  163. package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +0 -1
  164. package/dist/web/assets/settings-tab-BOmLAhkD.js +0 -1
  165. package/dist/web/assets/sqlite-viewer-CrrzHXqq.js +0 -1
  166. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +0 -1
  167. package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +0 -1
  168. /package/dist/web/assets/{api-client-DpGMOZNf.js → api-client-icCZ-07C.js} +0 -0
  169. /package/dist/web/assets/{array-BGFCBI0e.js → array-CLwNaqU1.js} +0 -0
  170. /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-Bcg3QJBg.js} +0 -0
  171. /package/dist/web/assets/{cytoscape.esm-Ccan6xou.js → cytoscape.esm-B-QQuWwK.js} +0 -0
  172. /package/dist/web/assets/{defaultLocale-CRZydyG6.js → defaultLocale-D_VMtRaY.js} +0 -0
  173. /package/dist/web/assets/{dist-T0Vhi0Mh.js → dist-CMmNEgEP.js} +0 -0
  174. /package/dist/web/assets/{dist-Cce3efmT.js → dist-Ckxnw5rl.js} +0 -0
  175. /package/dist/web/assets/{init-B8gtcn7T.js → init-vVpfz1D6.js} +0 -0
  176. /package/dist/web/assets/{isArrayLikeObject-B4pdpV8V.js → isArrayLikeObject-DvHDmeBe.js} +0 -0
  177. /package/dist/web/assets/{katex-Bbu770d9.js → katex-C3cZrCvP.js} +0 -0
  178. /package/dist/web/assets/{math-DwgHI-Cu.js → math-a44lmFDa.js} +0 -0
  179. /package/dist/web/assets/{path-DZF-JdEe.js → path-CuyvWNAH.js} +0 -0
  180. /package/dist/web/assets/{preload-helper-qlgyTAkD.js → preload-helper-CsoeaaUJ.js} +0 -0
  181. /package/dist/web/assets/{react-BGf7KNLk.js → react-BPIfZRKM.js} +0 -0
  182. /package/dist/web/assets/{rough.esm-VLpapkIG.js → rough.esm-c4PR5shF.js} +0 -0
  183. /package/dist/web/assets/{src-BoSBNdA_.js → src-CLWraeNW.js} +0 -0
  184. /package/dist/web/assets/{table-Yo02WRH-.js → table-C9jDaRl2.js} +0 -0
  185. /package/dist/web/assets/{tag-CaC1ng2E.js → tag-CENGyt_L.js} +0 -0
  186. /package/dist/web/assets/{utils-btZ8C8-R.js → utils-Bslrbb-G.js} +0 -0
@@ -0,0 +1,238 @@
1
+ import type {
2
+ AIProvider,
3
+ Session,
4
+ SessionConfig,
5
+ SessionInfo,
6
+ ChatEvent,
7
+ SendMessageOpts,
8
+ } from "./provider.interface.ts";
9
+ import { spawn, type ChildProcess } from "node:child_process";
10
+ import { parseNdjsonLines } from "../utils/ndjson-line-parser.ts";
11
+ import { configService } from "../services/config.service.ts";
12
+
13
+ /**
14
+ * Abstract base class for CLI-spawning AI providers.
15
+ * Handles process lifecycle, NDJSON streaming, abort, and cleanup.
16
+ * Subclasses only implement event mapping + arg building.
17
+ */
18
+ export abstract class CliProvider implements AIProvider {
19
+ abstract readonly id: string;
20
+ abstract readonly name: string;
21
+ abstract readonly cliCommand: string;
22
+
23
+ /** Build CLI args for a given message/session */
24
+ abstract buildArgs(params: {
25
+ sessionId?: string;
26
+ message: string;
27
+ model?: string;
28
+ permissionMode?: string;
29
+ isResume: boolean;
30
+ }): string[];
31
+
32
+ /** Map a raw JSON object from CLI stdout → ChatEvent[] */
33
+ abstract mapEvent(raw: unknown, sessionId: string): ChatEvent[];
34
+
35
+ /** Extract session ID from CLI init event (provider-specific) */
36
+ abstract extractSessionId(raw: unknown): string | null;
37
+
38
+ /** Check if CLI binary exists on this machine */
39
+ abstract isAvailable(): Promise<boolean>;
40
+
41
+ // --- Shared state ---
42
+ protected sessions = new Map<string, Session>();
43
+ protected activeProcesses = new Map<string, ChildProcess>();
44
+ private messageCount = new Map<string, number>();
45
+
46
+ // --- Session lifecycle ---
47
+
48
+ async createSession(config: SessionConfig): Promise<Session> {
49
+ const id = crypto.randomUUID();
50
+ const session: Session = {
51
+ id,
52
+ providerId: this.id,
53
+ title: config.title ?? "New Chat",
54
+ projectName: config.projectName,
55
+ projectPath: config.projectPath,
56
+ createdAt: new Date().toISOString(),
57
+ };
58
+ this.sessions.set(id, session);
59
+ this.messageCount.set(id, 0);
60
+ return session;
61
+ }
62
+
63
+ async resumeSession(sessionId: string): Promise<Session> {
64
+ const existing = this.sessions.get(sessionId);
65
+ if (existing) return existing;
66
+ const session: Session = {
67
+ id: sessionId,
68
+ providerId: this.id,
69
+ title: "Resumed Chat",
70
+ createdAt: new Date().toISOString(),
71
+ };
72
+ this.sessions.set(sessionId, session);
73
+ this.messageCount.set(sessionId, 1);
74
+ return session;
75
+ }
76
+
77
+ async listSessions(): Promise<SessionInfo[]> {
78
+ return Array.from(this.sessions.values()).map((s) => ({
79
+ id: s.id,
80
+ providerId: s.providerId,
81
+ title: s.title,
82
+ projectName: s.projectName,
83
+ createdAt: s.createdAt,
84
+ }));
85
+ }
86
+
87
+ async deleteSession(sessionId: string): Promise<void> {
88
+ this.sessions.delete(sessionId);
89
+ this.messageCount.delete(sessionId);
90
+ }
91
+
92
+ // --- Streaming ---
93
+
94
+ async *sendMessage(
95
+ sessionId: string,
96
+ message: string,
97
+ opts?: SendMessageOpts,
98
+ ): AsyncIterable<ChatEvent> {
99
+ if (!this.sessions.has(sessionId)) {
100
+ await this.resumeSession(sessionId);
101
+ }
102
+ const meta = this.sessions.get(sessionId)!;
103
+
104
+ if (meta.title === "New Chat") {
105
+ meta.title = message.slice(0, 50) + (message.length > 50 ? "..." : "");
106
+ }
107
+
108
+ const count = this.messageCount.get(sessionId) ?? 0;
109
+ const isResume = count > 0;
110
+ this.messageCount.set(sessionId, count + 1);
111
+
112
+ const config = this.getProviderConfig();
113
+ const args = this.buildArgs({
114
+ sessionId: isResume ? sessionId : undefined,
115
+ message,
116
+ model: config?.model,
117
+ permissionMode: opts?.permissionMode || config?.permission_mode,
118
+ isResume,
119
+ });
120
+
121
+ const cwd = meta.projectPath || process.cwd();
122
+ let capturedSessionId = isResume ? sessionId : null;
123
+
124
+ const proc = this.spawnProcess(args, cwd);
125
+ const processKey = sessionId;
126
+ this.activeProcesses.set(processKey, proc);
127
+
128
+ try {
129
+ for await (const raw of parseNdjsonLines(proc.stdout!)) {
130
+ if (!capturedSessionId) {
131
+ const extracted = this.extractSessionId(raw);
132
+ if (extracted) {
133
+ capturedSessionId = extracted;
134
+ if (capturedSessionId !== processKey) {
135
+ this.activeProcesses.delete(processKey);
136
+ this.activeProcesses.set(capturedSessionId, proc);
137
+ // Migrate session metadata to the real CLI-assigned ID
138
+ // so listSessions/getMessages use the correct key
139
+ const meta = this.sessions.get(processKey);
140
+ if (meta) {
141
+ this.sessions.delete(processKey);
142
+ meta.id = capturedSessionId;
143
+ this.sessions.set(capturedSessionId, meta);
144
+ }
145
+ const cnt = this.messageCount.get(processKey) ?? 0;
146
+ this.messageCount.delete(processKey);
147
+ this.messageCount.set(capturedSessionId, cnt);
148
+ // Notify frontend about the session ID change
149
+ yield { type: "session_migrated", oldSessionId: processKey, newSessionId: capturedSessionId } as ChatEvent;
150
+ }
151
+ }
152
+ }
153
+
154
+ const events = this.mapEvent(raw, capturedSessionId || sessionId);
155
+ for (const event of events) {
156
+ yield event;
157
+ }
158
+ }
159
+
160
+ const exitCode = await waitForExit(proc);
161
+ yield {
162
+ type: "done",
163
+ sessionId: capturedSessionId || sessionId,
164
+ resultSubtype: exitCode === 0 ? "success" : "error_during_execution",
165
+ };
166
+ } catch (err) {
167
+ yield {
168
+ type: "error",
169
+ message: err instanceof Error ? err.message : String(err),
170
+ };
171
+ yield {
172
+ type: "done",
173
+ sessionId: capturedSessionId || sessionId,
174
+ resultSubtype: "error_during_execution",
175
+ };
176
+ } finally {
177
+ this.activeProcesses.delete(capturedSessionId || processKey);
178
+ }
179
+ }
180
+
181
+ // --- Abort ---
182
+
183
+ abortQuery(sessionId: string): void {
184
+ const proc = this.activeProcesses.get(sessionId);
185
+ if (!proc) return;
186
+ console.log(`[${this.id}] Aborting session: ${sessionId}`);
187
+ proc.kill("SIGTERM");
188
+ setTimeout(() => {
189
+ try { proc.kill("SIGKILL"); } catch { /* already dead */ }
190
+ }, 2000);
191
+ this.activeProcesses.delete(sessionId);
192
+ }
193
+
194
+ // --- Helpers ---
195
+
196
+ protected spawnProcess(args: string[], cwd: string): ChildProcess {
197
+ console.log(`[${this.id}] spawn: ${this.cliCommand} ${args.join(" ")} (cwd=${cwd})`);
198
+ const proc = spawn(this.cliCommand, args, {
199
+ cwd,
200
+ stdio: ["pipe", "pipe", "pipe"],
201
+ env: { ...process.env },
202
+ });
203
+ proc.stdin?.end();
204
+
205
+ proc.stderr?.on("data", (data: Buffer) => {
206
+ const text = data.toString().trim();
207
+ if (text) console.error(`[${this.id}] stderr: ${text}`);
208
+ });
209
+
210
+ return proc;
211
+ }
212
+
213
+ /** Read provider config from PPM settings */
214
+ protected getProviderConfig() {
215
+ try {
216
+ const ai = configService.get("ai");
217
+ return ai.providers[this.id] ?? null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ /** Kill all active processes (cleanup on server start) */
224
+ cleanupAll(): void {
225
+ for (const [sessionId, proc] of this.activeProcesses) {
226
+ console.log(`[${this.id}] cleanup: killing orphaned process for session ${sessionId}`);
227
+ try { proc.kill("SIGTERM"); } catch { /* ignore */ }
228
+ }
229
+ this.activeProcesses.clear();
230
+ }
231
+ }
232
+
233
+ function waitForExit(proc: ChildProcess): Promise<number> {
234
+ return new Promise((resolve, reject) => {
235
+ proc.on("close", (code) => resolve(code ?? 1));
236
+ proc.on("error", (err) => reject(err));
237
+ });
238
+ }
@@ -0,0 +1,85 @@
1
+ import type { ChatEvent } from "../provider.interface.ts";
2
+
3
+ interface CursorRawEvent {
4
+ type: string;
5
+ subtype?: string;
6
+ session_id?: string;
7
+ model?: string;
8
+ message?: {
9
+ content?: Array<{
10
+ type?: string;
11
+ text?: string;
12
+ toolName?: string;
13
+ name?: string;
14
+ args?: unknown;
15
+ input?: unknown;
16
+ toolCallId?: string;
17
+ id?: string;
18
+ }>;
19
+ };
20
+ result?: string;
21
+ }
22
+
23
+ /**
24
+ * Map a single Cursor NDJSON line → ChatEvent[].
25
+ * Returns empty array for events we don't care about.
26
+ */
27
+ export function mapCursorEvent(raw: unknown, sessionId: string): ChatEvent[] {
28
+ const event = raw as CursorRawEvent;
29
+ if (!event?.type) return [];
30
+
31
+ switch (event.type) {
32
+ case "system":
33
+ if (event.subtype === "init") {
34
+ return [{ type: "system", subtype: "init" }];
35
+ }
36
+ return [];
37
+
38
+ case "user":
39
+ return [];
40
+
41
+ case "assistant": {
42
+ const events: ChatEvent[] = [];
43
+ const content = event.message?.content;
44
+ if (!Array.isArray(content)) return [];
45
+
46
+ for (const part of content) {
47
+ if (!part) continue;
48
+
49
+ if (part.type === "text" && part.text) {
50
+ events.push({ type: "text", content: part.text });
51
+ }
52
+
53
+ if (part.type === "reasoning" && part.text) {
54
+ events.push({ type: "thinking", content: part.text });
55
+ }
56
+
57
+ if (part.type === "tool-call" || part.type === "tool_use") {
58
+ const toolName = normalizeToolName(part.toolName || part.name || "Unknown");
59
+ const toolId = part.toolCallId || part.id || crypto.randomUUID();
60
+ events.push({
61
+ type: "tool_use",
62
+ tool: toolName,
63
+ input: part.args || part.input || {},
64
+ toolUseId: toolId,
65
+ });
66
+ }
67
+ }
68
+ return events;
69
+ }
70
+
71
+ case "result":
72
+ return [];
73
+
74
+ default:
75
+ return [];
76
+ }
77
+ }
78
+
79
+ /** Normalize Cursor tool names to PPM standard */
80
+ function normalizeToolName(name: string): string {
81
+ switch (name) {
82
+ case "ApplyPatch": return "Edit";
83
+ default: return name;
84
+ }
85
+ }
@@ -0,0 +1,207 @@
1
+ import { createHash } from "node:crypto";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { existsSync, readdirSync } from "node:fs";
5
+ import type { ChatMessage, SessionInfo } from "../provider.interface.ts";
6
+
7
+ const DEFAULT_CHATS_DIR = join(homedir(), ".cursor", "chats");
8
+
9
+ /**
10
+ * List all Cursor sessions found in ~/.cursor/chats/.
11
+ * Scans directory structure: {cwdHash}/{sessionId}/store.db
12
+ * Reads meta table for session name and createdAt.
13
+ * @param chatsDir — override for testing (defaults to ~/.cursor/chats)
14
+ */
15
+ export async function listCursorSessions(providerId: string, chatsDir?: string): Promise<SessionInfo[]> {
16
+ const dir = chatsDir ?? DEFAULT_CHATS_DIR;
17
+ if (!existsSync(dir)) return [];
18
+ const { Database } = await import("bun:sqlite");
19
+ const sessions: SessionInfo[] = [];
20
+
21
+ try {
22
+ for (const cwdHash of readdirSync(dir)) {
23
+ const cwdDir = join(dir, cwdHash);
24
+ try {
25
+ for (const sessionId of readdirSync(cwdDir)) {
26
+ const dbPath = join(cwdDir, sessionId, "store.db");
27
+ if (!existsSync(dbPath)) continue;
28
+
29
+ let title = `Cursor ${sessionId.slice(0, 8)}`;
30
+ let createdAt = new Date().toISOString();
31
+
32
+ // Read meta table for name + createdAt (value is hex-encoded JSON)
33
+ try {
34
+ const db = new Database(dbPath, { readonly: true });
35
+ const row = db.query("SELECT value FROM meta LIMIT 1").get() as { value: string | Buffer } | null;
36
+ db.close();
37
+ if (row?.value) {
38
+ const hex = typeof row.value === "string" ? row.value : Buffer.from(row.value).toString("utf-8");
39
+ const json = Buffer.from(hex, "hex").toString("utf-8");
40
+ const meta = JSON.parse(json);
41
+ if (meta.name) title = meta.name.split("\n")[0].slice(0, 80);
42
+ if (meta.createdAt) createdAt = new Date(meta.createdAt).toISOString();
43
+ }
44
+ } catch { /* use defaults */ }
45
+
46
+ sessions.push({ id: sessionId, providerId, title, createdAt });
47
+ }
48
+ } catch { /* skip unreadable dir */ }
49
+ }
50
+ } catch { /* skip if chats dir unreadable */ }
51
+
52
+ return sessions.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
53
+ }
54
+
55
+ /**
56
+ * Load chat history from Cursor's SQLite DAG storage.
57
+ * Path: ~/.cursor/chats/{MD5(cwd)}/{sessionId}/store.db
58
+ * Falls back to scanning all cwdHash dirs if projectPath doesn't match.
59
+ */
60
+ export async function loadCursorHistory(
61
+ sessionId: string,
62
+ projectPath?: string,
63
+ chatsDir?: string,
64
+ ): Promise<ChatMessage[]> {
65
+ const baseDir = chatsDir ?? DEFAULT_CHATS_DIR;
66
+ let dbPath: string | null = null;
67
+
68
+ // Try direct path first (fast path when projectPath is known)
69
+ if (projectPath) {
70
+ const cwdHash = createHash("md5").update(projectPath).digest("hex");
71
+ const candidate = join(baseDir, cwdHash, sessionId, "store.db");
72
+ if (existsSync(candidate)) dbPath = candidate;
73
+ }
74
+
75
+ // Fallback: scan all cwdHash dirs for this sessionId
76
+ if (!dbPath && existsSync(baseDir)) {
77
+ try {
78
+ for (const cwdHash of readdirSync(baseDir)) {
79
+ const candidate = join(baseDir, cwdHash, sessionId, "store.db");
80
+ if (existsSync(candidate)) { dbPath = candidate; break; }
81
+ }
82
+ } catch { /* skip */ }
83
+ }
84
+
85
+ if (!dbPath) return [];
86
+
87
+ try {
88
+ // Use Bun's native SQLite
89
+ const { Database } = await import("bun:sqlite");
90
+ const db = new Database(dbPath, { readonly: true });
91
+
92
+ const blobs = db
93
+ .query("SELECT rowid, id, data FROM blobs ORDER BY rowid")
94
+ .all() as Array<{ rowid: number; id: string; data: Buffer }>;
95
+ db.close();
96
+
97
+ return parseDagBlobs(blobs, sessionId);
98
+ } catch (err) {
99
+ console.warn(`[cursor-history] Failed to load session ${sessionId}:`, err);
100
+ return [];
101
+ }
102
+ }
103
+
104
+ /** Parse DAG blobs into ordered ChatMessages */
105
+ function parseDagBlobs(
106
+ blobs: Array<{ rowid: number; id: string; data: Buffer }>,
107
+ _sessionId: string,
108
+ ): ChatMessage[] {
109
+ const messages: ChatMessage[] = [];
110
+
111
+ for (const blob of blobs) {
112
+ try {
113
+ const text = extractTextContent(blob.data);
114
+ if (!text) continue;
115
+
116
+ // Try to parse as JSON
117
+ try {
118
+ const parsed = JSON.parse(text);
119
+
120
+ // Format: { role: "...", content: "..." | [...] } — structured message
121
+ // Skip system prompts — they're huge and not useful for history
122
+ if (parsed.role === "system") continue;
123
+ if (parsed.role && parsed.content) {
124
+ let content: string;
125
+ if (typeof parsed.content === "string") {
126
+ content = parsed.content;
127
+ } else if (Array.isArray(parsed.content)) {
128
+ // Content parts: [{ type: "text", text: "..." }, ...]
129
+ content = parsed.content
130
+ .filter((p: any) => p.type === "text" && p.text)
131
+ .map((p: any) => p.text)
132
+ .join("\n") || JSON.stringify(parsed.content);
133
+ } else {
134
+ content = JSON.stringify(parsed.content);
135
+ }
136
+ // Filter out Cursor's injected <user_info> system context messages
137
+ if (parsed.role === "user" && /^<user_info>\s/i.test(content.trimStart())) continue;
138
+ // Strip Cursor's <user_query> wrapper from user messages
139
+ if (parsed.role === "user") {
140
+ const match = content.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/);
141
+ if (match?.[1]) content = match[1];
142
+ }
143
+ messages.push({
144
+ id: blob.id,
145
+ role: parsed.role,
146
+ content,
147
+ timestamp: new Date().toISOString(),
148
+ });
149
+ continue;
150
+ }
151
+
152
+ // Format: [{ type: "text", text: "..." }] — content parts array
153
+ if (Array.isArray(parsed)) {
154
+ const textParts = parsed
155
+ .filter((p: any) => p.type === "text" && p.text)
156
+ .map((p: any) => p.text)
157
+ .join("\n");
158
+ if (textParts) {
159
+ messages.push({
160
+ id: blob.id,
161
+ role: messages.length % 2 === 0 ? "user" : "assistant",
162
+ content: textParts,
163
+ timestamp: new Date().toISOString(),
164
+ });
165
+ continue;
166
+ }
167
+ }
168
+ } catch { /* not JSON */ }
169
+ } catch { /* skip corrupt blob */ }
170
+ }
171
+
172
+ return messages;
173
+ }
174
+
175
+ /**
176
+ * Extract readable text from a DAG blob.
177
+ * Handles 2 known formats:
178
+ * 1. UTF-8 JSON string starting with { or [ (role/content messages)
179
+ * 2. JSON array (content parts like [{type:"text",text:"..."}])
180
+ * Skips binary DAG metadata blobs (parent refs, headers).
181
+ */
182
+ function extractTextContent(data: Buffer | Uint8Array): string | null {
183
+ if (!data || data.length === 0) return null;
184
+
185
+ const buf = Buffer.from(data);
186
+
187
+ // Quick binary check: if first byte is not printable ASCII, it's a DAG metadata blob
188
+ const firstByte = buf[0];
189
+ if (firstByte !== undefined && firstByte < 0x20 && firstByte !== 0x0a && firstByte !== 0x0d && firstByte !== 0x09) {
190
+ return null;
191
+ }
192
+
193
+ const text = buf.toString("utf-8");
194
+
195
+ // Only accept clean JSON starting with { or [
196
+ if (text.startsWith("{") || text.startsWith("[")) {
197
+ // Validate it's actually parseable JSON
198
+ try {
199
+ JSON.parse(text);
200
+ return text;
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+
206
+ return null;
207
+ }
@@ -0,0 +1,146 @@
1
+ import { CliProvider } from "../cli-provider-base.ts";
2
+ import { mapCursorEvent } from "./cursor-event-mapper.ts";
3
+ import { listCursorSessions, loadCursorHistory } from "./cursor-history.ts";
4
+ import type { ChatEvent, ChatMessage, SessionInfo, ModelOption } from "../provider.interface.ts";
5
+ import type { ChildProcess } from "node:child_process";
6
+
7
+ const TRUST_PATTERNS = [
8
+ /workspace trust required/i,
9
+ /do you trust the contents/i,
10
+ /pass --trust/i,
11
+ ];
12
+
13
+ /**
14
+ * Cursor CLI provider — spawns `cursor-agent` with NDJSON streaming.
15
+ * Extends CliProvider with Cursor-specific event mapping, arg building,
16
+ * workspace trust auto-retry, and SQLite DAG history.
17
+ */
18
+ export class CursorCliProvider extends CliProvider {
19
+ readonly id = "cursor";
20
+ readonly name = "Cursor";
21
+ readonly cliCommand = "cursor-agent";
22
+
23
+ async isAvailable(): Promise<boolean> {
24
+ try {
25
+ const cmd = process.platform === "win32" ? "where" : "which";
26
+ const proc = Bun.spawn([cmd, "cursor-agent"], { stdout: "pipe", stderr: "pipe" });
27
+ await proc.exited;
28
+ return proc.exitCode === 0;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ buildArgs(params: {
35
+ sessionId?: string;
36
+ message: string;
37
+ model?: string;
38
+ permissionMode?: string;
39
+ isResume: boolean;
40
+ }): string[] {
41
+ const args: string[] = [];
42
+
43
+ if (params.sessionId && params.isResume) {
44
+ args.push(`--resume=${params.sessionId}`);
45
+ }
46
+
47
+ args.push("-p", params.message);
48
+
49
+ if (!params.isResume && params.model) {
50
+ args.push("--model", params.model);
51
+ }
52
+
53
+ args.push("--output-format", "stream-json");
54
+
55
+ // Permission mode → CLI flags
56
+ const mode = params.permissionMode || "default";
57
+ if (mode === "bypassPermissions") {
58
+ args.push("-f");
59
+ }
60
+
61
+ return args;
62
+ }
63
+
64
+ mapEvent(raw: unknown, sessionId: string): ChatEvent[] {
65
+ return mapCursorEvent(raw, sessionId);
66
+ }
67
+
68
+ extractSessionId(raw: unknown): string | null {
69
+ const obj = raw as Record<string, unknown>;
70
+ if (obj?.type === "system" && obj?.subtype === "init") {
71
+ return (obj.session_id as string) || null;
72
+ }
73
+ return null;
74
+ }
75
+
76
+ // Override listSessions to include Cursor's native history
77
+ override async listSessions(): Promise<SessionInfo[]> {
78
+ const inMemory = await super.listSessions();
79
+ try {
80
+ const native = await listCursorSessions(this.id);
81
+ // Merge: in-memory first, then native (deduplicated)
82
+ const seen = new Set(inMemory.map((s) => s.id));
83
+ const merged = [...inMemory];
84
+ for (const s of native) {
85
+ if (!seen.has(s.id)) merged.push(s);
86
+ }
87
+ return merged;
88
+ } catch {
89
+ return inMemory;
90
+ }
91
+ }
92
+
93
+ // Optional: load history from SQLite
94
+ async getMessages(sessionId: string): Promise<ChatMessage[]> {
95
+ const meta = this.sessions.get(sessionId);
96
+ return loadCursorHistory(sessionId, meta?.projectPath);
97
+ }
98
+
99
+ /** Cached models list with TTL from `cursor-agent --list-models` */
100
+ private modelsCache: { models: ModelOption[]; expiry: number } | null = null;
101
+ private static CACHE_TTL = 5 * 60 * 1000; // 5 min
102
+
103
+ async listModels(): Promise<ModelOption[]> {
104
+ if (this.modelsCache && Date.now() < this.modelsCache.expiry) {
105
+ return this.modelsCache.models;
106
+ }
107
+ try {
108
+ const proc = Bun.spawn(["cursor-agent", "--list-models"], {
109
+ stdout: "pipe",
110
+ stderr: "pipe",
111
+ });
112
+ const timeout = setTimeout(() => proc.kill(), 10_000);
113
+ const text = await new Response(proc.stdout).text();
114
+ clearTimeout(timeout);
115
+ await proc.exited;
116
+ const models: ModelOption[] = [];
117
+ for (const line of text.split("\n")) {
118
+ // Format: "model-id - Model Label" or "model-id - Model Label (current, default)"
119
+ const match = line.match(/^(\S+)\s+-\s+(.+?)(?:\s+\(.*\))?$/);
120
+ if (match?.[1] && match[2]) {
121
+ models.push({ value: match[1], label: match[2].trim() });
122
+ }
123
+ }
124
+ if (models.length > 0) {
125
+ this.modelsCache = { models, expiry: Date.now() + CursorCliProvider.CACHE_TTL };
126
+ }
127
+ return models;
128
+ } catch {
129
+ return [];
130
+ }
131
+ }
132
+
133
+ // Workspace trust detection: log warning so user knows to re-run with --trust
134
+ protected override spawnProcess(args: string[], cwd: string): ChildProcess {
135
+ const proc = super.spawnProcess(args, cwd);
136
+
137
+ proc.stderr?.on("data", (data: Buffer) => {
138
+ const text = data.toString();
139
+ if (TRUST_PATTERNS.some((p) => p.test(text))) {
140
+ console.warn("[cursor] Workspace trust prompt detected. Re-run with bypassPermissions mode or add --trust flag.");
141
+ }
142
+ });
143
+
144
+ return proc;
145
+ }
146
+ }
@@ -167,7 +167,7 @@ export class MockProvider implements AIProvider {
167
167
  }
168
168
  }
169
169
 
170
- getMessages(sessionId: string): ChatMessage[] {
170
+ async getMessages(sessionId: string): Promise<ChatMessage[]> {
171
171
  return this.messageHistory.get(sessionId) ?? [];
172
172
  }
173
173
  }
@@ -8,4 +8,5 @@ export type {
8
8
  ToolApprovalHandler,
9
9
  UsageInfo,
10
10
  SendMessageOpts,
11
+ ModelOption,
11
12
  } from "../types/chat.ts";