@mrpatronz/nexusflow 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/agent/AcpCliAdapter.d.ts +75 -0
- package/dist/agent/AcpCliAdapter.d.ts.map +1 -0
- package/dist/agent/AcpCliAdapter.js +292 -0
- package/dist/agent/AcpCliAdapter.js.map +1 -0
- package/dist/agent/AcpCliAdapter.test.d.ts +2 -0
- package/dist/agent/AcpCliAdapter.test.d.ts.map +1 -0
- package/dist/agent/AcpCliAdapter.test.js +235 -0
- package/dist/agent/AcpCliAdapter.test.js.map +1 -0
- package/dist/agent/ClaudeCliAdapter.d.ts +40 -5
- package/dist/agent/ClaudeCliAdapter.d.ts.map +1 -1
- package/dist/agent/ClaudeCliAdapter.js +212 -15
- package/dist/agent/ClaudeCliAdapter.js.map +1 -1
- package/dist/agent/ClaudeCliAdapter.test.d.ts +2 -0
- package/dist/agent/ClaudeCliAdapter.test.d.ts.map +1 -0
- package/dist/agent/ClaudeCliAdapter.test.js +320 -0
- package/dist/agent/ClaudeCliAdapter.test.js.map +1 -0
- package/dist/agent/CliAdapterBase.d.ts +29 -12
- package/dist/agent/CliAdapterBase.d.ts.map +1 -1
- package/dist/agent/CliAdapterBase.js +76 -36
- package/dist/agent/CliAdapterBase.js.map +1 -1
- package/dist/agent/CodexCliAdapter.d.ts +51 -0
- package/dist/agent/CodexCliAdapter.d.ts.map +1 -0
- package/dist/agent/CodexCliAdapter.js +226 -0
- package/dist/agent/CodexCliAdapter.js.map +1 -0
- package/dist/agent/CodexCliAdapter.test.d.ts +2 -0
- package/dist/agent/CodexCliAdapter.test.d.ts.map +1 -0
- package/dist/agent/CodexCliAdapter.test.js +316 -0
- package/dist/agent/CodexCliAdapter.test.js.map +1 -0
- package/dist/agent/CopilotAcpAdapter.d.ts +7 -0
- package/dist/agent/CopilotAcpAdapter.d.ts.map +1 -0
- package/dist/agent/CopilotAcpAdapter.js +29 -0
- package/dist/agent/CopilotAcpAdapter.js.map +1 -0
- package/dist/agent/ProviderRegistry.d.ts +39 -3
- package/dist/agent/ProviderRegistry.d.ts.map +1 -1
- package/dist/agent/ProviderRegistry.js +43 -8
- package/dist/agent/ProviderRegistry.js.map +1 -1
- package/dist/agent/ProviderRegistry.test.js +52 -0
- package/dist/agent/ProviderRegistry.test.js.map +1 -1
- package/dist/agent/adapters.d.ts +7 -5
- package/dist/agent/adapters.d.ts.map +1 -1
- package/dist/agent/adapters.js +92 -17
- package/dist/agent/adapters.js.map +1 -1
- package/dist/agent/adapters.test.js +22 -0
- package/dist/agent/adapters.test.js.map +1 -1
- package/dist/agent/cliAvailability.d.ts +51 -13
- package/dist/agent/cliAvailability.d.ts.map +1 -1
- package/dist/agent/cliAvailability.js +163 -44
- package/dist/agent/cliAvailability.js.map +1 -1
- package/dist/agent/cliAvailability.test.js +140 -53
- package/dist/agent/cliAvailability.test.js.map +1 -1
- package/dist/agent/session.d.ts +2 -1
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +15 -4
- package/dist/agent/session.js.map +1 -1
- package/dist/agent/session.test.js +29 -8
- package/dist/agent/session.test.js.map +1 -1
- package/dist/core/workspace.d.ts +6 -0
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +17 -7
- package/dist/core/workspace.js.map +1 -1
- package/dist/core/workspace.test.js +12 -1
- package/dist/core/workspace.test.js.map +1 -1
- package/dist/gui/assets/DashboardPage-CUspfCap.js +1 -0
- package/dist/gui/assets/GettingStartedPage-BqtgARZW.js +1 -0
- package/dist/gui/assets/ProjectsPage-UBoQxI2G.js +1 -0
- package/dist/gui/assets/{ScaffoldRepoInline-D-2ZeuDK.js → ScaffoldRepoInline-DqoourJe.js} +1 -1
- package/dist/gui/assets/SettingsPage-BZl24tqD.js +1 -0
- package/dist/gui/assets/StartWorkPage-BGNFdBfj.js +1 -0
- package/dist/gui/assets/{StrategiesPage-Cizm0GWk.js → StrategiesPage-D0Y6A4Tw.js} +2 -2
- package/dist/gui/assets/WorkspaceLauncher-Cxv0O_wt.js +1 -0
- package/dist/gui/assets/WorkspacesPage-Cvb1rOeh.js +6 -0
- package/dist/gui/assets/index-CllPiiFL.js +52 -0
- package/dist/gui/assets/index-DGCBhMAn.css +2 -0
- package/dist/gui/index.html +2 -2
- package/dist/server.d.ts +13 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +267 -39
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +324 -18
- package/dist/server.test.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/detect-editors.d.ts.map +1 -1
- package/dist/utils/detect-editors.js +6 -0
- package/dist/utils/detect-editors.js.map +1 -1
- package/dist/utils/detect-editors.test.js +12 -0
- package/dist/utils/detect-editors.test.js.map +1 -1
- package/dist/utils/session-finder.d.ts +14 -0
- package/dist/utils/session-finder.d.ts.map +1 -1
- package/dist/utils/session-finder.js +127 -10
- package/dist/utils/session-finder.js.map +1 -1
- package/dist/utils/session-finder.test.js +104 -2
- package/dist/utils/session-finder.test.js.map +1 -1
- package/dist/utils/workspace-launch.d.ts +37 -0
- package/dist/utils/workspace-launch.d.ts.map +1 -0
- package/dist/utils/workspace-launch.js +200 -0
- package/dist/utils/workspace-launch.js.map +1 -0
- package/dist/utils/workspace-launch.test.d.ts +2 -0
- package/dist/utils/workspace-launch.test.d.ts.map +1 -0
- package/dist/utils/workspace-launch.test.js +94 -0
- package/dist/utils/workspace-launch.test.js.map +1 -0
- package/package.json +5 -3
- package/dist/gui/assets/DashboardPage-HNC3-OP0.js +0 -1
- package/dist/gui/assets/GettingStartedPage-Dguoya_Z.js +0 -1
- package/dist/gui/assets/ProjectsPage-DVR4j4sw.js +0 -1
- package/dist/gui/assets/SettingsPage-Bauo77q3.js +0 -1
- package/dist/gui/assets/StartWorkPage-DvuFEa5Y.js +0 -1
- package/dist/gui/assets/WorkspacesPage-DYOVfZqy.js +0 -6
- package/dist/gui/assets/external-link-BtYhMxno.js +0 -1
- package/dist/gui/assets/index-BqeoAIB1.js +0 -52
- package/dist/gui/assets/index-CsA2SueF.css +0 -2
package/dist/server.js
CHANGED
|
@@ -23,13 +23,14 @@ import { createNewRepo } from './core/new-repo.js';
|
|
|
23
23
|
import { loadProjects, createProject, updateProject, removeProject, slugifyProjectName } from './core/projects.js';
|
|
24
24
|
import { getSessionCwd, isInPlace, resolveFeatureRepoPath } from './utils/feature.js';
|
|
25
25
|
import { listBranches } from './utils/git.js';
|
|
26
|
-
import { createWorkspace, listWorkspaces, loadFeatureConfig, deleteWorkspace, addRepoToWorkspace } from './core/workspace.js';
|
|
26
|
+
import { createWorkspace, listWorkspaces, loadFeatureConfig, loadWorkspaceManifest, deleteWorkspace, addRepoToWorkspace } from './core/workspace.js';
|
|
27
27
|
import { loadWorkspaceState } from './core/workspace-state.js';
|
|
28
28
|
import { analyzeAllRepos } from './analyzers/index.js';
|
|
29
29
|
import { generateContextFiles } from './generators/index.js';
|
|
30
30
|
import { detectAIAssistants } from './utils/detect-ai.js';
|
|
31
31
|
import { detectEditors } from './utils/detect-editors.js';
|
|
32
|
-
import {
|
|
32
|
+
import { buildWorkspaceLaunchPrompt, detectWorkspaceLaunchTargets, launchTargetIdForEditorCommand, launchWorkspaceTarget, } from './utils/workspace-launch.js';
|
|
33
|
+
import { canOpenCodexSessionInWorkspace, findSessions, getSessionTranscript } from './utils/session-finder.js';
|
|
33
34
|
import { ProviderRegistry } from './agent/adapters.js';
|
|
34
35
|
import { isValidSessionUuid } from './agent/session.js';
|
|
35
36
|
import { getRepoStatus } from './utils/multi-git.js';
|
|
@@ -51,9 +52,45 @@ const __dirname = path.dirname(__filename);
|
|
|
51
52
|
const guiPath = path.join(__dirname, 'gui');
|
|
52
53
|
export const app = new Hono();
|
|
53
54
|
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
function hasTrustedLocalOrigin(origin) {
|
|
56
|
+
if (!origin)
|
|
57
|
+
return false;
|
|
58
|
+
try {
|
|
59
|
+
const { hostname } = new URL(origin);
|
|
60
|
+
return ['localhost', '127.0.0.1', '::1', '[::1]'].includes(hostname);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Validate and dispatch one renderer turn without letting malformed authority reach a harness. */
|
|
67
|
+
export function dispatchAgentInput(agent, provider, payload) {
|
|
68
|
+
if (typeof payload.input !== 'string' || !payload.input.trim()) {
|
|
69
|
+
return 'A non-empty input message is required.';
|
|
70
|
+
}
|
|
71
|
+
const executionProfile = ProviderRegistry.resolveExecutionProfile(provider, payload.executionProfile);
|
|
72
|
+
if (executionProfile === null) {
|
|
73
|
+
return 'Select a supported execution profile before sending this turn.';
|
|
74
|
+
}
|
|
75
|
+
void agent.send(payload.input, executionProfile);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** Per-socket admission guard so a second prompt is rejected, never dropped. */
|
|
79
|
+
export class AgentTurnGate {
|
|
80
|
+
active = false;
|
|
81
|
+
tryBegin() {
|
|
82
|
+
if (this.active)
|
|
83
|
+
return false;
|
|
84
|
+
this.active = true;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
settle() {
|
|
88
|
+
this.active = false;
|
|
89
|
+
}
|
|
90
|
+
isActive() {
|
|
91
|
+
return this.active;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
57
94
|
app.get('/ws', async (c, next) => {
|
|
58
95
|
// Prevent Cross-Site WebSocket Hijacking (CSWSH)
|
|
59
96
|
const origin = c.req.header('origin');
|
|
@@ -69,11 +106,16 @@ app.get('/ws', async (c, next) => {
|
|
|
69
106
|
}
|
|
70
107
|
}
|
|
71
108
|
// Chat protocol:
|
|
72
|
-
// client -> server: {type:'start', command, cwd, sessionId?, resume?}
|
|
73
|
-
//
|
|
74
|
-
//
|
|
109
|
+
// client -> server: {type:'start', command, cwd, sessionId?, resume?}
|
|
110
|
+
// | {type:'input', input, turnId?, executionProfile?} | {type:'stop'} | 'ping'
|
|
111
|
+
// server -> client: {type:'stream', text} | {type:'session', id} | {type:'status', state:'busy'|'idle'} | {type:'system', message}
|
|
112
|
+
// | {type:'accepted', turnId?} | {type:'error', message}
|
|
113
|
+
// | {type:'rejected', reason:'busy', message, turnId?}
|
|
114
|
+
// | {type:'close', code} | {type:'pong'}
|
|
75
115
|
return upgradeWebSocket((c) => {
|
|
76
116
|
let agent = null;
|
|
117
|
+
let activeProvider = null;
|
|
118
|
+
const turnGate = new AgentTurnGate();
|
|
77
119
|
return {
|
|
78
120
|
onMessage(event, ws) {
|
|
79
121
|
if (typeof event.data === 'string') {
|
|
@@ -87,8 +129,12 @@ app.get('/ws', async (c, next) => {
|
|
|
87
129
|
ws.send(JSON.stringify({ type: 'pong' }));
|
|
88
130
|
}
|
|
89
131
|
else if (payload.type === 'start') {
|
|
132
|
+
turnGate.settle();
|
|
90
133
|
if (agent) {
|
|
91
|
-
agent
|
|
134
|
+
const previousAgent = agent;
|
|
135
|
+
agent = null;
|
|
136
|
+
activeProvider = null;
|
|
137
|
+
previousAgent.stop();
|
|
92
138
|
}
|
|
93
139
|
const provider = ProviderRegistry.getProvider(payload.command);
|
|
94
140
|
if (!provider) {
|
|
@@ -103,35 +149,86 @@ app.get('/ws', async (c, next) => {
|
|
|
103
149
|
}
|
|
104
150
|
session = { id: payload.sessionId, resume: Boolean(payload.resume) };
|
|
105
151
|
}
|
|
106
|
-
|
|
107
|
-
agent
|
|
152
|
+
const startedAgent = provider.createInstance();
|
|
153
|
+
agent = startedAgent;
|
|
154
|
+
activeProvider = provider;
|
|
155
|
+
const isCurrentAgent = () => agent === startedAgent;
|
|
156
|
+
startedAgent.on('data', (text) => {
|
|
157
|
+
if (!isCurrentAgent())
|
|
158
|
+
return;
|
|
108
159
|
ws.send(JSON.stringify({ type: 'stream', text }));
|
|
109
160
|
});
|
|
110
|
-
|
|
161
|
+
startedAgent.on('system', (message) => {
|
|
162
|
+
if (!isCurrentAgent())
|
|
163
|
+
return;
|
|
111
164
|
ws.send(JSON.stringify({ type: 'system', message }));
|
|
112
165
|
});
|
|
113
|
-
|
|
166
|
+
startedAgent.on('session', (id) => {
|
|
167
|
+
if (!isCurrentAgent())
|
|
168
|
+
return;
|
|
169
|
+
// Provider output is still boundary input. Only inert UUIDs
|
|
170
|
+
// may be persisted by the renderer and sent back in argv.
|
|
171
|
+
if (isValidSessionUuid(id)) {
|
|
172
|
+
ws.send(JSON.stringify({ type: 'session', id }));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
startedAgent.on('idle', () => {
|
|
176
|
+
if (!isCurrentAgent())
|
|
177
|
+
return;
|
|
178
|
+
turnGate.settle();
|
|
114
179
|
ws.send(JSON.stringify({ type: 'status', state: 'idle' }));
|
|
115
180
|
});
|
|
116
|
-
|
|
181
|
+
startedAgent.on('close', (code) => {
|
|
182
|
+
if (!isCurrentAgent())
|
|
183
|
+
return;
|
|
184
|
+
turnGate.settle();
|
|
117
185
|
ws.send(JSON.stringify({ type: 'close', code }));
|
|
118
186
|
});
|
|
119
|
-
|
|
187
|
+
startedAgent.on('error', (error) => {
|
|
188
|
+
if (!isCurrentAgent())
|
|
189
|
+
return;
|
|
120
190
|
ws.send(JSON.stringify({ type: 'error', message: error?.message ?? String(error) }));
|
|
121
|
-
ws.send(JSON.stringify({ type: 'status', state: 'idle' }));
|
|
122
191
|
});
|
|
123
|
-
|
|
192
|
+
startedAgent.start(payload.cwd, session);
|
|
124
193
|
}
|
|
125
194
|
else if (payload.type === 'input') {
|
|
126
|
-
if (agent) {
|
|
127
|
-
|
|
128
|
-
|
|
195
|
+
if (agent && activeProvider) {
|
|
196
|
+
if (!turnGate.tryBegin()) {
|
|
197
|
+
const turnId = isValidSessionUuid(payload.turnId) ? payload.turnId : undefined;
|
|
198
|
+
ws.send(JSON.stringify({
|
|
199
|
+
type: 'rejected',
|
|
200
|
+
reason: 'busy',
|
|
201
|
+
message: 'The agent is still processing the current turn.',
|
|
202
|
+
...(turnId ? { turnId } : {}),
|
|
203
|
+
}));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const error = dispatchAgentInput(agent, activeProvider, payload);
|
|
207
|
+
if (error) {
|
|
208
|
+
turnGate.settle();
|
|
209
|
+
ws.send(JSON.stringify({ type: 'error', message: error }));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Some harness validation failures emit error + idle
|
|
213
|
+
// synchronously from send(); do not overwrite that settled
|
|
214
|
+
// state with a late busy frame.
|
|
215
|
+
if (turnGate.isActive()) {
|
|
216
|
+
const turnId = isValidSessionUuid(payload.turnId) ? payload.turnId : undefined;
|
|
217
|
+
ws.send(JSON.stringify({
|
|
218
|
+
type: 'accepted',
|
|
219
|
+
...(turnId ? { turnId } : {}),
|
|
220
|
+
}));
|
|
221
|
+
ws.send(JSON.stringify({ type: 'status', state: 'busy' }));
|
|
222
|
+
}
|
|
129
223
|
}
|
|
130
224
|
}
|
|
131
225
|
else if (payload.type === 'stop') {
|
|
226
|
+
turnGate.settle();
|
|
132
227
|
if (agent) {
|
|
133
|
-
agent
|
|
228
|
+
const stoppedAgent = agent;
|
|
134
229
|
agent = null;
|
|
230
|
+
activeProvider = null;
|
|
231
|
+
stoppedAgent.stop();
|
|
135
232
|
}
|
|
136
233
|
}
|
|
137
234
|
}
|
|
@@ -145,9 +242,12 @@ app.get('/ws', async (c, next) => {
|
|
|
145
242
|
},
|
|
146
243
|
onClose(_event, _ws) {
|
|
147
244
|
console.log('[WS] Client disconnected');
|
|
245
|
+
turnGate.settle();
|
|
148
246
|
if (agent) {
|
|
149
|
-
agent
|
|
247
|
+
const disconnectedAgent = agent;
|
|
150
248
|
agent = null;
|
|
249
|
+
activeProvider = null;
|
|
250
|
+
disconnectedAgent.stop();
|
|
151
251
|
}
|
|
152
252
|
},
|
|
153
253
|
};
|
|
@@ -182,6 +282,31 @@ function assertWithin(baseDir, target) {
|
|
|
182
282
|
export function resolveWorkspacePath(workspacesDir, id) {
|
|
183
283
|
return assertWithin(workspacesDir, path.join(workspacesDir, id));
|
|
184
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Resolve a launch path through the filesystem and require it to be the exact
|
|
287
|
+
* workspace declared by its manifest. This prevents symlink escapes and stops
|
|
288
|
+
* loadFeatureConfig's parent-directory fallback from authorizing a child path.
|
|
289
|
+
*/
|
|
290
|
+
async function resolveExactLaunchWorkspace(workspacesDir, candidatePath) {
|
|
291
|
+
const lexicalPath = assertWithin(workspacesDir, candidatePath);
|
|
292
|
+
const [canonicalRoot, canonicalWorkspace] = await Promise.all([
|
|
293
|
+
fs.realpath(workspacesDir),
|
|
294
|
+
fs.realpath(lexicalPath),
|
|
295
|
+
]);
|
|
296
|
+
const safeWorkspacePath = assertWithin(canonicalRoot, canonicalWorkspace);
|
|
297
|
+
const feature = await loadWorkspaceManifest(safeWorkspacePath);
|
|
298
|
+
if (!feature || typeof feature.workspacePath !== 'string')
|
|
299
|
+
return null;
|
|
300
|
+
try {
|
|
301
|
+
const declaredWorkspace = await fs.realpath(path.resolve(feature.workspacePath));
|
|
302
|
+
return path.resolve(declaredWorkspace) === path.resolve(safeWorkspacePath)
|
|
303
|
+
? safeWorkspacePath
|
|
304
|
+
: null;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
185
310
|
/** Safe sub-repo path for a repo name, contained within the workspace. */
|
|
186
311
|
export function resolveRepoPath(workspacePath, repoName) {
|
|
187
312
|
return assertWithin(workspacePath, path.join(workspacePath, repoName));
|
|
@@ -236,6 +361,29 @@ app.use('/api/*', cors({
|
|
|
236
361
|
},
|
|
237
362
|
}));
|
|
238
363
|
// ─── API Endpoints ────────────────────────────────────────────────────────
|
|
364
|
+
// Status checks may execute a trusted local CLI from PATH. Keep that work
|
|
365
|
+
// behind a non-simple, same-machine POST so a remote page cannot trigger it
|
|
366
|
+
// with an image, link, or cross-origin fetch. These routes intentionally sit
|
|
367
|
+
// after the API CORS middleware so the separate Vite dev origin can read them.
|
|
368
|
+
app.get('/api/adapters/status', (c) => {
|
|
369
|
+
return c.json({ error: 'Use the same-origin POST status endpoint.' }, 405);
|
|
370
|
+
});
|
|
371
|
+
app.post('/api/adapters/status', (c) => {
|
|
372
|
+
if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
|
|
373
|
+
return c.json({ error: 'A local browser origin is required.' }, 403);
|
|
374
|
+
}
|
|
375
|
+
return c.json(ProviderRegistry.getAllStatus());
|
|
376
|
+
});
|
|
377
|
+
app.post('/api/adapters/status/refresh', async (c) => {
|
|
378
|
+
if (!hasTrustedLocalOrigin(c.req.header('origin'))) {
|
|
379
|
+
return c.json({ error: 'A local browser origin is required.' }, 403);
|
|
380
|
+
}
|
|
381
|
+
const body = await c.req.json().catch(() => null);
|
|
382
|
+
if (body?.providerId !== 'claude-cli' && body?.providerId !== 'codex-cli') {
|
|
383
|
+
return c.json({ error: 'Only Claude Code and Codex status can be refreshed.' }, 400);
|
|
384
|
+
}
|
|
385
|
+
return c.json(ProviderRegistry.getAllStatus({ refreshProviderId: body.providerId }));
|
|
386
|
+
});
|
|
239
387
|
// 1. Get current configuration
|
|
240
388
|
app.get('/api/config', async (c) => {
|
|
241
389
|
try {
|
|
@@ -787,16 +935,93 @@ app.post('/api/workspace/suggest-workflow', async (c) => {
|
|
|
787
935
|
return errorResponse(c, error);
|
|
788
936
|
}
|
|
789
937
|
});
|
|
790
|
-
// 8.
|
|
938
|
+
// 8. List the closed, server-owned launch catalog.
|
|
939
|
+
app.get('/api/workspace-launch-targets', async (c) => {
|
|
940
|
+
try {
|
|
941
|
+
return c.json(await detectWorkspaceLaunchTargets());
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
return errorResponse(c, error);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
// 8a. Open a real NexusFlow workspace in a selected desktop app or editor.
|
|
948
|
+
app.post('/api/workspace/:id/launch', async (c) => {
|
|
949
|
+
try {
|
|
950
|
+
const id = decodeURIComponent(c.req.param('id'));
|
|
951
|
+
const { targetId, action = 'new', sessionId } = await c.req.json().catch(() => ({}));
|
|
952
|
+
if (typeof targetId !== 'string' || !targetId) {
|
|
953
|
+
return c.json({ error: 'A workspace launch target is required.' }, 400);
|
|
954
|
+
}
|
|
955
|
+
if (action !== 'new' && action !== 'resume') {
|
|
956
|
+
return c.json({ error: 'Unknown workspace launch action.' }, 400);
|
|
957
|
+
}
|
|
958
|
+
if (action === 'new' && sessionId !== undefined) {
|
|
959
|
+
return c.json({ error: 'A new workspace launch cannot include a session id.' }, 400);
|
|
960
|
+
}
|
|
961
|
+
const config = await loadConfig();
|
|
962
|
+
const requestedWorkspacePath = resolveWorkspacePath(config.workspacesDir, id);
|
|
963
|
+
let workspacePath;
|
|
964
|
+
try {
|
|
965
|
+
workspacePath = await resolveExactLaunchWorkspace(config.workspacesDir, requestedWorkspacePath);
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
if (error instanceof PathAccessError)
|
|
969
|
+
throw error;
|
|
970
|
+
return c.json({ error: 'Workspace configuration not found.' }, 404);
|
|
971
|
+
}
|
|
972
|
+
if (!workspacePath) {
|
|
973
|
+
return c.json({ error: 'Workspace configuration not found.' }, 404);
|
|
974
|
+
}
|
|
975
|
+
const feature = await loadWorkspaceManifest(workspacePath);
|
|
976
|
+
if (!feature) {
|
|
977
|
+
return c.json({ error: 'Workspace configuration not found.' }, 404);
|
|
978
|
+
}
|
|
979
|
+
const targets = await detectWorkspaceLaunchTargets();
|
|
980
|
+
const target = targets.find((candidate) => candidate.id === targetId);
|
|
981
|
+
if (!target)
|
|
982
|
+
return c.json({ error: 'Unknown workspace launch target.' }, 400);
|
|
983
|
+
if (!target.available) {
|
|
984
|
+
return c.json({ error: target.unavailableReason ?? `${target.name} is unavailable.` }, 409);
|
|
985
|
+
}
|
|
986
|
+
if (action === 'resume') {
|
|
987
|
+
if (targetId !== 'codex-desktop') {
|
|
988
|
+
return c.json({ error: 'This app cannot open an existing coding session.' }, 400);
|
|
989
|
+
}
|
|
990
|
+
if (typeof sessionId !== 'string' || !isValidSessionUuid(sessionId)) {
|
|
991
|
+
return c.json({ error: 'A valid Codex session id is required.' }, 400);
|
|
992
|
+
}
|
|
993
|
+
const ownsSession = await canOpenCodexSessionInWorkspace(workspacePath, feature.repos, sessionId);
|
|
994
|
+
if (!ownsSession) {
|
|
995
|
+
return c.json({ error: 'Codex session not found in this workspace.' }, 404);
|
|
996
|
+
}
|
|
997
|
+
await launchWorkspaceTarget(targetId, workspacePath, { kind: 'resume-session', sessionId });
|
|
998
|
+
return c.json({ success: true, targetId, action, sessionId });
|
|
999
|
+
}
|
|
1000
|
+
await launchWorkspaceTarget(targetId, workspacePath, {
|
|
1001
|
+
kind: 'new-workspace',
|
|
1002
|
+
prompt: buildWorkspaceLaunchPrompt(feature),
|
|
1003
|
+
});
|
|
1004
|
+
return c.json({ success: true, targetId, action });
|
|
1005
|
+
}
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
return errorResponse(c, error);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
// 8b. Legacy editor route retained for older GUI clients using recognized
|
|
1011
|
+
// graphical editors. Interactive terminal editors are intentionally rejected:
|
|
1012
|
+
// a detached HTTP request cannot safely provide their required TTY.
|
|
791
1013
|
app.post('/api/open-editor', async (c) => {
|
|
792
1014
|
try {
|
|
793
1015
|
const { workspacePath, command } = await c.req.json();
|
|
794
|
-
|
|
1016
|
+
const targetId = launchTargetIdForEditorCommand(command);
|
|
1017
|
+
if (!ALLOWED_EDITORS.has(command) || !targetId) {
|
|
795
1018
|
return c.json({ error: 'Forbidden editor command' }, 400);
|
|
796
1019
|
}
|
|
797
|
-
|
|
1020
|
+
const config = await loadConfig();
|
|
1021
|
+
const safeWorkspacePath = assertWithin(config.workspacesDir, workspacePath);
|
|
1022
|
+
// Validate this is an existing NexusFlow workspace, not an arbitrary path.
|
|
798
1023
|
try {
|
|
799
|
-
const stats = await fs.stat(
|
|
1024
|
+
const stats = await fs.stat(safeWorkspacePath);
|
|
800
1025
|
if (!stats.isDirectory()) {
|
|
801
1026
|
return c.json({ error: 'Workspace path is not a directory' }, 400);
|
|
802
1027
|
}
|
|
@@ -804,18 +1029,11 @@ app.post('/api/open-editor', async (c) => {
|
|
|
804
1029
|
catch {
|
|
805
1030
|
return c.json({ error: 'Workspace path does not exist' }, 400);
|
|
806
1031
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
shell: isWin,
|
|
813
|
-
cleanup: false,
|
|
814
|
-
});
|
|
815
|
-
child.unref();
|
|
816
|
-
child.catch((err) => {
|
|
817
|
-
console.error(`Failed to launch editor ${command} for path ${workspacePath}:`, err);
|
|
818
|
-
});
|
|
1032
|
+
const exactWorkspacePath = await resolveExactLaunchWorkspace(config.workspacesDir, safeWorkspacePath);
|
|
1033
|
+
if (!exactWorkspacePath) {
|
|
1034
|
+
return c.json({ error: 'Workspace configuration not found.' }, 404);
|
|
1035
|
+
}
|
|
1036
|
+
await launchWorkspaceTarget(targetId, exactWorkspacePath);
|
|
819
1037
|
return c.json({ success: true });
|
|
820
1038
|
}
|
|
821
1039
|
catch (error) {
|
|
@@ -1380,6 +1598,9 @@ app.get('/api/session/:assistant/:sessionId/transcript', async (c) => {
|
|
|
1380
1598
|
const assistant = c.req.param('assistant');
|
|
1381
1599
|
const sessionId = c.req.param('sessionId');
|
|
1382
1600
|
try {
|
|
1601
|
+
if (assistant === 'codex' && !isValidSessionUuid(sessionId)) {
|
|
1602
|
+
return c.json({ error: 'Invalid Codex session id.' }, 400);
|
|
1603
|
+
}
|
|
1383
1604
|
const messages = await getSessionTranscript(assistant, sessionId);
|
|
1384
1605
|
return c.json({ messages });
|
|
1385
1606
|
}
|
|
@@ -1559,6 +1780,7 @@ app.post('/api/workflows/templates/:id/analyze', async (c) => {
|
|
|
1559
1780
|
const selectedAssistant = assistant || 'antigravity';
|
|
1560
1781
|
let command = '';
|
|
1561
1782
|
let args = [];
|
|
1783
|
+
let commandInput = '';
|
|
1562
1784
|
let prompt = `You are an expert AI system engineering reviewer. Analyze the following Agent Teamwork Strategy guidelines.
|
|
1563
1785
|
Evaluate its instructions, identify any ambiguities or contradictions, rate its expected effectiveness for orchestrating subagents, and provide specific recommendations or improvements. Format your analysis in clean Markdown with clear headings (e.g. Overview, Strengths, Weaknesses, Recommendations).
|
|
1564
1786
|
|
|
@@ -1585,11 +1807,17 @@ and suffix it with:
|
|
|
1585
1807
|
else if (command === 'agy') {
|
|
1586
1808
|
args = [prompt];
|
|
1587
1809
|
}
|
|
1810
|
+
else if (command === 'codex') {
|
|
1811
|
+
// Captured stdio cannot host the interactive TUI. `codex exec` is the
|
|
1812
|
+
// supported non-interactive surface and reuses the user's CLI login.
|
|
1813
|
+
args = ['exec', '--color', 'never', '-'];
|
|
1814
|
+
commandInput = prompt;
|
|
1815
|
+
}
|
|
1588
1816
|
else {
|
|
1589
1817
|
args = [prompt];
|
|
1590
1818
|
}
|
|
1591
1819
|
const result = await execa(command, args, {
|
|
1592
|
-
input:
|
|
1820
|
+
input: commandInput,
|
|
1593
1821
|
shell: false,
|
|
1594
1822
|
reject: false
|
|
1595
1823
|
});
|