@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.
- package/LICENSE +21 -675
- package/dist/api-docs.html +2 -2
- package/dist/assets/App-GBcTeeUS.js +460 -0
- package/dist/assets/App-qxJ8_QYu.css +32 -0
- package/dist/assets/ReviewApp-C9K--AQE.js +1 -0
- package/dist/assets/{_basePickBy-C19AekOu.js → _basePickBy-DR_8uFCo.js} +1 -1
- package/dist/assets/{_baseUniq-JsnevLw_.js → _baseUniq-D0njlQ_7.js} +1 -1
- package/dist/assets/{arc-BLpcuBlf.js → arc-CKlr_Rec.js} +1 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-BmO_uLUH.js +36 -0
- package/dist/assets/{blockDiagram-WCTKOSBZ-DQBLwsUS.js → blockDiagram-WCTKOSBZ-DhAeO-56.js} +3 -3
- package/dist/assets/c4Diagram-IC4MRINW-C67kFoXx.js +10 -0
- package/dist/assets/channel-V3MBjKys.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-De63kbgc.js → chunk-4BX2VUAB-mLLagvJi.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DtTDDdM9.js → chunk-55IACEB6-Lx-hOjlM.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DHuwd8tw.js → chunk-FMBD7UC4-Bt-XmVUV.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-BgytFtmO.js → chunk-JSJVCQXG-Cya6gaDV.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-nZdp86aN.js → chunk-KX2RTZJC-Bd7Ig6tF.js} +1 -1
- package/dist/assets/chunk-NQ4KR5QH-5UAE0Vg-.js +220 -0
- package/dist/assets/{chunk-QZHKN3VN-DvUQ3mnO.js → chunk-QZHKN3VN-BAxZ8m7w.js} +1 -1
- package/dist/assets/chunk-WL4C6EOR-DjDPvUUP.js +189 -0
- package/dist/assets/classDiagram-VBA2DB6C-C790yYiY.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-C790yYiY.js +1 -0
- package/dist/assets/clone-BbMGfZwt.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-D-60XrkJ.js +1 -0
- package/dist/assets/cytoscape.esm-2ZfV8NB5.js +331 -0
- package/dist/assets/{dagre-KLK3FWXG-CHYIvW47.js → dagre-KLK3FWXG-bqu3ZS4K.js} +1 -1
- package/dist/assets/diagram-E7M64L7V-BueeqoYm.js +24 -0
- package/dist/assets/{diagram-IFDJBPK2-Dzsiln_C.js → diagram-IFDJBPK2-D4fDv2E7.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-DKnGbUpE.js → diagram-P4PSJMXO-WqipY3fN.js} +1 -1
- package/dist/assets/erDiagram-INFDFZHY-D0oVnO-x.js +70 -0
- package/dist/assets/{flowDiagram-PKNHOUZH-BAZ2-jKp.js → flowDiagram-PKNHOUZH-DzbGyxrr.js} +4 -4
- package/dist/assets/ganttDiagram-A5KZAMGK-BwhbbgCP.js +292 -0
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BflpyjGy.js → gitGraphDiagram-K3NZZRJ6-DZgAh_KM.js} +1 -1
- package/dist/assets/{graph-suelaXFh.js → graph-DzKos-N0.js} +1 -1
- package/dist/assets/highlighted-body-TPN3WLV5-CKDMgz3X.js +1 -0
- package/dist/assets/index-DiQlHzGj.js +2 -0
- package/dist/assets/index-Drat2nB9.css +1 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-pfD1FA3p.js → infoDiagram-LFFYTUFH-BFicZbTf.js} +1 -1
- package/dist/assets/ishikawaDiagram-PHBUUO56-CtihxDxl.js +70 -0
- package/dist/assets/journeyDiagram-4ABVD52K-Du00J8_d.js +139 -0
- package/dist/assets/{kanban-definition-K7BYSVSG-FWinmur1.js → kanban-definition-K7BYSVSG-BJi9S0iQ.js} +5 -5
- package/dist/assets/{layout-vcz43XvZ.js → layout-B80Sityu.js} +1 -1
- package/dist/assets/{linear-le4gc0vx.js → linear-sRQLOf5H.js} +1 -1
- package/dist/assets/mermaid-O7DHMXV3-CBuVs4eJ.js +1038 -0
- package/dist/assets/mindmap-definition-YRQLILUH-C5IL_xi-.js +68 -0
- package/dist/assets/{pieDiagram-SKSYHLDU-C7PKDh3b.js → pieDiagram-SKSYHLDU-CeTwlJ8z.js} +2 -2
- package/dist/assets/quadrantDiagram-337W2JSQ-COfUcLWt.js +7 -0
- package/dist/assets/requirementDiagram-Z7DCOOCP-DSb-CJ5B.js +73 -0
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-4gulcOP4.js → sankeyDiagram-WA2Y5GQK-8jtuVb45.js} +3 -3
- package/dist/assets/sequenceDiagram-2WXFIKYE-C2VpkMwA.js +145 -0
- package/dist/assets/{stateDiagram-RAJIS63D-CB4Vl7qM.js → stateDiagram-RAJIS63D-fmwMqxxc.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-9GGXVWrR.js +1 -0
- package/dist/assets/timeline-definition-YZTLITO2-Dx1hP5lg.js +61 -0
- package/dist/assets/{treemap-KZPCXAKY-DZSEE6Hz.js → treemap-KZPCXAKY-CkLOdYCZ.js} +58 -58
- package/dist/assets/vendor-codemirror-BxPY6emf.js +39 -0
- package/dist/assets/vendor-react-xmA_f8ig.js +59 -0
- package/dist/assets/vendor-xterm-DfaPXD3y.js +66 -0
- package/dist/assets/{vennDiagram-LZ73GAT5-8E_G06fI.js → vennDiagram-LZ73GAT5-D6KWcnln.js} +4 -4
- package/dist/assets/xychartDiagram-JWTSCODW-6fh6qmzN.js +7 -0
- package/dist/index.html +5 -5
- package/package.json +36 -35
- package/server/acp-runtime/client.js +91 -17
- package/server/acp-runtime/index.js +5 -16
- package/server/acp-runtime/session-store.js +4 -4
- package/server/channels/runtime/AgentRuntimeAdapter.js +1 -10
- package/server/claude-sdk.js +1 -3
- package/server/cli.js +159 -2
- package/server/external-agent/service.js +24 -6
- package/server/external-agent/ws.js +63 -3
- package/server/gemini-cli.js +1 -3
- package/server/index.js +120 -19
- package/server/openai-codex.js +1 -3
- package/server/opencode-cli.js +1 -3
- package/server/projects.js +654 -236
- package/server/routes/cc-connect.js +1131 -0
- package/server/routes/cli-auth.js +1 -73
- package/server/routes/commands.js +4 -9
- package/server/routes/projects.js +45 -24
- package/server/routes/session-core.js +149 -86
- package/server/session-core/eventStore.js +45 -18
- package/server/session-core/providerAdapters.js +50 -13
- package/server/session-core/providerDiscovery.js +8 -3
- package/server/session-core/runtimeState.js +8 -0
- package/server/utils/ccConnectManager.js +390 -0
- package/server/utils/ccConnectState.js +575 -0
- package/server/utils/resolveCommandPath.js +71 -0
- package/server/utils/workspaceRoots.js +154 -0
- package/shared/conversationEvents.js +78 -14
- package/dist/assets/App-BWSqiXAT.js +0 -220
- package/dist/assets/App-DrlLKa8f.css +0 -1
- package/dist/assets/ReviewApp-nz3mbArg.js +0 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +0 -36
- package/dist/assets/c4Diagram-IC4MRINW-CGobwBIj.js +0 -10
- package/dist/assets/channel-DkFNxV_H.js +0 -1
- package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +0 -220
- package/dist/assets/chunk-WL4C6EOR-Dn7db_6t.js +0 -189
- package/dist/assets/classDiagram-VBA2DB6C-DtwCEe8S.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-DtwCEe8S.js +0 -1
- package/dist/assets/clone-C0lCEIEO.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-DD_nzqsz.js +0 -1
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +0 -321
- package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +0 -24
- package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +0 -70
- package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +0 -292
- package/dist/assets/highlighted-body-OFNGDK62-CZrBMazC.js +0 -1
- package/dist/assets/index-B01NxbUv.css +0 -1
- package/dist/assets/index-DW5pGgQ_.js +0 -2
- package/dist/assets/ishikawaDiagram-PHBUUO56-ndm9snwO.js +0 -70
- package/dist/assets/journeyDiagram-4ABVD52K-HgF2t7z5.js +0 -139
- package/dist/assets/mermaid-GHXKKRXX-CK8m3lad.js +0 -870
- package/dist/assets/mindmap-definition-YRQLILUH-CNq9SKj4.js +0 -68
- package/dist/assets/quadrantDiagram-337W2JSQ-B7FnztNO.js +0 -7
- package/dist/assets/requirementDiagram-Z7DCOOCP-Bl_BM2Th.js +0 -73
- package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +0 -145
- package/dist/assets/stateDiagram-v2-FVOUBMTO-C85ucl39.js +0 -1
- package/dist/assets/timeline-definition-YZTLITO2-BPGKhi7f.js +0 -61
- package/dist/assets/vendor-codemirror-CyOKkaQZ.js +0 -31
- package/dist/assets/vendor-react-CP4yFTs7.js +0 -8
- package/dist/assets/vendor-xterm-DfcmCpbH.js +0 -66
- package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +0 -7
- package/server/_legacy-providers/README.md +0 -30
- package/server/_legacy-providers/claude-sdk.js +0 -956
- package/server/_legacy-providers/gemini-cli.js +0 -368
- package/server/_legacy-providers/openai-codex.js +0 -705
- package/server/_legacy-providers/opencode-cli.js +0 -674
- package/server/acp-runtime/client.test.js +0 -688
- package/server/acp-runtime/session-store.test.js +0 -89
- package/server/cli.test.js +0 -76
- package/server/external-agent/service.test.js +0 -53
- package/server/external-agent/ws.test.js +0 -289
- 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
|
|
203
|
-
|
|
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 ||
|
|
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
|
-
|
|
14
|
+
export {
|
|
15
|
+
DEFAULT_WORKSPACES_ROOT,
|
|
16
|
+
HAS_WORKSPACES_ROOT_RESTRICTION,
|
|
17
|
+
WORKSPACES_ROOTS
|
|
18
|
+
} from '../utils/workspaceRoots.js';
|
|
8
19
|
|
|
9
|
-
|
|
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
|
|
83
|
-
realPath = await
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
123
|
+
if (hasWorkspaceRootRestriction && allowedWorkspaceRoots.length > 0) {
|
|
124
|
+
const resolvedWorkspaceRoots = await resolveAllowedWorkspaceRoots({
|
|
125
|
+
fsImpl,
|
|
126
|
+
allowedWorkspaceRoots
|
|
127
|
+
});
|
|
109
128
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
122
|
-
const stats = await
|
|
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
|
|
148
|
+
const linkTarget = await fsImpl.readlink(absolutePath);
|
|
127
149
|
const resolvedTarget = path.resolve(path.dirname(absolutePath), linkTarget);
|
|
128
|
-
const realTarget = await
|
|
150
|
+
const realTarget = await fsImpl.realpath(resolvedTarget);
|
|
129
151
|
|
|
130
|
-
if (!realTarget
|
|
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
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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.
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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 [];
|