@axhub/genie 0.2.7 → 0.2.9

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 (131) hide show
  1. package/LICENSE +21 -675
  2. package/dist/api-docs.html +2 -2
  3. package/dist/assets/App-GBcTeeUS.js +460 -0
  4. package/dist/assets/App-qxJ8_QYu.css +32 -0
  5. package/dist/assets/ReviewApp-C9K--AQE.js +1 -0
  6. package/dist/assets/{_basePickBy-C19AekOu.js → _basePickBy-DR_8uFCo.js} +1 -1
  7. package/dist/assets/{_baseUniq-JsnevLw_.js → _baseUniq-D0njlQ_7.js} +1 -1
  8. package/dist/assets/{arc-BLpcuBlf.js → arc-CKlr_Rec.js} +1 -1
  9. package/dist/assets/architectureDiagram-2XIMDMQ5-BmO_uLUH.js +36 -0
  10. package/dist/assets/{blockDiagram-WCTKOSBZ-DQBLwsUS.js → blockDiagram-WCTKOSBZ-DhAeO-56.js} +3 -3
  11. package/dist/assets/c4Diagram-IC4MRINW-C67kFoXx.js +10 -0
  12. package/dist/assets/channel-V3MBjKys.js +1 -0
  13. package/dist/assets/{chunk-4BX2VUAB-De63kbgc.js → chunk-4BX2VUAB-mLLagvJi.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-DtTDDdM9.js → chunk-55IACEB6-Lx-hOjlM.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-DHuwd8tw.js → chunk-FMBD7UC4-Bt-XmVUV.js} +1 -1
  16. package/dist/assets/{chunk-JSJVCQXG-BgytFtmO.js → chunk-JSJVCQXG-Cya6gaDV.js} +1 -1
  17. package/dist/assets/{chunk-KX2RTZJC-nZdp86aN.js → chunk-KX2RTZJC-Bd7Ig6tF.js} +1 -1
  18. package/dist/assets/chunk-NQ4KR5QH-5UAE0Vg-.js +220 -0
  19. package/dist/assets/{chunk-QZHKN3VN-DvUQ3mnO.js → chunk-QZHKN3VN-BAxZ8m7w.js} +1 -1
  20. package/dist/assets/chunk-WL4C6EOR-DjDPvUUP.js +189 -0
  21. package/dist/assets/classDiagram-VBA2DB6C-C790yYiY.js +1 -0
  22. package/dist/assets/classDiagram-v2-RAHNMMFH-C790yYiY.js +1 -0
  23. package/dist/assets/clone-BbMGfZwt.js +1 -0
  24. package/dist/assets/cose-bilkent-S5V4N54A-D-60XrkJ.js +1 -0
  25. package/dist/assets/cytoscape.esm-2ZfV8NB5.js +331 -0
  26. package/dist/assets/{dagre-KLK3FWXG-CHYIvW47.js → dagre-KLK3FWXG-bqu3ZS4K.js} +1 -1
  27. package/dist/assets/diagram-E7M64L7V-BueeqoYm.js +24 -0
  28. package/dist/assets/{diagram-IFDJBPK2-Dzsiln_C.js → diagram-IFDJBPK2-D4fDv2E7.js} +1 -1
  29. package/dist/assets/{diagram-P4PSJMXO-DKnGbUpE.js → diagram-P4PSJMXO-WqipY3fN.js} +1 -1
  30. package/dist/assets/erDiagram-INFDFZHY-D0oVnO-x.js +70 -0
  31. package/dist/assets/{flowDiagram-PKNHOUZH-BAZ2-jKp.js → flowDiagram-PKNHOUZH-DzbGyxrr.js} +4 -4
  32. package/dist/assets/ganttDiagram-A5KZAMGK-BwhbbgCP.js +292 -0
  33. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BflpyjGy.js → gitGraphDiagram-K3NZZRJ6-DZgAh_KM.js} +1 -1
  34. package/dist/assets/{graph-suelaXFh.js → graph-DzKos-N0.js} +1 -1
  35. package/dist/assets/highlighted-body-TPN3WLV5-CKDMgz3X.js +1 -0
  36. package/dist/assets/index-DiQlHzGj.js +2 -0
  37. package/dist/assets/index-Drat2nB9.css +1 -0
  38. package/dist/assets/{infoDiagram-LFFYTUFH-pfD1FA3p.js → infoDiagram-LFFYTUFH-BFicZbTf.js} +1 -1
  39. package/dist/assets/ishikawaDiagram-PHBUUO56-CtihxDxl.js +70 -0
  40. package/dist/assets/journeyDiagram-4ABVD52K-Du00J8_d.js +139 -0
  41. package/dist/assets/{kanban-definition-K7BYSVSG-FWinmur1.js → kanban-definition-K7BYSVSG-BJi9S0iQ.js} +5 -5
  42. package/dist/assets/{layout-vcz43XvZ.js → layout-B80Sityu.js} +1 -1
  43. package/dist/assets/{linear-le4gc0vx.js → linear-sRQLOf5H.js} +1 -1
  44. package/dist/assets/mermaid-O7DHMXV3-CBuVs4eJ.js +1038 -0
  45. package/dist/assets/mindmap-definition-YRQLILUH-C5IL_xi-.js +68 -0
  46. package/dist/assets/{pieDiagram-SKSYHLDU-C7PKDh3b.js → pieDiagram-SKSYHLDU-CeTwlJ8z.js} +2 -2
  47. package/dist/assets/quadrantDiagram-337W2JSQ-COfUcLWt.js +7 -0
  48. package/dist/assets/requirementDiagram-Z7DCOOCP-DSb-CJ5B.js +73 -0
  49. package/dist/assets/{sankeyDiagram-WA2Y5GQK-4gulcOP4.js → sankeyDiagram-WA2Y5GQK-8jtuVb45.js} +3 -3
  50. package/dist/assets/sequenceDiagram-2WXFIKYE-C2VpkMwA.js +145 -0
  51. package/dist/assets/{stateDiagram-RAJIS63D-CB4Vl7qM.js → stateDiagram-RAJIS63D-fmwMqxxc.js} +1 -1
  52. package/dist/assets/stateDiagram-v2-FVOUBMTO-9GGXVWrR.js +1 -0
  53. package/dist/assets/timeline-definition-YZTLITO2-Dx1hP5lg.js +61 -0
  54. package/dist/assets/{treemap-KZPCXAKY-DZSEE6Hz.js → treemap-KZPCXAKY-CkLOdYCZ.js} +58 -58
  55. package/dist/assets/vendor-codemirror-BxPY6emf.js +39 -0
  56. package/dist/assets/vendor-react-xmA_f8ig.js +59 -0
  57. package/dist/assets/vendor-xterm-DfaPXD3y.js +66 -0
  58. package/dist/assets/{vennDiagram-LZ73GAT5-8E_G06fI.js → vennDiagram-LZ73GAT5-D6KWcnln.js} +4 -4
  59. package/dist/assets/xychartDiagram-JWTSCODW-6fh6qmzN.js +7 -0
  60. package/dist/index.html +5 -5
  61. package/package.json +36 -35
  62. package/server/acp-runtime/client.js +91 -17
  63. package/server/acp-runtime/index.js +5 -16
  64. package/server/acp-runtime/session-store.js +4 -4
  65. package/server/channels/runtime/AgentRuntimeAdapter.js +1 -10
  66. package/server/claude-sdk.js +1 -3
  67. package/server/cli.js +159 -2
  68. package/server/external-agent/service.js +24 -6
  69. package/server/external-agent/ws.js +63 -3
  70. package/server/gemini-cli.js +1 -3
  71. package/server/index.js +120 -19
  72. package/server/openai-codex.js +1 -3
  73. package/server/opencode-cli.js +1 -3
  74. package/server/projects.js +654 -236
  75. package/server/routes/cc-connect.js +1131 -0
  76. package/server/routes/cli-auth.js +1 -73
  77. package/server/routes/commands.js +4 -9
  78. package/server/routes/projects.js +45 -24
  79. package/server/routes/session-core.js +149 -86
  80. package/server/session-core/eventStore.js +45 -18
  81. package/server/session-core/providerAdapters.js +50 -13
  82. package/server/session-core/providerDiscovery.js +8 -3
  83. package/server/session-core/runtimeState.js +8 -0
  84. package/server/utils/ccConnectManager.js +390 -0
  85. package/server/utils/ccConnectState.js +575 -0
  86. package/server/utils/resolveCommandPath.js +71 -0
  87. package/server/utils/workspaceRoots.js +154 -0
  88. package/shared/conversationEvents.js +78 -14
  89. package/dist/assets/App-BWSqiXAT.js +0 -220
  90. package/dist/assets/App-DrlLKa8f.css +0 -1
  91. package/dist/assets/ReviewApp-nz3mbArg.js +0 -1
  92. package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +0 -36
  93. package/dist/assets/c4Diagram-IC4MRINW-CGobwBIj.js +0 -10
  94. package/dist/assets/channel-DkFNxV_H.js +0 -1
  95. package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +0 -220
  96. package/dist/assets/chunk-WL4C6EOR-Dn7db_6t.js +0 -189
  97. package/dist/assets/classDiagram-VBA2DB6C-DtwCEe8S.js +0 -1
  98. package/dist/assets/classDiagram-v2-RAHNMMFH-DtwCEe8S.js +0 -1
  99. package/dist/assets/clone-C0lCEIEO.js +0 -1
  100. package/dist/assets/cose-bilkent-S5V4N54A-DD_nzqsz.js +0 -1
  101. package/dist/assets/cytoscape.esm-5J0xJHOV.js +0 -321
  102. package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +0 -24
  103. package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +0 -70
  104. package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +0 -292
  105. package/dist/assets/highlighted-body-OFNGDK62-CZrBMazC.js +0 -1
  106. package/dist/assets/index-B01NxbUv.css +0 -1
  107. package/dist/assets/index-DW5pGgQ_.js +0 -2
  108. package/dist/assets/ishikawaDiagram-PHBUUO56-ndm9snwO.js +0 -70
  109. package/dist/assets/journeyDiagram-4ABVD52K-HgF2t7z5.js +0 -139
  110. package/dist/assets/mermaid-GHXKKRXX-CK8m3lad.js +0 -870
  111. package/dist/assets/mindmap-definition-YRQLILUH-CNq9SKj4.js +0 -68
  112. package/dist/assets/quadrantDiagram-337W2JSQ-B7FnztNO.js +0 -7
  113. package/dist/assets/requirementDiagram-Z7DCOOCP-Bl_BM2Th.js +0 -73
  114. package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +0 -145
  115. package/dist/assets/stateDiagram-v2-FVOUBMTO-C85ucl39.js +0 -1
  116. package/dist/assets/timeline-definition-YZTLITO2-BPGKhi7f.js +0 -61
  117. package/dist/assets/vendor-codemirror-CyOKkaQZ.js +0 -31
  118. package/dist/assets/vendor-react-CP4yFTs7.js +0 -8
  119. package/dist/assets/vendor-xterm-DfcmCpbH.js +0 -66
  120. package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +0 -7
  121. package/server/_legacy-providers/README.md +0 -30
  122. package/server/_legacy-providers/claude-sdk.js +0 -956
  123. package/server/_legacy-providers/gemini-cli.js +0 -368
  124. package/server/_legacy-providers/openai-codex.js +0 -705
  125. package/server/_legacy-providers/opencode-cli.js +0 -674
  126. package/server/acp-runtime/client.test.js +0 -688
  127. package/server/acp-runtime/session-store.test.js +0 -89
  128. package/server/cli.test.js +0 -76
  129. package/server/external-agent/service.test.js +0 -53
  130. package/server/external-agent/ws.test.js +0 -289
  131. package/shared/conversationEvents.test.js +0 -403
@@ -1,9 +1,9 @@
1
1
  import express from 'express';
2
- import { constants as fsConstants } from 'fs';
3
2
  import fs from 'fs/promises';
4
3
  import path from 'path';
5
4
  import os from 'os';
6
5
  import { listOpencodeModels } from '../opencode-cli.js';
6
+ import { resolveCommandPath } from '../utils/resolveCommandPath.js';
7
7
 
8
8
  const router = express.Router();
9
9
  const INSTALLATION_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
@@ -22,78 +22,6 @@ const PROVIDER_INSTALL_HINTS = {
22
22
  const installationStatusCache = new Map(); // provider -> status
23
23
  const installationStatusInFlight = new Map(); // provider -> Promise
24
24
 
25
- function getLocatorCommand() {
26
- return process.platform === 'win32' ? 'where' : 'which';
27
- }
28
-
29
- async function isRunnableCommand(candidatePath) {
30
- try {
31
- if (process.platform === 'win32') {
32
- await fs.access(candidatePath, fsConstants.F_OK);
33
- return true;
34
- }
35
-
36
- await fs.access(candidatePath, fsConstants.X_OK);
37
- return true;
38
- } catch {
39
- return false;
40
- }
41
- }
42
-
43
- async function resolveCommandPath(command) {
44
- const normalizedCommand = String(command || '').trim();
45
- if (!normalizedCommand) {
46
- return {
47
- found: false,
48
- resolvedPath: null,
49
- reason: 'Command name is empty'
50
- };
51
- }
52
-
53
- const isDirectPath = normalizedCommand.includes(path.sep) || (process.platform === 'win32' && normalizedCommand.includes('/'));
54
- const pathEntries = isDirectPath
55
- ? ['']
56
- : String(process.env.PATH || '')
57
- .split(path.delimiter)
58
- .map((entry) => entry.trim())
59
- .filter(Boolean);
60
-
61
- const windowsExtensions = process.platform === 'win32'
62
- ? String(process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
63
- .split(';')
64
- .map((ext) => ext.trim())
65
- .filter(Boolean)
66
- : [''];
67
-
68
- const candidatePaths = [];
69
- for (const baseDir of pathEntries) {
70
- const baseCandidate = isDirectPath ? normalizedCommand : path.join(baseDir, normalizedCommand);
71
- candidatePaths.push(baseCandidate);
72
-
73
- if (process.platform === 'win32' && !path.extname(baseCandidate)) {
74
- for (const ext of windowsExtensions) {
75
- candidatePaths.push(`${baseCandidate}${ext}`);
76
- }
77
- }
78
- }
79
-
80
- for (const candidatePath of candidatePaths) {
81
- if (await isRunnableCommand(candidatePath)) {
82
- return {
83
- found: true,
84
- resolvedPath: candidatePath,
85
- reason: null
86
- };
87
- }
88
- }
89
-
90
- return {
91
- found: false,
92
- resolvedPath: null,
93
- reason: `${normalizedCommand} not found in PATH`
94
- };
95
- }
96
-
97
25
  function buildInstallationStatus(provider, command, installed, resolvedPath, reason, cacheHit = false) {
98
26
  return {
99
27
  success: true,
@@ -199,13 +199,8 @@ Custom commands can be created in:
199
199
  }, {});
200
200
 
201
201
  const currentProvider = context?.provider || 'claude';
202
- const providerDefaults = {
203
- claude: CLAUDE_MODELS.DEFAULT,
204
- codex: CODEX_MODELS.DEFAULT,
205
- gemini: GEMINI_MODELS.DEFAULT,
206
- opencode: OPENCODE_MODELS.DEFAULT
207
- };
208
- const currentModel = context?.model || providerDefaults[currentProvider] || CLAUDE_MODELS.DEFAULT;
202
+ const discoveredCurrentModel = providerDiscovery.find((item) => item.provider === currentProvider)?.currentModel || null;
203
+ const currentModel = context?.model || discoveredCurrentModel || null;
209
204
  const opencodeDiscovery = providerDiscovery.find((item) => item.provider === 'opencode') || null;
210
205
 
211
206
  return {
@@ -220,7 +215,7 @@ Custom commands can be created in:
220
215
  opencodeDiscovery,
221
216
  message: args.length > 0
222
217
  ? `Switching to model: ${args[0]}`
223
- : `Current model: ${currentModel}`
218
+ : (currentModel ? `Current model: ${currentModel}` : 'Current model is managed by the active provider configuration.')
224
219
  }
225
220
  };
226
221
  },
@@ -254,7 +249,7 @@ Custom commands can be created in:
254
249
  packageName,
255
250
  uptime: uptimeFormatted,
256
251
  uptimeSeconds: Math.floor(uptime),
257
- model: context?.model || 'claude-sonnet-4.5',
252
+ model: context?.model || null,
258
253
  provider: context?.provider || 'claude',
259
254
  nodeVersion: process.version,
260
255
  platform: process.platform
@@ -1,13 +1,23 @@
1
1
  import express from 'express';
2
2
  import { promises as fs } from 'fs';
3
3
  import path from 'path';
4
- import os from 'os';
5
4
  import { addProjectManually } from '../projects.js';
5
+ import {
6
+ DEFAULT_WORKSPACES_ROOT,
7
+ HAS_WORKSPACES_ROOT_RESTRICTION,
8
+ WORKSPACES_ROOTS,
9
+ formatAllowedWorkspaceRoots,
10
+ isPathWithinAllowedRoots,
11
+ resolveAllowedWorkspaceRoots
12
+ } from '../utils/workspaceRoots.js';
6
13
 
7
- const router = express.Router();
14
+ export {
15
+ DEFAULT_WORKSPACES_ROOT,
16
+ HAS_WORKSPACES_ROOT_RESTRICTION,
17
+ WORKSPACES_ROOTS
18
+ } from '../utils/workspaceRoots.js';
8
19
 
9
- // Configure allowed workspace root (defaults to user's home directory)
10
- export const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir();
20
+ const router = express.Router();
11
21
 
12
22
  // System-critical paths that should never be used as workspace directories
13
23
  export const FORBIDDEN_PATHS = [
@@ -42,8 +52,14 @@ export const FORBIDDEN_PATHS = [
42
52
  * @param {string} requestedPath - The path to validate
43
53
  * @returns {Promise<{valid: boolean, resolvedPath?: string, error?: string}>}
44
54
  */
45
- export async function validateWorkspacePath(requestedPath) {
55
+ export async function validateWorkspacePath(requestedPath, options = {}) {
46
56
  try {
57
+ const {
58
+ fsImpl = fs,
59
+ hasWorkspaceRootRestriction = HAS_WORKSPACES_ROOT_RESTRICTION,
60
+ allowedWorkspaceRoots = WORKSPACES_ROOTS
61
+ } = options;
62
+
47
63
  // Resolve to absolute path
48
64
  let absolutePath = path.resolve(requestedPath);
49
65
 
@@ -79,14 +95,14 @@ export async function validateWorkspacePath(requestedPath) {
79
95
  let realPath;
80
96
  try {
81
97
  // Check if path exists to resolve real path
82
- await fs.access(absolutePath);
83
- realPath = await fs.realpath(absolutePath);
98
+ await fsImpl.access(absolutePath);
99
+ realPath = await fsImpl.realpath(absolutePath);
84
100
  } catch (error) {
85
101
  if (error.code === 'ENOENT') {
86
102
  // Path doesn't exist yet - check parent directory
87
103
  let parentPath = path.dirname(absolutePath);
88
104
  try {
89
- const parentRealPath = await fs.realpath(parentPath);
105
+ const parentRealPath = await fsImpl.realpath(parentPath);
90
106
 
91
107
  // Reconstruct the full path with real parent
92
108
  realPath = path.join(parentRealPath, path.basename(absolutePath));
@@ -104,31 +120,36 @@ export async function validateWorkspacePath(requestedPath) {
104
120
  }
105
121
  }
106
122
 
107
- // Resolve the workspace root to its real path
108
- const resolvedWorkspaceRoot = await fs.realpath(WORKSPACES_ROOT);
123
+ if (hasWorkspaceRootRestriction && allowedWorkspaceRoots.length > 0) {
124
+ const resolvedWorkspaceRoots = await resolveAllowedWorkspaceRoots({
125
+ fsImpl,
126
+ allowedWorkspaceRoots
127
+ });
109
128
 
110
- // Ensure the resolved path is contained within the allowed workspace root
111
- if (!realPath.startsWith(resolvedWorkspaceRoot + path.sep) &&
112
- realPath !== resolvedWorkspaceRoot) {
113
- return {
114
- valid: false,
115
- error: `Workspace path must be within the allowed workspace root: ${WORKSPACES_ROOT}`
116
- };
129
+ if (!isPathWithinAllowedRoots(realPath, resolvedWorkspaceRoots)) {
130
+ return {
131
+ valid: false,
132
+ error: `Workspace path must be within one of the allowed workspace roots: ${formatAllowedWorkspaceRoots(allowedWorkspaceRoots)}`
133
+ };
134
+ }
117
135
  }
118
136
 
119
137
  // Additional symlink check for existing paths
120
138
  try {
121
- await fs.access(absolutePath);
122
- const stats = await fs.lstat(absolutePath);
139
+ await fsImpl.access(absolutePath);
140
+ const stats = await fsImpl.lstat(absolutePath);
123
141
 
124
- if (stats.isSymbolicLink()) {
142
+ if (stats.isSymbolicLink() && hasWorkspaceRootRestriction && allowedWorkspaceRoots.length > 0) {
143
+ const resolvedWorkspaceRoots = await resolveAllowedWorkspaceRoots({
144
+ fsImpl,
145
+ allowedWorkspaceRoots
146
+ });
125
147
  // Verify symlink target is also within allowed root
126
- const linkTarget = await fs.readlink(absolutePath);
148
+ const linkTarget = await fsImpl.readlink(absolutePath);
127
149
  const resolvedTarget = path.resolve(path.dirname(absolutePath), linkTarget);
128
- const realTarget = await fs.realpath(resolvedTarget);
150
+ const realTarget = await fsImpl.realpath(resolvedTarget);
129
151
 
130
- if (!realTarget.startsWith(resolvedWorkspaceRoot + path.sep) &&
131
- realTarget !== resolvedWorkspaceRoot) {
152
+ if (!isPathWithinAllowedRoots(realTarget, resolvedWorkspaceRoots)) {
132
153
  return {
133
154
  valid: false,
134
155
  error: 'Symlink target is outside the allowed workspace root'
@@ -1,8 +1,17 @@
1
1
  import express from 'express';
2
- import { getProjects } from '../projects.js';
2
+ import crypto from 'node:crypto';
3
+ import path from 'path';
4
+ import {
5
+ getClaudeSessionMetadata,
6
+ getCodexSessionMetadata,
7
+ getGeminiSessionMetadata,
8
+ getOpencodeSessionMetadata,
9
+ getProjects,
10
+ getProjectsList
11
+ } from '../projects.js';
3
12
  import { discoverAllProviders, discoverProvider } from '../session-core/providerDiscovery.js';
4
13
  import { getProviderAdapter } from '../session-core/providerAdapters.js';
5
- import { listAcpSessions } from '../acp-runtime/session-store.js';
14
+ import { findAcpSessionRecord, listAcpSessions } from '../acp-runtime/session-store.js';
6
15
 
7
16
  const router = express.Router();
8
17
 
@@ -29,6 +38,127 @@ async function flattenProjectSessions(project) {
29
38
  .sort((a, b) => new Date(b.lastActivity || b.updated_at || b.createdAt || 0) - new Date(a.lastActivity || a.updated_at || a.createdAt || 0));
30
39
  }
31
40
 
41
+ async function resolveProviderSessionRoute({
42
+ provider,
43
+ sessionId,
44
+ projectList = null
45
+ }) {
46
+ const normalizedProvider = String(provider || '').trim().toLowerCase();
47
+ const normalizedSessionId = String(sessionId || '').trim();
48
+
49
+ if (!normalizedProvider || !normalizedSessionId) {
50
+ return null;
51
+ }
52
+
53
+ const projects = Array.isArray(projectList) ? projectList : await getProjectsList();
54
+ const normalizeComparableProjectPath = (projectPath) => {
55
+ if (typeof projectPath !== 'string' || !projectPath.trim()) {
56
+ return '';
57
+ }
58
+
59
+ const withoutWindowsLongPathPrefix = projectPath.startsWith('\\\\?\\')
60
+ ? projectPath.slice(4)
61
+ : projectPath;
62
+
63
+ return path.normalize(withoutWindowsLongPathPrefix);
64
+ };
65
+
66
+ const findProjectByPath = (projectPath) => {
67
+ const normalizedProjectPath = normalizeComparableProjectPath(projectPath);
68
+ if (!normalizedProjectPath) {
69
+ return null;
70
+ }
71
+
72
+ return projects.find((project) => {
73
+ const candidatePath = project.fullPath || project.path || '';
74
+ return normalizeComparableProjectPath(candidatePath) === normalizedProjectPath;
75
+ }) || null;
76
+ };
77
+
78
+ const acpRecord = await findAcpSessionRecord(normalizedSessionId, normalizedProvider);
79
+ const acpProject = findProjectByPath(acpRecord?.projectPath || null);
80
+
81
+ let matchedProject = acpProject;
82
+ let resolvedSource = acpRecord ? 'acp' : 'legacy';
83
+
84
+ if (!matchedProject) {
85
+ switch (normalizedProvider) {
86
+ case 'claude': {
87
+ const metadata = await getClaudeSessionMetadata(normalizedSessionId);
88
+ matchedProject = findProjectByPath(metadata?.cwd || null);
89
+ break;
90
+ }
91
+ case 'codex': {
92
+ const metadata = await getCodexSessionMetadata(normalizedSessionId);
93
+ matchedProject = findProjectByPath(metadata?.cwd || null);
94
+ break;
95
+ }
96
+ case 'gemini': {
97
+ const metadata = await getGeminiSessionMetadata(normalizedSessionId);
98
+ const projectHash = String(metadata?.projectHash || '').trim();
99
+
100
+ if (projectHash) {
101
+ matchedProject = projects.find((project) => {
102
+ const projectPath = normalizeComparableProjectPath(project.fullPath || project.path || '');
103
+ if (!projectPath) {
104
+ return false;
105
+ }
106
+
107
+ const candidateHash = crypto.createHash('sha256').update(projectPath).digest('hex');
108
+ return candidateHash === projectHash;
109
+ }) || null;
110
+ }
111
+ break;
112
+ }
113
+ case 'opencode': {
114
+ const metadata = await getOpencodeSessionMetadata(normalizedSessionId);
115
+ matchedProject = findProjectByPath(metadata?.cwd || null);
116
+ break;
117
+ }
118
+ default:
119
+ break;
120
+ }
121
+ }
122
+
123
+ if (!matchedProject) {
124
+ return null;
125
+ }
126
+
127
+ const adapter = getProviderAdapter(normalizedProvider);
128
+ let sessions = [];
129
+
130
+ if (normalizedProvider === 'claude') {
131
+ sessions = await adapter.listSessions({
132
+ projectName: matchedProject.name,
133
+ projectPath: matchedProject.fullPath || matchedProject.path,
134
+ limit: 1000,
135
+ offset: 0
136
+ });
137
+ } else {
138
+ sessions = await adapter.listSessions({
139
+ projectPath: matchedProject.fullPath || matchedProject.path,
140
+ limit: 0
141
+ });
142
+ }
143
+
144
+ const matchedSession = (Array.isArray(sessions) ? sessions : []).find((session) => session.id === normalizedSessionId);
145
+ if (!matchedSession) {
146
+ return null;
147
+ }
148
+
149
+ return {
150
+ provider: normalizedProvider,
151
+ source: matchedSession.source || resolvedSource,
152
+ session: matchedSession,
153
+ project: {
154
+ name: matchedProject.name,
155
+ fullPath: matchedProject.fullPath || matchedProject.path,
156
+ path: matchedProject.path,
157
+ displayName: matchedProject.displayName || matchedProject.name
158
+ }
159
+ };
160
+ }
161
+
32
162
  router.get('/providers', async (req, res) => {
33
163
  try {
34
164
  const providers = await discoverAllProviders({ projectPath: req.query.projectPath });
@@ -68,98 +198,29 @@ router.get('/projects/:projectName/history-index', async (req, res) => {
68
198
  }
69
199
  });
70
200
 
71
- router.get('/sessions/:sessionId/resolve', async (req, res) => {
201
+ router.get('/sessions/:provider/:sessionId/resolve', async (req, res) => {
72
202
  try {
73
- const projects = await getProjects();
203
+ const requestedProvider = String(req.params.provider || '').trim().toLowerCase();
74
204
  const requestedSessionId = String(req.params.sessionId || '').trim();
75
- const providerHint = typeof req.query.provider === 'string' ? req.query.provider.trim().toLowerCase() : '';
76
-
77
- if (!requestedSessionId) {
78
- return res.status(400).json({ success: false, error: 'Session id is required' });
79
- }
80
205
 
81
- const providerOrder = [
82
- providerHint,
83
- 'codex',
84
- 'claude',
85
- 'gemini',
86
- 'opencode'
87
- ].filter((provider, index, all) => provider && all.indexOf(provider) === index);
88
-
89
- let directProjectMatch = null;
90
- let directMatchSessions = [];
91
-
92
- for (const project of projects) {
93
- const flattened = await flattenProjectSessions(project);
94
- if (flattened.some((session) => session.id === requestedSessionId)) {
95
- directProjectMatch = project;
96
- directMatchSessions = flattened;
97
- break;
98
- }
206
+ if (!requestedProvider || !requestedSessionId) {
207
+ return res.status(400).json({ success: false, error: 'provider and session id are required' });
99
208
  }
100
209
 
101
- if (directProjectMatch) {
102
- const matchedSession = directMatchSessions.find((session) => session.id === requestedSessionId);
103
-
104
- return res.json({
105
- success: true,
106
- found: true,
107
- provider: matchedSession.provider,
108
- source: matchedSession.source || 'legacy',
109
- session: matchedSession,
110
- project: {
111
- name: directProjectMatch.name,
112
- fullPath: directProjectMatch.fullPath || directProjectMatch.path,
113
- path: directProjectMatch.path,
114
- displayName: directProjectMatch.displayName || directProjectMatch.name
115
- }
116
- });
117
- }
118
-
119
- for (const provider of providerOrder) {
120
- const adapter = getProviderAdapter(provider);
121
-
122
- for (const project of projects) {
123
- let sessions = [];
124
-
125
- try {
126
- if (provider === 'claude') {
127
- const result = await adapter.listSessions({
128
- projectName: project.name,
129
- projectPath: project.fullPath || project.path,
130
- limit: 1000,
131
- offset: 0
132
- });
133
- sessions = Array.isArray(result) ? result : [];
134
- } else {
135
- sessions = await adapter.listSessions({ projectPath: project.fullPath || project.path, limit: 0 });
136
- }
137
- } catch (error) {
138
- continue;
139
- }
140
-
141
- const matchedSession = sessions.find((session) => session.id === requestedSessionId);
142
- if (!matchedSession) {
143
- continue;
144
- }
210
+ const result = await resolveProviderSessionRoute({
211
+ provider: requestedProvider,
212
+ sessionId: requestedSessionId
213
+ });
145
214
 
146
- return res.json({
147
- success: true,
148
- found: true,
149
- provider,
150
- source: matchedSession.source || 'legacy',
151
- session: matchedSession,
152
- project: {
153
- name: project.name,
154
- fullPath: project.fullPath || project.path,
155
- path: project.path,
156
- displayName: project.displayName || project.name
157
- }
158
- });
159
- }
215
+ if (!result) {
216
+ return res.status(404).json({ success: true, found: false });
160
217
  }
161
218
 
162
- res.status(404).json({ success: true, found: false });
219
+ res.json({
220
+ success: true,
221
+ found: true,
222
+ ...result
223
+ });
163
224
  } catch (error) {
164
225
  res.status(500).json({ success: false, error: error.message });
165
226
  }
@@ -208,4 +269,6 @@ router.get('/sessions/:provider/:sessionId/events', async (req, res) => {
208
269
  }
209
270
  });
210
271
 
272
+ export { resolveProviderSessionRoute };
273
+
211
274
  export default router;
@@ -1,6 +1,8 @@
1
+ import fsSync from 'fs';
1
2
  import fs from 'fs/promises';
2
3
  import os from 'os';
3
4
  import path from 'path';
5
+ import readline from 'readline';
4
6
 
5
7
  import {
6
8
  CONVERSATION_EVENT_KINDS,
@@ -13,7 +15,11 @@ const PERSISTED_EVENT_KINDS = new Set([
13
15
  CONVERSATION_EVENT_KINDS.ERROR,
14
16
  CONVERSATION_EVENT_KINDS.APPROVAL_REQUEST,
15
17
  CONVERSATION_EVENT_KINDS.APPROVAL_RESOLVED,
16
- CONVERSATION_EVENT_KINDS.SYSTEM_NOTICE
18
+ CONVERSATION_EVENT_KINDS.SYSTEM_NOTICE,
19
+ CONVERSATION_EVENT_KINDS.MODE_UPDATE,
20
+ CONVERSATION_EVENT_KINDS.AVAILABLE_COMMANDS_UPDATE,
21
+ CONVERSATION_EVENT_KINDS.PLAN_UPDATE,
22
+ CONVERSATION_EVENT_KINDS.ARTIFACT_CREATED
17
23
  ]);
18
24
 
19
25
  function getSessionEventFilePath(provider, sessionId) {
@@ -24,14 +30,19 @@ function normalizePersistedEvents(events = []) {
24
30
  return events.filter((event) => (
25
31
  isConversationEvent(event) &&
26
32
  event.sessionId &&
27
- (
28
- event.extensions?.runtimeSource === 'acp' ||
29
- event.rawRef?.runtime === 'acp' ||
30
- PERSISTED_EVENT_KINDS.has(event.kind)
31
- )
33
+ PERSISTED_EVENT_KINDS.has(event.kind)
32
34
  ));
33
35
  }
34
36
 
37
+ function extractSerializedEventKind(line) {
38
+ if (typeof line !== 'string' || !line) {
39
+ return null;
40
+ }
41
+
42
+ const match = line.match(/"kind"\s*:\s*"([^"]+)"/);
43
+ return match?.[1] || null;
44
+ }
45
+
35
46
  function sortObjectKeys(value) {
36
47
  if (Array.isArray(value)) {
37
48
  return value.map(sortObjectKeys);
@@ -91,19 +102,35 @@ export async function readMirroredConversationEvents(provider, sessionId) {
91
102
  const filePath = getSessionEventFilePath(provider, sessionId);
92
103
 
93
104
  try {
94
- const content = await fs.readFile(filePath, 'utf8');
95
- return content
96
- .split('\n')
97
- .map((line) => line.trim())
98
- .filter(Boolean)
99
- .map((line) => {
100
- try {
101
- return JSON.parse(line);
102
- } catch {
103
- return null;
105
+ const fileStream = fsSync.createReadStream(filePath, { encoding: 'utf8' });
106
+ const rl = readline.createInterface({
107
+ input: fileStream,
108
+ crlfDelay: Infinity
109
+ });
110
+ const events = [];
111
+
112
+ for await (const rawLine of rl) {
113
+ const line = rawLine.trim();
114
+ if (!line) {
115
+ continue;
116
+ }
117
+
118
+ const kind = extractSerializedEventKind(line);
119
+ if (!kind || !PERSISTED_EVENT_KINDS.has(kind)) {
120
+ continue;
121
+ }
122
+
123
+ try {
124
+ const event = JSON.parse(line);
125
+ if (isConversationEvent(event) && PERSISTED_EVENT_KINDS.has(event.kind)) {
126
+ events.push(event);
104
127
  }
105
- })
106
- .filter(isConversationEvent);
128
+ } catch {
129
+ // Skip malformed lines and oversized legacy transcript events.
130
+ }
131
+ }
132
+
133
+ return events;
107
134
  } catch (error) {
108
135
  if (error?.code === 'ENOENT') {
109
136
  return [];