@axhub/genie 0.2.6 → 0.2.8

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 (102) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-CTKZtqB1.js +460 -0
  3. package/dist/assets/{ReviewApp-BEicSBzW.js → ReviewApp-DM6BNAzR.js} +1 -1
  4. package/dist/assets/{_basePickBy-DkiHsp3X.js → _basePickBy-CqJbRZ9y.js} +1 -1
  5. package/dist/assets/{_baseUniq-7ElXb2sX.js → _baseUniq-BS8YH8jO.js} +1 -1
  6. package/dist/assets/{arc-CEsS3MdK.js → arc-BBmKEN-S.js} +1 -1
  7. package/dist/assets/{architectureDiagram-2XIMDMQ5-BubZ7T3U.js → architectureDiagram-2XIMDMQ5-N5lcb82R.js} +1 -1
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-Cza6M6Ht.js → blockDiagram-WCTKOSBZ-DTMwHuLn.js} +1 -1
  9. package/dist/assets/{c4Diagram-IC4MRINW-jhjtOQ12.js → c4Diagram-IC4MRINW-BTKlkXI9.js} +1 -1
  10. package/dist/assets/channel-1oJBvF-0.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB--HkodwbY.js → chunk-4BX2VUAB-DUdoTxAc.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-CyBuez4e.js → chunk-55IACEB6-Bm_92xe4.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-CuzG4iAl.js → chunk-FMBD7UC4-CGW0g62g.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-BNi8S861.js → chunk-JSJVCQXG-DYkTH3w1.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-D817O-GT.js → chunk-KX2RTZJC-C9oTlISU.js} +1 -1
  16. package/dist/assets/{chunk-NQ4KR5QH-DyujyOvx.js → chunk-NQ4KR5QH-CM50ygWP.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-VMEn-zxh.js → chunk-QZHKN3VN-7dzpYeNJ.js} +1 -1
  18. package/dist/assets/{chunk-WL4C6EOR-CQHHFLvx.js → chunk-WL4C6EOR-Cm9nQrsr.js} +1 -1
  19. package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +1 -0
  21. package/dist/assets/clone-CinxIlEu.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-qykDd54p.js → cose-bilkent-S5V4N54A-Ccp_p0JZ.js} +1 -1
  23. package/dist/assets/{dagre-KLK3FWXG-Bqp7DjEa.js → dagre-KLK3FWXG-fBwTLUp9.js} +1 -1
  24. package/dist/assets/{diagram-E7M64L7V-BKtx468K.js → diagram-E7M64L7V-CeNVmFUp.js} +1 -1
  25. package/dist/assets/{diagram-IFDJBPK2--fHfW6V2.js → diagram-IFDJBPK2-CtavyLGa.js} +1 -1
  26. package/dist/assets/{diagram-P4PSJMXO-D1kQI5RB.js → diagram-P4PSJMXO-CpQTjQwc.js} +1 -1
  27. package/dist/assets/{erDiagram-INFDFZHY-DT9YzdNw.js → erDiagram-INFDFZHY-B8R5vwhd.js} +1 -1
  28. package/dist/assets/{flowDiagram-PKNHOUZH-DWeNr4yg.js → flowDiagram-PKNHOUZH-BvkVVwIQ.js} +1 -1
  29. package/dist/assets/{ganttDiagram-A5KZAMGK--IgwcUhI.js → ganttDiagram-A5KZAMGK-DOu3hSNa.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-B5a8UWjN.js → gitGraphDiagram-K3NZZRJ6-C7zT67YE.js} +1 -1
  31. package/dist/assets/{graph-Cw1rYoD9.js → graph-D11wiwHo.js} +1 -1
  32. package/dist/assets/{highlighted-body-TPN3WLV5-BCxJHuqY.js → highlighted-body-TPN3WLV5-Babpthg-.js} +1 -1
  33. package/dist/assets/index-DFxzgWoO.js +2 -0
  34. package/dist/assets/index-YCFGDVKw.css +1 -0
  35. package/dist/assets/{infoDiagram-LFFYTUFH-D2u70rhN.js → infoDiagram-LFFYTUFH-BmA7IpQG.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-PHBUUO56-Cl8yrezU.js → ishikawaDiagram-PHBUUO56-BEquZd3E.js} +1 -1
  37. package/dist/assets/{journeyDiagram-4ABVD52K-ddP0AMU9.js → journeyDiagram-4ABVD52K-BfemGz7f.js} +1 -1
  38. package/dist/assets/{kanban-definition-K7BYSVSG-DbVt0v29.js → kanban-definition-K7BYSVSG-CWja3mln.js} +1 -1
  39. package/dist/assets/{layout-W_tRx4UV.js → layout-BLUNf-PJ.js} +1 -1
  40. package/dist/assets/{linear-CcMb2ay-.js → linear-DukIV_Xv.js} +1 -1
  41. package/dist/assets/{mermaid-O7DHMXV3-BBJqt8pT.js → mermaid-O7DHMXV3-SgtM28qI.js} +265 -215
  42. package/dist/assets/{mindmap-definition-YRQLILUH-BGhZa7Na.js → mindmap-definition-YRQLILUH-4UjqXITU.js} +1 -1
  43. package/dist/assets/{pieDiagram-SKSYHLDU-CDyJaACv.js → pieDiagram-SKSYHLDU-8AxqJd0M.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-337W2JSQ-BSYuqf0Q.js → quadrantDiagram-337W2JSQ-D60m8V8r.js} +1 -1
  45. package/dist/assets/{requirementDiagram-Z7DCOOCP-Cfi9YX9H.js → requirementDiagram-Z7DCOOCP-zqh9jBVf.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-WA2Y5GQK-Di1ShaMF.js → sankeyDiagram-WA2Y5GQK-CDZILTLI.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-2WXFIKYE-CYTTG38e.js → sequenceDiagram-2WXFIKYE-7BReFd0L.js} +1 -1
  48. package/dist/assets/{stateDiagram-RAJIS63D-CVZYMqyW.js → stateDiagram-RAJIS63D-HPTVdIG4.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +1 -0
  50. package/dist/assets/{timeline-definition-YZTLITO2-B1sdb5mK.js → timeline-definition-YZTLITO2-CTVllFgr.js} +1 -1
  51. package/dist/assets/{treemap-KZPCXAKY-CGG4gx3C.js → treemap-KZPCXAKY-BtyxboJZ.js} +1 -1
  52. package/dist/assets/{vennDiagram-LZ73GAT5-Dds37L2k.js → vennDiagram-LZ73GAT5-D96ZI6Mg.js} +1 -1
  53. package/dist/assets/{xychartDiagram-JWTSCODW-C8QKSyRR.js → xychartDiagram-JWTSCODW-eRk-39YO.js} +1 -1
  54. package/dist/index.html +2 -2
  55. package/package.json +35 -33
  56. package/server/_legacy-providers/README.md +30 -0
  57. package/server/_legacy-providers/claude-sdk.js +956 -0
  58. package/server/_legacy-providers/gemini-cli.js +368 -0
  59. package/server/_legacy-providers/openai-codex.js +705 -0
  60. package/server/_legacy-providers/opencode-cli.js +674 -0
  61. package/server/acp-runtime/client.js +1872 -0
  62. package/server/acp-runtime/index.js +408 -0
  63. package/server/acp-runtime/registry.js +45 -0
  64. package/server/acp-runtime/session-store.js +254 -0
  65. package/server/channels/runtime/AgentRuntimeAdapter.js +22 -80
  66. package/server/claude-sdk.js +24 -946
  67. package/server/cli.js +140 -2
  68. package/server/external-agent/service.js +52 -63
  69. package/server/gemini-cli.js +21 -360
  70. package/server/index.js +133 -58
  71. package/server/openai-codex.js +19 -695
  72. package/server/opencode-cli.js +68 -640
  73. package/server/projects.js +128 -85
  74. package/server/routes/agent.js +2 -0
  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/git.js +3 -20
  79. package/server/routes/projects.js +45 -24
  80. package/server/routes/session-core.js +44 -10
  81. package/server/session-core/abortSession.js +2 -18
  82. package/server/session-core/eventStore.js +5 -1
  83. package/server/session-core/providerAdapters.js +98 -10
  84. package/server/session-core/providerDiscovery.js +8 -3
  85. package/server/session-core/runtimeState.js +16 -17
  86. package/server/session-core/runtimeWriter.js +19 -12
  87. package/server/utils/ccConnectManager.js +390 -0
  88. package/server/utils/ccConnectState.js +575 -0
  89. package/server/utils/resolveCommandPath.js +71 -0
  90. package/server/utils/workspaceRoots.js +154 -0
  91. package/shared/conversationEvents.js +347 -10
  92. package/dist/assets/App-CYTE30Cf.js +0 -484
  93. package/dist/assets/channel-RmqTALN0.js +0 -1
  94. package/dist/assets/classDiagram-VBA2DB6C-wvVV1ggz.js +0 -1
  95. package/dist/assets/classDiagram-v2-RAHNMMFH-wvVV1ggz.js +0 -1
  96. package/dist/assets/clone-oT5aWXpf.js +0 -1
  97. package/dist/assets/index-CBuAXA5S.js +0 -2
  98. package/dist/assets/index-CyLWKyxy.css +0 -1
  99. package/dist/assets/stateDiagram-v2-FVOUBMTO-Bbl0b4-i.js +0 -1
  100. package/server/cli.test.js +0 -76
  101. package/server/external-agent/service.test.js +0 -53
  102. package/server/external-agent/ws.test.js +0 -289
@@ -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
@@ -610,26 +610,9 @@ Generate the commit message:`;
610
610
  const parsed = typeof data === 'string' ? JSON.parse(data) : data;
611
611
  console.log('🔍 Writer received message type:', parsed.type);
612
612
 
613
- // Handle different message formats from supported providers
614
- // Claude SDK sends: {type: 'claude-response', data: {message: {content: [...]}}}
615
- if (parsed.type === 'claude-response' && parsed.data) {
616
- const message = parsed.data.message || parsed.data;
617
- console.log('📦 Claude response message:', JSON.stringify(message, null, 2).substring(0, 500));
618
- if (message.content && Array.isArray(message.content)) {
619
- // Extract text from content array
620
- for (const item of message.content) {
621
- if (item.type === 'text' && item.text) {
622
- console.log('✅ Extracted text chunk:', item.text.substring(0, 100));
623
- responseText += item.text;
624
- }
625
- }
626
- }
627
- }
628
- else if (parsed.type === 'claude-response' && parsed.data?.type === 'content_block_delta' && parsed.data?.delta?.text) {
629
- responseText += parsed.data.delta.text;
630
- }
631
- // Also handle direct text messages
632
- else if (parsed.type === 'text' && parsed.text) {
613
+ if (parsed.type === 'conversation-event' && parsed.event?.kind === 'assistant_text_delta' && parsed.event?.payload?.text) {
614
+ responseText += parsed.event.payload.text;
615
+ } else if (parsed.type === 'text' && parsed.text) {
633
616
  console.log('✅ Direct text:', parsed.text.substring(0, 100));
634
617
  responseText += parsed.text;
635
618
  }
@@ -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'
@@ -2,10 +2,17 @@ import express from 'express';
2
2
  import { getProjects } from '../projects.js';
3
3
  import { discoverAllProviders, discoverProvider } from '../session-core/providerDiscovery.js';
4
4
  import { getProviderAdapter } from '../session-core/providerAdapters.js';
5
+ import { listAcpSessions } from '../acp-runtime/session-store.js';
5
6
 
6
7
  const router = express.Router();
7
8
 
8
- function flattenProjectSessions(project) {
9
+ router.use((req, res, next) => {
10
+ res.setHeader('X-Runtime-Engine', 'acp');
11
+ next();
12
+ });
13
+
14
+ async function flattenProjectSessions(project) {
15
+ const projectPath = project.fullPath || project.path || null;
9
16
  const groups = [
10
17
  { provider: 'claude', items: project.sessions || [] },
11
18
  { provider: 'codex', items: project.codexSessions || [] },
@@ -13,7 +20,12 @@ function flattenProjectSessions(project) {
13
20
  { provider: 'opencode', items: project.opencodeSessions || [] }
14
21
  ];
15
22
 
16
- return groups.flatMap(({ provider, items }) => items.map((item) => ({ ...item, provider, __provider: provider })))
23
+ const acpSessions = await listAcpSessions({ projectPath });
24
+
25
+ return [
26
+ ...groups.flatMap(({ provider, items }) => items.map((item) => ({ ...item, provider, __provider: provider, source: item?.source || 'legacy' }))),
27
+ ...acpSessions.map((item) => ({ ...item, provider: item.provider, __provider: item.provider, source: 'acp' }))
28
+ ]
17
29
  .sort((a, b) => new Date(b.lastActivity || b.updated_at || b.createdAt || 0) - new Date(a.lastActivity || a.updated_at || a.createdAt || 0));
18
30
  }
19
31
 
@@ -42,7 +54,15 @@ router.get('/projects/:projectName/history-index', async (req, res) => {
42
54
  if (!project) {
43
55
  return res.status(404).json({ success: false, error: 'Project not found' });
44
56
  }
45
- res.json({ success: true, project: { name: project.name, fullPath: project.fullPath || project.path, displayName: project.displayName || project.name }, sessions: flattenProjectSessions(project) });
57
+ res.json({
58
+ success: true,
59
+ project: {
60
+ name: project.name,
61
+ fullPath: project.fullPath || project.path,
62
+ displayName: project.displayName || project.name
63
+ },
64
+ sessions: await flattenProjectSessions(project)
65
+ });
46
66
  } catch (error) {
47
67
  res.status(500).json({ success: false, error: error.message });
48
68
  }
@@ -66,19 +86,26 @@ router.get('/sessions/:sessionId/resolve', async (req, res) => {
66
86
  'opencode'
67
87
  ].filter((provider, index, all) => provider && all.indexOf(provider) === index);
68
88
 
69
- const directProjectMatch = projects.find((project) => {
70
- const flattened = flattenProjectSessions(project);
71
- return flattened.some((session) => session.id === requestedSessionId);
72
- });
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
+ }
99
+ }
73
100
 
74
101
  if (directProjectMatch) {
75
- const flattened = flattenProjectSessions(directProjectMatch);
76
- const matchedSession = flattened.find((session) => session.id === requestedSessionId);
102
+ const matchedSession = directMatchSessions.find((session) => session.id === requestedSessionId);
77
103
 
78
104
  return res.json({
79
105
  success: true,
80
106
  found: true,
81
107
  provider: matchedSession.provider,
108
+ source: matchedSession.source || 'legacy',
82
109
  session: matchedSession,
83
110
  project: {
84
111
  name: directProjectMatch.name,
@@ -97,7 +124,12 @@ router.get('/sessions/:sessionId/resolve', async (req, res) => {
97
124
 
98
125
  try {
99
126
  if (provider === 'claude') {
100
- const result = await adapter.listSessions({ projectName: project.name, limit: 1000, offset: 0 });
127
+ const result = await adapter.listSessions({
128
+ projectName: project.name,
129
+ projectPath: project.fullPath || project.path,
130
+ limit: 1000,
131
+ offset: 0
132
+ });
101
133
  sessions = Array.isArray(result) ? result : [];
102
134
  } else {
103
135
  sessions = await adapter.listSessions({ projectPath: project.fullPath || project.path, limit: 0 });
@@ -115,6 +147,7 @@ router.get('/sessions/:sessionId/resolve', async (req, res) => {
115
147
  success: true,
116
148
  found: true,
117
149
  provider,
150
+ source: matchedSession.source || 'legacy',
118
151
  session: matchedSession,
119
152
  project: {
120
153
  name: project.name,
@@ -157,6 +190,7 @@ router.get('/sessions/:provider/:sessionId/events', async (req, res) => {
157
190
  success: true,
158
191
  provider: req.params.provider,
159
192
  sessionId: req.params.sessionId,
193
+ source: result?.source || 'legacy',
160
194
  events,
161
195
  total,
162
196
  hasMore,
@@ -1,7 +1,4 @@
1
- import { abortClaudeSDKSession } from '../claude-sdk.js';
2
- import { abortCodexSession } from '../openai-codex.js';
3
- import { abortGeminiSession } from '../gemini-cli.js';
4
- import { abortOpencodeSession } from '../opencode-cli.js';
1
+ import { abortAgentSession as abortAcpAgentSession } from '../acp-runtime/index.js';
5
2
 
6
3
  export const ABORTABLE_AGENT_PROVIDERS = ['claude', 'codex', 'gemini', 'opencode'];
7
4
 
@@ -11,20 +8,7 @@ export function isAbortableAgentProvider(provider) {
11
8
 
12
9
  export async function abortAgentSession(provider, sessionId) {
13
10
  const normalizedProvider = String(provider || 'claude').trim().toLowerCase();
14
-
15
- if (normalizedProvider === 'codex') {
16
- return abortCodexSession(sessionId);
17
- }
18
-
19
- if (normalizedProvider === 'gemini') {
20
- return abortGeminiSession(sessionId);
21
- }
22
-
23
- if (normalizedProvider === 'opencode') {
24
- return abortOpencodeSession(sessionId);
25
- }
26
-
27
- return abortClaudeSDKSession(sessionId);
11
+ return abortAcpAgentSession(normalizedProvider, sessionId);
28
12
  }
29
13
 
30
14
  export async function abortAgentSessionWithWriter({ provider = 'claude', sessionId, writer }) {
@@ -24,7 +24,11 @@ function normalizePersistedEvents(events = []) {
24
24
  return events.filter((event) => (
25
25
  isConversationEvent(event) &&
26
26
  event.sessionId &&
27
- PERSISTED_EVENT_KINDS.has(event.kind)
27
+ (
28
+ event.extensions?.runtimeSource === 'acp' ||
29
+ event.rawRef?.runtime === 'acp' ||
30
+ PERSISTED_EVENT_KINDS.has(event.kind)
31
+ )
28
32
  ));
29
33
  }
30
34
 
@@ -10,6 +10,10 @@ import {
10
10
  getOpencodeSessions,
11
11
  getGeminiSessions
12
12
  } from '../projects.js';
13
+ import {
14
+ findAcpSessionRecord,
15
+ listAcpSessions
16
+ } from '../acp-runtime/session-store.js';
13
17
 
14
18
  async function flattenLegacyMessages(result) {
15
19
  if (Array.isArray(result)) return result;
@@ -28,47 +32,131 @@ async function normalizeLegacyLoadResult(result, provider, sessionId) {
28
32
 
29
33
  return {
30
34
  ...result,
31
- events
35
+ events,
36
+ source: 'legacy'
37
+ };
38
+ }
39
+
40
+ function mergeSessionLists(legacySessions = [], acpSessions = []) {
41
+ const merged = new Map();
42
+
43
+ legacySessions.forEach((session) => {
44
+ if (session?.id) {
45
+ merged.set(session.id, session);
46
+ }
47
+ });
48
+
49
+ acpSessions.forEach((session) => {
50
+ if (session?.id) {
51
+ merged.set(session.id, session);
52
+ }
53
+ });
54
+
55
+ return Array.from(merged.values()).sort((left, right) => {
56
+ const leftTime = new Date(left?.lastActivity || left?.updatedAt || left?.createdAt || 0).getTime();
57
+ const rightTime = new Date(right?.lastActivity || right?.updatedAt || right?.createdAt || 0).getTime();
58
+ return rightTime - leftTime;
59
+ });
60
+ }
61
+
62
+ async function loadAcpEvents(provider, sessionId) {
63
+ const record = await findAcpSessionRecord(sessionId, provider);
64
+ if (!record) {
65
+ return null;
66
+ }
67
+
68
+ const events = await readMirroredConversationEvents(provider, sessionId);
69
+ return {
70
+ events,
71
+ total: events.length,
72
+ hasMore: false,
73
+ offset: 0,
74
+ limit: null,
75
+ source: 'acp'
32
76
  };
33
77
  }
34
78
 
35
79
  const PROVIDER_ADAPTERS = {
36
80
  claude: {
37
- async listSessions({ projectName, limit = 50, offset = 0 }) {
38
- const result = await getSessions(projectName, limit, offset);
39
- return (result?.sessions || []).map((session) => ({ ...session, provider: 'claude' }));
81
+ async listSessions({ projectName, projectPath, limit = 50, offset = 0 }) {
82
+ const [result, acpSessions] = await Promise.all([
83
+ getSessions(projectName, limit, offset),
84
+ listAcpSessions({ provider: 'claude', projectPath: projectPath || null })
85
+ ]);
86
+ return mergeSessionLists(
87
+ (result?.sessions || []).map((session) => ({ ...session, provider: 'claude', source: 'legacy' })),
88
+ acpSessions
89
+ );
40
90
  },
41
91
  async loadEvents({ projectName, sessionId, limit = null, offset = 0 }) {
92
+ const acpResult = await loadAcpEvents('claude', sessionId);
93
+ if (acpResult) {
94
+ return acpResult;
95
+ }
96
+
42
97
  const rawMessages = await getSessionMessages(projectName, sessionId, limit, offset);
43
98
  return normalizeLegacyLoadResult(rawMessages, 'claude', sessionId);
44
99
  }
45
100
  },
46
101
  codex: {
47
102
  async listSessions({ projectPath, limit = 50 }) {
48
- const sessions = await getCodexSessions(projectPath, { limit });
49
- return sessions.map((session) => ({ ...session, provider: 'codex' }));
103
+ const [sessions, acpSessions] = await Promise.all([
104
+ getCodexSessions(projectPath, { limit }),
105
+ listAcpSessions({ provider: 'codex', projectPath: projectPath || null })
106
+ ]);
107
+ return mergeSessionLists(
108
+ sessions.map((session) => ({ ...session, provider: 'codex', source: 'legacy' })),
109
+ acpSessions
110
+ );
50
111
  },
51
112
  async loadEvents({ sessionId, limit = null, offset = 0 }) {
113
+ const acpResult = await loadAcpEvents('codex', sessionId);
114
+ if (acpResult) {
115
+ return acpResult;
116
+ }
117
+
52
118
  const rawMessages = await getCodexSessionMessages(sessionId, limit, offset);
53
119
  return normalizeLegacyLoadResult(rawMessages, 'codex', sessionId);
54
120
  }
55
121
  },
56
122
  gemini: {
57
123
  async listSessions({ projectPath, limit = 50 }) {
58
- const sessions = await getGeminiSessions(projectPath, { limit });
59
- return sessions.map((session) => ({ ...session, provider: 'gemini' }));
124
+ const [sessions, acpSessions] = await Promise.all([
125
+ getGeminiSessions(projectPath, { limit }),
126
+ listAcpSessions({ provider: 'gemini', projectPath: projectPath || null })
127
+ ]);
128
+ return mergeSessionLists(
129
+ sessions.map((session) => ({ ...session, provider: 'gemini', source: 'legacy' })),
130
+ acpSessions
131
+ );
60
132
  },
61
133
  async loadEvents({ sessionId, limit = null, offset = 0 }) {
134
+ const acpResult = await loadAcpEvents('gemini', sessionId);
135
+ if (acpResult) {
136
+ return acpResult;
137
+ }
138
+
62
139
  const rawMessages = await getGeminiSessionMessages(sessionId, limit, offset);
63
140
  return normalizeLegacyLoadResult(rawMessages, 'gemini', sessionId);
64
141
  }
65
142
  },
66
143
  opencode: {
67
144
  async listSessions({ projectPath, limit = 50 }) {
68
- const sessions = await getOpencodeSessions(projectPath, { limit });
69
- return sessions.map((session) => ({ ...session, provider: 'opencode' }));
145
+ const [sessions, acpSessions] = await Promise.all([
146
+ getOpencodeSessions(projectPath, { limit }),
147
+ listAcpSessions({ provider: 'opencode', projectPath: projectPath || null })
148
+ ]);
149
+ return mergeSessionLists(
150
+ sessions.map((session) => ({ ...session, provider: 'opencode', source: 'legacy' })),
151
+ acpSessions
152
+ );
70
153
  },
71
154
  async loadEvents({ sessionId, limit = null, offset = 0 }) {
155
+ const acpResult = await loadAcpEvents('opencode', sessionId);
156
+ if (acpResult) {
157
+ return acpResult;
158
+ }
159
+
72
160
  const rawMessages = await getOpencodeSessionMessages(sessionId, limit, offset);
73
161
  return normalizeLegacyLoadResult(rawMessages, 'opencode', sessionId);
74
162
  }
@@ -151,7 +151,12 @@ async function discoverOpenCodeModels(projectPath) {
151
151
  }
152
152
 
153
153
  async function discoverClaudeModels() {
154
- return { models: mapModelOptions(CLAUDE_MODELS.OPTIONS, 'runtime-fallback'), source: 'runtime-fallback', error: null, currentModel: process.env.CLAUDE_MODEL || CLAUDE_MODELS.DEFAULT };
154
+ return {
155
+ models: mapModelOptions(CLAUDE_MODELS.OPTIONS, 'runtime-fallback'),
156
+ source: 'runtime-fallback',
157
+ error: null,
158
+ currentModel: null
159
+ };
155
160
  }
156
161
 
157
162
  async function discoverCodexModels() {
@@ -163,11 +168,11 @@ async function discoverCodexModels() {
163
168
  if (match) currentModel = match[1];
164
169
  } catch {
165
170
  }
166
- return { models: mapModelOptions(CODEX_MODELS.OPTIONS, 'runtime-fallback'), source: 'runtime-fallback', error: null, currentModel: currentModel || CODEX_MODELS.DEFAULT };
171
+ return { models: mapModelOptions(CODEX_MODELS.OPTIONS, 'runtime-fallback'), source: 'runtime-fallback', error: null, currentModel: currentModel || null };
167
172
  }
168
173
 
169
174
  async function discoverGeminiModels() {
170
- return { models: mapModelOptions(GEMINI_MODELS.OPTIONS, 'runtime-fallback'), source: 'runtime-fallback', error: null, currentModel: GEMINI_MODELS.DEFAULT };
175
+ return { models: mapModelOptions(GEMINI_MODELS.OPTIONS, 'runtime-fallback'), source: 'runtime-fallback', error: null, currentModel: null };
171
176
  }
172
177
 
173
178
  const AUTH_CHECKERS = {
@@ -7,10 +7,8 @@ import {
7
7
  } from './eventStore.js';
8
8
  import { getProviderAdapter } from './providerAdapters.js';
9
9
  import { getProjects } from '../projects.js';
10
- import { isClaudeSDKSessionActive } from '../claude-sdk.js';
11
- import { isCodexSessionActive } from '../openai-codex.js';
12
- import { isGeminiSessionActive } from '../gemini-cli.js';
13
- import { isOpencodeSessionActive } from '../opencode-cli.js';
10
+ import { findAcpSessionRecord } from '../acp-runtime/session-store.js';
11
+ import { isAgentSessionActive } from '../acp-runtime/index.js';
14
12
 
15
13
  export const AGENT_RUNTIME_PHASES = {
16
14
  IDLE: 'idle',
@@ -86,6 +84,18 @@ async function resolveSessionProjectContext(provider, sessionId) {
86
84
  return null;
87
85
  }
88
86
 
87
+ const acpRecord = await findAcpSessionRecord(normalizedSessionId, normalizedProvider);
88
+ if (acpRecord) {
89
+ return {
90
+ projectName: null,
91
+ projectPath: acpRecord.projectPath || null,
92
+ session: {
93
+ id: normalizedSessionId,
94
+ source: 'acp'
95
+ }
96
+ };
97
+ }
98
+
89
99
  const projects = await getProjects();
90
100
 
91
101
  for (const project of projects) {
@@ -149,19 +159,7 @@ function isSessionActive(provider, sessionId) {
149
159
  return false;
150
160
  }
151
161
 
152
- if (normalizedProvider === 'codex') {
153
- return Boolean(isCodexSessionActive(normalizedSessionId));
154
- }
155
-
156
- if (normalizedProvider === 'gemini') {
157
- return Boolean(isGeminiSessionActive(normalizedSessionId));
158
- }
159
-
160
- if (normalizedProvider === 'opencode') {
161
- return Boolean(isOpencodeSessionActive(normalizedSessionId));
162
- }
163
-
164
- return Boolean(isClaudeSDKSessionActive(normalizedSessionId));
162
+ return Boolean(isAgentSessionActive(normalizedProvider, normalizedSessionId));
165
163
  }
166
164
 
167
165
  function inferPhaseFromEvents(events = []) {
@@ -204,6 +202,7 @@ function inferPhaseFromEvents(events = []) {
204
202
  event.kind === CONVERSATION_EVENT_KINDS.TOOL_CALL_INPUT ||
205
203
  event.kind === CONVERSATION_EVENT_KINDS.TOOL_CALL_END ||
206
204
  event.kind === CONVERSATION_EVENT_KINDS.TOOL_RESULT ||
205
+ event.kind === CONVERSATION_EVENT_KINDS.PLAN_UPDATE ||
207
206
  event.kind === CONVERSATION_EVENT_KINDS.SYSTEM_NOTICE
208
207
  ) {
209
208
  inferredPhase = AGENT_RUNTIME_PHASES.STREAMING;