@juspay/shooter 1.15.0 → 1.17.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 (171) hide show
  1. package/.claude/hooks/codex-hooks.example.json +75 -0
  2. package/.claude/hooks/notifier.cjs +158 -8
  3. package/build/client/_app/immutable/assets/{0.BZLcOr5z.css → 0.B0O0vCnX.css} +1 -1
  4. package/build/client/_app/immutable/assets/0.B0O0vCnX.css.br +0 -0
  5. package/build/client/_app/immutable/assets/0.B0O0vCnX.css.gz +0 -0
  6. package/build/client/_app/immutable/chunks/{X-tVU_3P.js → BctvtE4d.js} +1 -1
  7. package/build/client/_app/immutable/chunks/BctvtE4d.js.br +0 -0
  8. package/build/client/_app/immutable/chunks/BctvtE4d.js.gz +0 -0
  9. package/build/client/_app/immutable/chunks/BxFShcQO.js +1 -0
  10. package/build/client/_app/immutable/chunks/BxFShcQO.js.br +0 -0
  11. package/build/client/_app/immutable/chunks/BxFShcQO.js.gz +0 -0
  12. package/build/client/_app/immutable/chunks/{gxvWeAns.js → ByzqAuXw.js} +1 -1
  13. package/build/client/_app/immutable/chunks/ByzqAuXw.js.br +0 -0
  14. package/build/client/_app/immutable/chunks/ByzqAuXw.js.gz +0 -0
  15. package/build/client/_app/immutable/chunks/{pMo6RVvN.js → CjfxuHdN.js} +1 -1
  16. package/build/client/_app/immutable/chunks/CjfxuHdN.js.br +0 -0
  17. package/build/client/_app/immutable/chunks/CjfxuHdN.js.gz +0 -0
  18. package/build/client/_app/immutable/entry/{app.B0PrrcUG.js → app.CNaTe-zm.js} +2 -2
  19. package/build/client/_app/immutable/entry/app.CNaTe-zm.js.br +0 -0
  20. package/build/client/_app/immutable/entry/app.CNaTe-zm.js.gz +0 -0
  21. package/build/client/_app/immutable/entry/start.hxYnjcDu.js +1 -0
  22. package/build/client/_app/immutable/entry/start.hxYnjcDu.js.br +0 -0
  23. package/build/client/_app/immutable/entry/start.hxYnjcDu.js.gz +0 -0
  24. package/build/client/_app/immutable/nodes/{0.D4GLHqPM.js → 0.C3ELOf4c.js} +1 -1
  25. package/build/client/_app/immutable/nodes/0.C3ELOf4c.js.br +0 -0
  26. package/build/client/_app/immutable/nodes/0.C3ELOf4c.js.gz +0 -0
  27. package/build/client/_app/immutable/nodes/{1.nJde5z5O.js → 1.Fqso94b3.js} +1 -1
  28. package/build/client/_app/immutable/nodes/1.Fqso94b3.js.br +0 -0
  29. package/build/client/_app/immutable/nodes/1.Fqso94b3.js.gz +0 -0
  30. package/build/client/_app/immutable/nodes/{2.CLtsjLeG.js → 2.BusCVJWk.js} +1 -1
  31. package/build/client/_app/immutable/nodes/2.BusCVJWk.js.br +0 -0
  32. package/build/client/_app/immutable/nodes/2.BusCVJWk.js.gz +0 -0
  33. package/build/client/_app/immutable/nodes/{3.CKTUHtnx.js → 3.DUlpocIc.js} +1 -1
  34. package/build/client/_app/immutable/nodes/3.DUlpocIc.js.br +0 -0
  35. package/build/client/_app/immutable/nodes/3.DUlpocIc.js.gz +0 -0
  36. package/build/client/_app/immutable/nodes/6.CG4eKRH0.js +1 -0
  37. package/build/client/_app/immutable/nodes/6.CG4eKRH0.js.br +0 -0
  38. package/build/client/_app/immutable/nodes/6.CG4eKRH0.js.gz +0 -0
  39. package/build/client/_app/immutable/nodes/7.DHilxD1o.js +4 -0
  40. package/build/client/_app/immutable/nodes/7.DHilxD1o.js.br +0 -0
  41. package/build/client/_app/immutable/nodes/7.DHilxD1o.js.gz +0 -0
  42. package/build/client/_app/immutable/nodes/8.BjKgvSie.js +2 -0
  43. package/build/client/_app/immutable/nodes/8.BjKgvSie.js.br +0 -0
  44. package/build/client/_app/immutable/nodes/8.BjKgvSie.js.gz +0 -0
  45. package/build/client/_app/immutable/nodes/9.BRT6HOXB.js +2 -0
  46. package/build/client/_app/immutable/nodes/9.BRT6HOXB.js.br +0 -0
  47. package/build/client/_app/immutable/nodes/9.BRT6HOXB.js.gz +0 -0
  48. package/build/client/_app/version.json +1 -1
  49. package/build/client/_app/version.json.br +0 -0
  50. package/build/client/_app/version.json.gz +0 -0
  51. package/build/server/chunks/{0-BUSWGJr9.js → 0-BWFSL107.js} +3 -3
  52. package/build/server/chunks/{0-BUSWGJr9.js.map → 0-BWFSL107.js.map} +1 -1
  53. package/build/server/chunks/{1-DjiQE1K0.js → 1-Bw5KlAjL.js} +2 -2
  54. package/build/server/chunks/{1-DjiQE1K0.js.map → 1-Bw5KlAjL.js.map} +1 -1
  55. package/build/server/chunks/{2-ThgVrRKa.js → 2-CQ3yYSVK.js} +2 -2
  56. package/build/server/chunks/{2-ThgVrRKa.js.map → 2-CQ3yYSVK.js.map} +1 -1
  57. package/build/server/chunks/{3-G5LiDFQ9.js → 3-DZ4H9hPs.js} +2 -2
  58. package/build/server/chunks/{3-G5LiDFQ9.js.map → 3-DZ4H9hPs.js.map} +1 -1
  59. package/build/server/chunks/{6--I7fF3Bx.js → 6-BZ0enR6b.js} +2 -2
  60. package/build/server/chunks/6-BZ0enR6b.js.map +1 -0
  61. package/build/server/chunks/{7-BwPLVwOR.js → 7-Lg8imTZn.js} +2 -2
  62. package/build/server/chunks/7-Lg8imTZn.js.map +1 -0
  63. package/build/server/chunks/{8-BwOMHaoQ.js → 8-DKs4yOL7.js} +2 -2
  64. package/build/server/chunks/8-DKs4yOL7.js.map +1 -0
  65. package/build/server/chunks/{9-DkO6aJIB.js → 9-UNmpUWDY.js} +2 -2
  66. package/build/server/chunks/9-UNmpUWDY.js.map +1 -0
  67. package/build/server/chunks/{_server.ts-BuYyCrnF.js → _server.ts-5wx4ZppI.js} +4 -3
  68. package/build/server/chunks/_server.ts-5wx4ZppI.js.map +1 -0
  69. package/build/server/chunks/{_server.ts-40c_epk8.js → _server.ts-B1z0q6qZ.js} +10 -8
  70. package/build/server/chunks/_server.ts-B1z0q6qZ.js.map +1 -0
  71. package/build/server/chunks/{_server.ts-ByPExYfO.js → _server.ts-BLNDdFWC.js} +3 -3
  72. package/build/server/chunks/_server.ts-BLNDdFWC.js.map +1 -0
  73. package/build/server/chunks/_server.ts-BMMTS86y.js +82 -0
  74. package/build/server/chunks/_server.ts-BMMTS86y.js.map +1 -0
  75. package/build/server/chunks/{_server.ts-CjpQ10xh.js → _server.ts-Bt7EAfjo.js} +50 -2
  76. package/build/server/chunks/_server.ts-Bt7EAfjo.js.map +1 -0
  77. package/build/server/chunks/{_server.ts-0Xr2fWaq.js → _server.ts-CKXVBbwb.js} +18 -8
  78. package/build/server/chunks/_server.ts-CKXVBbwb.js.map +1 -0
  79. package/build/server/chunks/{_server.ts-2ixC-X3K.js → _server.ts-CgHc1Zpx.js} +4 -3
  80. package/build/server/chunks/_server.ts-CgHc1Zpx.js.map +1 -0
  81. package/build/server/chunks/{_server.ts-CFX-S_8q.js → _server.ts-DZ5naqSL.js} +2 -2
  82. package/build/server/chunks/{_server.ts-CFX-S_8q.js.map → _server.ts-DZ5naqSL.js.map} +1 -1
  83. package/build/server/chunks/opencode-db-path-BwaPufWf.js +411 -0
  84. package/build/server/chunks/opencode-db-path-BwaPufWf.js.map +1 -0
  85. package/build/server/chunks/{pty-manager-TyMUpDA9.js → pty-manager-RmhVe2Ez.js} +35 -2
  86. package/build/server/chunks/pty-manager-RmhVe2Ez.js.map +1 -0
  87. package/build/server/chunks/qwen-reader-2fTFuC_D.js +622 -0
  88. package/build/server/chunks/qwen-reader-2fTFuC_D.js.map +1 -0
  89. package/build/server/chunks/{_server.ts-CilRds58.js → registry-DzJj2E6I.js} +95 -92
  90. package/build/server/chunks/registry-DzJj2E6I.js.map +1 -0
  91. package/build/server/index.js +1 -1
  92. package/build/server/index.js.map +1 -1
  93. package/build/server/manifest.js +17 -17
  94. package/build/server/manifest.js.map +1 -1
  95. package/package.json +2 -2
  96. package/server.ts +12 -0
  97. package/src/lib/modules/client/common/index.ts +1 -0
  98. package/src/lib/modules/client/common/provider.ts +43 -0
  99. package/src/lib/modules/client/terminal/LaunchSheet.svelte +3 -0
  100. package/src/lib/modules/server/sessions/codex-parser.ts +286 -0
  101. package/src/lib/modules/server/sessions/codex-reader.ts +294 -0
  102. package/src/lib/modules/server/sessions/gemini-reader.ts +571 -0
  103. package/src/lib/modules/server/sessions/opencode-db-path.ts +19 -10
  104. package/src/lib/modules/server/sessions/process-detector.ts +67 -0
  105. package/src/lib/modules/server/sessions/qwen-reader.ts +310 -0
  106. package/src/lib/modules/server/sessions/registry.ts +137 -0
  107. package/src/lib/modules/server/terminal/codex-watcher.ts +182 -0
  108. package/src/lib/modules/server/terminal/pty-manager.ts +41 -0
  109. package/src/lib/modules/server/ws/session-handler.ts +23 -19
  110. package/src/lib/theme.css +54 -1
  111. package/src/lib/types/codex.ts +21 -0
  112. package/src/lib/types/gemini.ts +100 -0
  113. package/src/lib/types/generated/Sessions.ts +24 -1
  114. package/src/lib/types/index.ts +2 -0
  115. package/src/lib/types/server.ts +18 -5
  116. package/src/lib/types/sessions.ts +23 -2
  117. package/src/routes/api/device-token/+server.ts +7 -3
  118. package/src/routes/api/sessions/+server.ts +5 -40
  119. package/src/routes/api/sessions/connect/+server.ts +22 -11
  120. package/src/routes/api/terminals/+server.ts +7 -5
  121. package/src/routes/project/+page.svelte +7 -23
  122. package/src/routes/session/[id]/+page.svelte +3 -3
  123. package/src/routes/terminals/+page.svelte +7 -2
  124. package/src/routes/terminals/[id]/+page.svelte +1 -2
  125. package/build/client/_app/immutable/assets/0.BZLcOr5z.css.br +0 -0
  126. package/build/client/_app/immutable/assets/0.BZLcOr5z.css.gz +0 -0
  127. package/build/client/_app/immutable/chunks/X-tVU_3P.js.br +0 -0
  128. package/build/client/_app/immutable/chunks/X-tVU_3P.js.gz +0 -0
  129. package/build/client/_app/immutable/chunks/gxvWeAns.js.br +0 -0
  130. package/build/client/_app/immutable/chunks/gxvWeAns.js.gz +0 -0
  131. package/build/client/_app/immutable/chunks/pMo6RVvN.js.br +0 -0
  132. package/build/client/_app/immutable/chunks/pMo6RVvN.js.gz +0 -0
  133. package/build/client/_app/immutable/entry/app.B0PrrcUG.js.br +0 -0
  134. package/build/client/_app/immutable/entry/app.B0PrrcUG.js.gz +0 -0
  135. package/build/client/_app/immutable/entry/start.B1obDjVk.js +0 -1
  136. package/build/client/_app/immutable/entry/start.B1obDjVk.js.br +0 -0
  137. package/build/client/_app/immutable/entry/start.B1obDjVk.js.gz +0 -0
  138. package/build/client/_app/immutable/nodes/0.D4GLHqPM.js.br +0 -0
  139. package/build/client/_app/immutable/nodes/0.D4GLHqPM.js.gz +0 -0
  140. package/build/client/_app/immutable/nodes/1.nJde5z5O.js.br +0 -0
  141. package/build/client/_app/immutable/nodes/1.nJde5z5O.js.gz +0 -0
  142. package/build/client/_app/immutable/nodes/2.CLtsjLeG.js.br +0 -0
  143. package/build/client/_app/immutable/nodes/2.CLtsjLeG.js.gz +0 -0
  144. package/build/client/_app/immutable/nodes/3.CKTUHtnx.js.br +0 -0
  145. package/build/client/_app/immutable/nodes/3.CKTUHtnx.js.gz +0 -0
  146. package/build/client/_app/immutable/nodes/6.Cgc5iTlM.js +0 -1
  147. package/build/client/_app/immutable/nodes/6.Cgc5iTlM.js.br +0 -0
  148. package/build/client/_app/immutable/nodes/6.Cgc5iTlM.js.gz +0 -0
  149. package/build/client/_app/immutable/nodes/7.BXKvUopV.js +0 -4
  150. package/build/client/_app/immutable/nodes/7.BXKvUopV.js.br +0 -0
  151. package/build/client/_app/immutable/nodes/7.BXKvUopV.js.gz +0 -0
  152. package/build/client/_app/immutable/nodes/8.Df0leW0d.js +0 -2
  153. package/build/client/_app/immutable/nodes/8.Df0leW0d.js.br +0 -0
  154. package/build/client/_app/immutable/nodes/8.Df0leW0d.js.gz +0 -0
  155. package/build/client/_app/immutable/nodes/9.C4-N3geF.js +0 -2
  156. package/build/client/_app/immutable/nodes/9.C4-N3geF.js.br +0 -0
  157. package/build/client/_app/immutable/nodes/9.C4-N3geF.js.gz +0 -0
  158. package/build/server/chunks/6--I7fF3Bx.js.map +0 -1
  159. package/build/server/chunks/7-BwPLVwOR.js.map +0 -1
  160. package/build/server/chunks/8-BwOMHaoQ.js.map +0 -1
  161. package/build/server/chunks/9-DkO6aJIB.js.map +0 -1
  162. package/build/server/chunks/_server.ts-0Xr2fWaq.js.map +0 -1
  163. package/build/server/chunks/_server.ts-2ixC-X3K.js.map +0 -1
  164. package/build/server/chunks/_server.ts-40c_epk8.js.map +0 -1
  165. package/build/server/chunks/_server.ts-BuYyCrnF.js.map +0 -1
  166. package/build/server/chunks/_server.ts-ByPExYfO.js.map +0 -1
  167. package/build/server/chunks/_server.ts-CilRds58.js.map +0 -1
  168. package/build/server/chunks/_server.ts-CjpQ10xh.js.map +0 -1
  169. package/build/server/chunks/opencode-db-path-DcfhJtJy.js +0 -15
  170. package/build/server/chunks/opencode-db-path-DcfhJtJy.js.map +0 -1
  171. package/build/server/chunks/pty-manager-TyMUpDA9.js.map +0 -1
@@ -22,6 +22,8 @@ import type { WebSocket } from 'ws';
22
22
  import * as fs from 'fs';
23
23
  import * as path from 'path';
24
24
 
25
+ import { findCodexRolloutById } from '../sessions/codex-reader';
26
+
25
27
  // ── Module-level references ──────────────────────────────────────────
26
28
 
27
29
  let _ptyManager: null | PtyManagerLike = null;
@@ -171,34 +173,36 @@ function conversationToLive(msg: ConversationMessage): ServerMessage[] {
171
173
  * Returns the absolute path if found, or null.
172
174
  */
173
175
  function findJsonlFileForSession(sessionId: string): null | string {
174
- const claudeProjectsDir = path.join(process.env.HOME || '', '.claude', 'projects');
175
-
176
- if (!fs.existsSync(claudeProjectsDir)) {
176
+ // Reject anything that could traverse out of the session directories before
177
+ // it is interpolated into a filesystem path.
178
+ if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
177
179
  return null;
178
180
  }
179
181
 
180
- try {
181
- const projectDirs = fs.readdirSync(claudeProjectsDir);
182
- for (const dir of projectDirs) {
183
- const fullDir = path.join(claudeProjectsDir, dir);
184
- try {
185
- if (!fs.statSync(fullDir).isDirectory()) {
182
+ const claudeProjectsDir = path.join(process.env.HOME || '', '.claude', 'projects');
183
+ if (fs.existsSync(claudeProjectsDir)) {
184
+ try {
185
+ for (const dir of fs.readdirSync(claudeProjectsDir)) {
186
+ const fullDir = path.join(claudeProjectsDir, dir);
187
+ try {
188
+ if (!fs.statSync(fullDir).isDirectory()) {
189
+ continue;
190
+ }
191
+ } catch {
186
192
  continue;
187
193
  }
188
- } catch {
189
- continue;
190
- }
191
-
192
- const jsonlPath = path.join(fullDir, `${sessionId}.jsonl`);
193
- if (fs.existsSync(jsonlPath)) {
194
- return jsonlPath;
194
+ const jsonlPath = path.join(fullDir, `${sessionId}.jsonl`);
195
+ if (fs.existsSync(jsonlPath)) {
196
+ return jsonlPath;
197
+ }
195
198
  }
199
+ } catch {
200
+ // Ignore filesystem errors
196
201
  }
197
- } catch {
198
- // Ignore filesystem errors
199
202
  }
200
203
 
201
- return null;
204
+ // Fall back to an external Codex session: ~/.codex/sessions/**/rollout-*-<id>.jsonl
205
+ return findCodexRolloutById(sessionId);
202
206
  }
203
207
 
204
208
  // ── Helpers ──────────────────────────────────────────────────────────
package/src/lib/theme.css CHANGED
@@ -486,7 +486,7 @@
486
486
  --pill-cursor: inherit;
487
487
  }
488
488
 
489
- .pill-source-claude {
489
+ .pill-source-claude-code {
490
490
  --pill-background: var(--ds-blue-100);
491
491
  --pill-color: var(--ds-blue-900);
492
492
  --pill-font-size: 10px;
@@ -506,6 +506,59 @@
506
506
  --pill-cursor: inherit;
507
507
  }
508
508
 
509
+ .pill-source-codex {
510
+ --pill-background: var(--ds-green-100);
511
+ --pill-color: var(--ds-green-900);
512
+ --pill-font-size: 10px;
513
+ --pill-padding: 2px 8px;
514
+ --pill-hover-background: var(--ds-green-100);
515
+ --pill-hover-color: var(--ds-green-900);
516
+ --pill-cursor: inherit;
517
+ }
518
+
519
+ .pill-source-gemini {
520
+ --pill-background: var(--ds-gray-100);
521
+ --pill-color: var(--ds-gray-900);
522
+ --pill-font-size: 10px;
523
+ --pill-padding: 2px 8px;
524
+ --pill-hover-background: var(--ds-gray-100);
525
+ --pill-hover-color: var(--ds-gray-900);
526
+ --pill-cursor: inherit;
527
+ }
528
+
529
+ /* Additional providers reuse the existing colour families (cosmetic only). */
530
+ .pill-source-qwen,
531
+ .pill-source-iflow {
532
+ --pill-background: var(--ds-red-100);
533
+ --pill-color: var(--ds-red-900);
534
+ --pill-font-size: 10px;
535
+ --pill-padding: 2px 8px;
536
+ --pill-hover-background: var(--ds-red-100);
537
+ --pill-hover-color: var(--ds-red-900);
538
+ --pill-cursor: inherit;
539
+ }
540
+
541
+ .pill-source-cursor {
542
+ --pill-background: var(--ds-blue-100);
543
+ --pill-color: var(--ds-blue-900);
544
+ --pill-font-size: 10px;
545
+ --pill-padding: 2px 8px;
546
+ --pill-hover-background: var(--ds-blue-100);
547
+ --pill-hover-color: var(--ds-blue-900);
548
+ --pill-cursor: inherit;
549
+ }
550
+
551
+ .pill-source-copilot,
552
+ .pill-source-amp {
553
+ --pill-background: var(--ds-amber-100);
554
+ --pill-color: var(--ds-amber-900);
555
+ --pill-font-size: 10px;
556
+ --pill-padding: 2px 8px;
557
+ --pill-hover-background: var(--ds-amber-100);
558
+ --pill-hover-color: var(--ds-amber-900);
559
+ --pill-cursor: inherit;
560
+ }
561
+
509
562
  /* ===== Icon Sizes ===== */
510
563
  .icon-14 {
511
564
  --icon-width: 14px;
@@ -0,0 +1,21 @@
1
+ // Codex CLI session types (hand-written: the rollout record shapes are
2
+ // provider-specific and not worth round-tripping through the YAML codegen).
3
+ // Codex stores sessions as JSONL "rollout" files under
4
+ // ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl.
5
+
6
+ import type { ConversationMessage } from './sessions';
7
+
8
+ /** Result of parsing a Codex rollout file into the canonical message model. */
9
+ export interface CodexParseResult {
10
+ messages: ConversationMessage[];
11
+ meta: CodexSessionMeta | null;
12
+ }
13
+
14
+ /** First-line `session_meta` payload of a Codex rollout file. */
15
+ export interface CodexSessionMeta {
16
+ cliVersion: string;
17
+ cwd: string;
18
+ id: string;
19
+ model: string;
20
+ startedAt: string;
21
+ }
@@ -0,0 +1,100 @@
1
+ // Gemini CLI session types (hand-written: the on-disk formats are
2
+ // provider-specific and not worth round-tripping through the YAML codegen).
3
+ // Gemini CLI stores user messages in ~/.gemini/tmp/<projectHash>/logs.json
4
+ // and full conversation records in
5
+ // ~/.gemini/tmp/<projectHash>/chats/session-*.json (newer versions only).
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // logs.json — user-messages-only format (all versions)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /** Union of all part shapes that appear in a ConversationRecord message. */
12
+ export type GeminiContentPart = GeminiFunctionCallPart | GeminiTextPart | GeminiThoughtPart;
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // chats/session-*.json — full ConversationRecord (newer versions)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** Full conversation record stored in chats/session-*.json. */
19
+ export interface GeminiConversationRecord {
20
+ directories?: string[];
21
+ kind?: 'main' | 'subagent';
22
+ lastUpdated: string;
23
+ messages: GeminiMessageRecord[];
24
+ projectHash: string;
25
+ sessionId: string;
26
+ startTime: string;
27
+ summary?: string;
28
+ }
29
+
30
+ /** Inline function-call part from the Google GenAI SDK. */
31
+ export interface GeminiFunctionCallPart {
32
+ functionCall: {
33
+ args: Record<string, unknown>;
34
+ id?: string;
35
+ name: string;
36
+ };
37
+ }
38
+
39
+ /** A single entry in ~/.gemini/tmp/<projectHash>/logs.json. */
40
+ export interface GeminiLogEntry {
41
+ message: string;
42
+ messageId: number;
43
+ sessionId: string;
44
+ timestamp: string;
45
+ type: 'user';
46
+ }
47
+
48
+ /** A single message record in a full ConversationRecord. */
49
+ export type GeminiMessageRecord =
50
+ | {
51
+ content: GeminiContentPart[] | string;
52
+ id: string;
53
+ thoughts?: GeminiThoughtSummary[];
54
+ timestamp: string;
55
+ toolCalls?: GeminiToolCallRecord[];
56
+ type: 'gemini';
57
+ }
58
+ | {
59
+ content: GeminiContentPart[] | string;
60
+ id: string;
61
+ timestamp: string;
62
+ type: 'error' | 'info' | 'user' | 'warning';
63
+ };
64
+
65
+ /** Contents of ~/.gemini/projects.json (present only in newer gemini-cli). */
66
+ export type GeminiProjectsJson = Record<string, string>;
67
+
68
+ /** Plain-text content part from the Google GenAI SDK. */
69
+ export interface GeminiTextPart {
70
+ text: string;
71
+ thought?: false;
72
+ }
73
+
74
+ /** Inline thinking/reasoning part from the Google GenAI SDK. */
75
+ export interface GeminiThoughtPart {
76
+ text: string;
77
+ thought: true;
78
+ }
79
+
80
+ /** A thought-summary entry attached to a 'gemini'-type message. */
81
+ export interface GeminiThoughtSummary {
82
+ summary?: string;
83
+ timestamp: string;
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // projects.json — project slug → absolute path registry (newer versions)
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /** A single tool-call record attached to a 'gemini'-type message. */
91
+ export interface GeminiToolCallRecord {
92
+ args: Record<string, unknown>;
93
+ description?: string;
94
+ displayName?: string;
95
+ id: string;
96
+ name: string;
97
+ result?: unknown;
98
+ status: 'cancelled' | 'error' | 'pending' | 'success';
99
+ timestamp: string;
100
+ }
@@ -14,12 +14,28 @@ import {
14
14
  * @type { SessionSource }
15
15
  * @description Source tool that produced the session
16
16
  */
17
- export type SessionSource = 'claude-code' | 'opencode';
17
+ export type SessionSource =
18
+ | 'claude-code'
19
+ | 'opencode'
20
+ | 'codex'
21
+ | 'gemini'
22
+ | 'qwen'
23
+ | 'cursor'
24
+ | 'copilot'
25
+ | 'amp'
26
+ | 'iflow';
18
27
 
19
28
  export function decodeSessionSource(rawInput: unknown): SessionSource | null {
20
29
  switch (rawInput) {
21
30
  case 'claude-code':
22
31
  case 'opencode':
32
+ case 'codex':
33
+ case 'gemini':
34
+ case 'qwen':
35
+ case 'cursor':
36
+ case 'copilot':
37
+ case 'amp':
38
+ case 'iflow':
23
39
  return rawInput;
24
40
  }
25
41
  return null;
@@ -29,6 +45,13 @@ export function _decodeSessionSource(rawInput: unknown): SessionSource | undefin
29
45
  switch (rawInput) {
30
46
  case 'claude-code':
31
47
  case 'opencode':
48
+ case 'codex':
49
+ case 'gemini':
50
+ case 'qwen':
51
+ case 'cursor':
52
+ case 'copilot':
53
+ case 'amp':
54
+ case 'iflow':
32
55
  return rawInput;
33
56
  }
34
57
  return;
@@ -4,9 +4,11 @@
4
4
  export type * from './activity';
5
5
  export type * from './apn';
6
6
  export type * from './cli';
7
+ export type * from './codex';
7
8
  export type * from './common';
8
9
  export type * from './dashboard';
9
10
  export * from './decision';
11
+ export type * from './gemini';
10
12
  export * from './generated';
11
13
  export type * from './neurolink';
12
14
  export type * from './server';
@@ -8,11 +8,22 @@
8
8
  import type { FSWatcher } from 'chokidar';
9
9
  import type WebSocket from 'ws';
10
10
 
11
+ import type { CodexStreamParser } from '../modules/server/sessions/codex-parser';
11
12
  import type { HolderClient } from '../modules/server/terminal/holder-client';
12
13
  import type { ConversationMessage } from './sessions';
13
14
 
14
15
  // ── holder-client types ─────────────────────────────────────────────
15
16
 
17
+ export interface CodexWatchState {
18
+ callbacks: Set<(messages: ConversationMessage[]) => void>;
19
+ idleTimer: null | ReturnType<typeof setTimeout>;
20
+ /** Incomplete trailing line buffered between reads. */
21
+ lineBuffer: string;
22
+ offset: number;
23
+ parser: CodexStreamParser;
24
+ watcher: FSWatcher;
25
+ }
26
+
16
27
  /** Messages received from the holder process (local ndjson protocol). */
17
28
  export type HolderIncomingMessage =
18
29
  | { active: boolean; type: 'activity' }
@@ -22,19 +33,21 @@ export type HolderIncomingMessage =
22
33
  | { exitCode: null | number; exited: boolean; pid: number; type: 'info' }
23
34
  | { path: string; type: 'cwd' };
24
35
 
36
+ // ── pty-manager types ───────────────────────────────────────────────
37
+
25
38
  /** Messages sent to the holder process (local ndjson protocol). */
26
39
  export type HolderOutgoingMessage =
27
40
  | { cols: number; rows: number; type: 'resize' }
28
41
  | { data: string; type: 'input' }
29
42
  | { signal?: string; type: 'kill' };
30
43
 
31
- // ── pty-manager types ───────────────────────────────────────────────
32
-
33
44
  /**
34
45
  * Callback invoked when new JSONL entries are parsed from a watched file.
35
46
  */
36
47
  export type OnNewEntries = (entries: ConversationMessage[]) => void;
37
48
 
49
+ // ── session-watcher types ───────────────────────────────────────────
50
+
38
51
  export interface OpenCodeWatchState {
39
52
  callbacks: Set<(messages: ConversationMessage[]) => void>;
40
53
  /** Set of message IDs we have already emitted, to avoid duplicates. */
@@ -49,8 +62,6 @@ export interface OpenCodeWatchState {
49
62
  sessionId: string;
50
63
  }
51
64
 
52
- // ── session-watcher types ───────────────────────────────────────────
53
-
54
65
  export interface PtyManagedTerminal {
55
66
  args: string[];
56
67
  clients: Set<WebSocket>;
@@ -78,12 +89,14 @@ export interface PtyManagedTerminal {
78
89
  watcherOffset: number;
79
90
  }
80
91
 
92
+ // ── opencode-watcher types ──────────────────────────────────────────
93
+
81
94
  export interface PtyOutputBuffer {
82
95
  data: string[];
83
96
  size: number;
84
97
  }
85
98
 
86
- // ── opencode-watcher types ──────────────────────────────────────────
99
+ // ── codex-watcher types ─────────────────────────────────────────────
87
100
 
88
101
  export interface SessionWatchedFile {
89
102
  callbacks: Set<OnNewEntries>;
@@ -3,7 +3,7 @@
3
3
  // classes and `type: string` instead of the string-literal discriminated unions
4
4
  // required at runtime.
5
5
 
6
- import type { MessageRole } from './generated';
6
+ import type { MessageRole, ProjectGroup, SessionSource } from './generated';
7
7
 
8
8
  /** Internal structure from ~/.claude/sessions/<PID>.json */
9
9
  export interface ClaudeSessionFile {
@@ -23,7 +23,16 @@ export interface ConversationMessage {
23
23
  }
24
24
 
25
25
  export interface DetectedProcess {
26
- command: 'claude' | 'opencode';
26
+ command:
27
+ | 'amp'
28
+ | 'claude'
29
+ | 'codex'
30
+ | 'copilot'
31
+ | 'cursor-agent'
32
+ | 'gemini'
33
+ | 'iflow'
34
+ | 'opencode'
35
+ | 'qwen';
27
36
  cwd: string;
28
37
  kind: string;
29
38
  pid: number;
@@ -34,6 +43,18 @@ export interface DetectedProcess {
34
43
 
35
44
  export type MessagePart = TextPart | ThinkingPart | ToolResultPart | ToolUsePart;
36
45
 
46
+ /** A registered AI-agent provider (see server/sessions/registry.ts). */
47
+ export interface ProviderDef {
48
+ command: string;
49
+ getConversation: (sessionId: string, offset: number, limit: number) => ConversationMessage[];
50
+ isAI: boolean;
51
+ label: string;
52
+ listProjects: () => ProjectGroup[];
53
+ nameSuffix?: string;
54
+ resumeArgs: (sessionId: string) => string[];
55
+ source: SessionSource;
56
+ }
57
+
37
58
  export interface TextPart {
38
59
  content: string;
39
60
  type: 'text';
@@ -1,4 +1,3 @@
1
- import { env } from '$env/dynamic/private';
2
1
  import { validateAuth } from '$lib/modules/server/auth';
3
2
  import { json } from '@sveltejs/kit';
4
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
@@ -70,9 +69,14 @@ export const POST: RequestHandler = async ({ request }) => {
70
69
  tokens[platform] = token;
71
70
  writeTokens(tokens);
72
71
 
73
- // Update in-memory env so APNs can use it immediately (iOS is the primary APNs target)
72
+ // Update in-memory env so APNs can use it immediately (iOS is the primary APNs target).
73
+ // SvelteKit's $env/dynamic/private exposes a Proxy whose getter reads process.env at
74
+ // access time but whose setter does NOT propagate to process.env. Assigning via the
75
+ // Proxy is a silent no-op, so subsequent /api/notify calls still read the stale value
76
+ // from .env. Write straight to process.env so env.DEVICE_TOKEN picks up the new token
77
+ // on the next read.
74
78
  if (platform === 'ios') {
75
- (env as Record<string, string>).DEVICE_TOKEN = token;
79
+ process.env.DEVICE_TOKEN = token;
76
80
  }
77
81
 
78
82
  console.log(`[device-token] Registered ${platform} token (length: ${token.length})`);
@@ -2,13 +2,9 @@ import type { ProjectGroup } from '$lib/types';
2
2
 
3
3
  import { validateAuth } from '$lib/modules/server/auth';
4
4
  import {
5
- getSessionConversation,
6
- listProjectsWithSessions,
7
- } from '$lib/modules/server/sessions/jsonl-reader';
8
- import {
9
- getOpenCodeConversation,
10
- listOpenCodeProjects,
11
- } from '$lib/modules/server/sessions/opencode-reader';
5
+ getProviderConversation,
6
+ listAllProviderProjects,
7
+ } from '$lib/modules/server/sessions/registry';
12
8
  import { json } from '@sveltejs/kit';
13
9
 
14
10
  import type { RequestHandler } from './$types';
@@ -24,33 +20,7 @@ function getMergedProjects(): ProjectGroup[] {
24
20
  return cachedProjects;
25
21
  }
26
22
 
27
- const claudeProjects = listProjectsWithSessions();
28
- const openCodeProjects = listOpenCodeProjects();
29
- const projectsByPath = new Map<string, ProjectGroup>();
30
-
31
- for (const p of claudeProjects) {
32
- projectsByPath.set(p.fullPath, { ...p });
33
- }
34
-
35
- for (const op of openCodeProjects) {
36
- const existing = projectsByPath.get(op.fullPath);
37
- if (existing) {
38
- existing.sessions.push(...op.sessions);
39
- existing.sessions.sort(
40
- (a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime()
41
- );
42
- existing.sessionCount = existing.sessions.length;
43
- existing.lastModified = existing.sessions[0]?.modified || existing.lastModified;
44
- existing.name = existing.name.replace(' (OpenCode)', '');
45
- } else {
46
- op.name = op.name.replace(' (OpenCode)', '');
47
- projectsByPath.set(op.fullPath, { ...op });
48
- }
49
- }
50
-
51
- cachedProjects = [...projectsByPath.values()].sort(
52
- (a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
53
- );
23
+ cachedProjects = listAllProviderProjects();
54
24
  cacheTimestamp = now;
55
25
  return cachedProjects;
56
26
  }
@@ -86,12 +56,7 @@ export const GET: RequestHandler = ({ request, url }) => {
86
56
  const claudeProjectDir = matchedProject
87
57
  ? matchedProject.fullPath.replace(/\//g, '-')
88
58
  : undefined;
89
- let messages = getSessionConversation(sessionId, offset, limit, claudeProjectDir);
90
-
91
- // If Claude Code reader found nothing, try OpenCode
92
- if (messages.length === 0) {
93
- messages = getOpenCodeConversation(sessionId, offset, limit);
94
- }
59
+ const messages = getProviderConversation(sessionId, offset, limit, claudeProjectDir);
95
60
 
96
61
  // Find session info — short-circuit when project is already resolved
97
62
  let sessionInfo = matchedProject?.sessions.find((s) => s.id === sessionId) ?? null;
@@ -1,4 +1,5 @@
1
1
  import { validateAuth } from '$lib/modules/server/auth';
2
+ import { PROVIDER_COMMANDS, resumeArgsForCommand } from '$lib/modules/server/sessions/registry';
2
3
  import { ptyManager } from '$lib/modules/server/terminal/pty-manager';
3
4
  import { toErrorMessage } from '$lib/modules/server/utils/error';
4
5
  import { json } from '@sveltejs/kit';
@@ -34,12 +35,21 @@ export const POST: RequestHandler = async ({ request }) => {
34
35
  return json({ error: 'sessionId is required (string)' }, { status: 400 });
35
36
  }
36
37
 
38
+ // sessionId becomes a process argument (e.g. `codex resume <id>`); restrict it
39
+ // to safe identifier characters to prevent argument/path injection.
40
+ if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
41
+ return json({ error: 'Invalid sessionId format' }, { status: 400 });
42
+ }
43
+
37
44
  if (!cwd || typeof cwd !== 'string') {
38
45
  return json({ error: 'cwd is required (string)' }, { status: 400 });
39
46
  }
40
47
 
41
- if (!command || (command !== 'claude' && command !== 'opencode')) {
42
- return json({ error: 'command must be "claude" or "opencode"' }, { status: 400 });
48
+ if (!command || !PROVIDER_COMMANDS.includes(command)) {
49
+ return json(
50
+ { error: `command must be one of: ${PROVIDER_COMMANDS.join(', ')}` },
51
+ { status: 400 }
52
+ );
43
53
  }
44
54
 
45
55
  // --- Validate cwd (same checks as POST /api/terminals) ---
@@ -63,13 +73,14 @@ export const POST: RequestHandler = async ({ request }) => {
63
73
 
64
74
  // --- Reuse existing terminal if one is already running for this session ---
65
75
 
66
- const existing = ptyManager
67
- .list()
68
- .find(
69
- (t) =>
70
- t.status === 'running' &&
71
- (t.sessionFile?.endsWith(`/${sessionId}.jsonl`) || t.openCodeSessionId === sessionId)
72
- );
76
+ const existing = ptyManager.list().find(
77
+ (t) =>
78
+ t.status === 'running' &&
79
+ // Claude: <id>.jsonl ; Codex rollout: rollout-<ts>-<id>.jsonl ; OpenCode: session id
80
+ (t.sessionFile?.endsWith(`/${sessionId}.jsonl`) ||
81
+ t.sessionFile?.endsWith(`-${sessionId}.jsonl`) ||
82
+ t.openCodeSessionId === sessionId)
83
+ );
73
84
 
74
85
  if (existing) {
75
86
  console.log(
@@ -94,9 +105,9 @@ export const POST: RequestHandler = async ({ request }) => {
94
105
  return json({ error: 'No existing terminal for this session' }, { status: 404 });
95
106
  }
96
107
 
97
- // --- Build args based on command ---
108
+ // --- Build args based on command (resume convention differs per agent CLI) ---
98
109
 
99
- const args: string[] = command === 'claude' ? ['--resume', sessionId] : ['--session', sessionId];
110
+ const args: string[] = resumeArgsForCommand(command, sessionId);
100
111
 
101
112
  try {
102
113
  const terminal = await ptyManager.create(command, args, realCwd, 120, 40);
@@ -1,13 +1,15 @@
1
1
  import { validateAuth } from '$lib/modules/server/auth';
2
+ import { PROVIDER_COMMANDS } from '$lib/modules/server/sessions/registry';
2
3
  import { ptyManager } from '$lib/modules/server/terminal/pty-manager';
3
4
  import { toErrorMessage } from '$lib/modules/server/utils/error';
4
5
  import { json } from '@sveltejs/kit';
5
6
  import { realpathSync, statSync } from 'fs';
6
- import { basename, isAbsolute, relative } from 'path';
7
+ import { isAbsolute, relative } from 'path';
7
8
 
8
9
  import type { RequestHandler } from './$types';
9
10
 
10
- const ALLOWED_COMMANDS = ['zsh', 'bash', 'sh', 'fish', 'claude', 'opencode'];
11
+ // Plain shells + every registered AI-agent binary.
12
+ const ALLOWED_COMMANDS = ['zsh', 'bash', 'sh', 'fish', ...PROVIDER_COMMANDS];
11
13
 
12
14
  /** Extract the last non-empty line from a scrollback string. */
13
15
  function lastScrollbackLine(scrollback: string): null | string {
@@ -86,9 +88,9 @@ export const POST: RequestHandler = async ({ request }) => {
86
88
  return json({ error: 'command is required' }, { status: 400 });
87
89
  }
88
90
 
89
- // Issue 4: Command allowlist — only allow known safe commands
90
- const commandBasename = basename(command);
91
- if (!ALLOWED_COMMANDS.includes(commandBasename)) {
91
+ // Command allowlist — only bare allowlisted binary names. Reject any path
92
+ // component so an absolute path like "/tmp/bash" (basename "bash") can't slip through.
93
+ if (command.includes('/') || command.includes('\\') || !ALLOWED_COMMANDS.includes(command)) {
92
94
  return json(
93
95
  { error: `Command not allowed. Allowed: ${ALLOWED_COMMANDS.join(', ')}` },
94
96
  { status: 400 }