fraim-framework 2.0.185 → 2.0.187
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/dist/src/ai-hub/conversation-store.js +97 -6
- package/dist/src/ai-hub/hosts.js +127 -15
- package/dist/src/ai-hub/openclaw-bridge.js +17 -6
- package/dist/src/ai-hub/server.js +215 -38
- package/dist/src/cli/commands/add-ide.js +3 -2
- package/dist/src/cli/commands/add-provider.js +7 -1
- package/dist/src/cli/commands/sync.js +2 -1
- package/dist/src/cli/mcp/ide-formats.js +36 -1
- package/dist/src/cli/setup/auto-mcp-setup.js +1 -1
- package/dist/src/cli/setup/ide-detector.js +29 -1
- package/dist/src/cli/setup/ide-global-integration.js +1 -1
- package/dist/src/cli/setup/mcp-config-generator.js +12 -1
- package/dist/src/cli/utils/agent-adapters.js +8 -2
- package/dist/src/first-run/types.js +1 -1
- package/package.json +1 -1
- package/public/ai-hub/script.js +484 -28
- package/public/ai-hub/styles.css +30 -0
|
@@ -227,6 +227,9 @@ function safeHttpUrl(value) {
|
|
|
227
227
|
}
|
|
228
228
|
// ─── Issue #578: Deployment + Host stores ─────────────────────────────────────
|
|
229
229
|
const VALID_EMPLOYEE_IDS = ['codex', 'claude', 'gemini', 'copilot'];
|
|
230
|
+
function startSessionSeedForHost(hostId, runId) {
|
|
231
|
+
return hostId === 'gemini' ? undefined : runId;
|
|
232
|
+
}
|
|
230
233
|
class DeploymentStore {
|
|
231
234
|
constructor(filePath) {
|
|
232
235
|
this.filePath = filePath ?? path_1.default.join(getUserHubDir(), 'hub-deployments.json');
|
|
@@ -587,6 +590,42 @@ function emptyTotals() {
|
|
|
587
590
|
},
|
|
588
591
|
};
|
|
589
592
|
}
|
|
593
|
+
const PHASE_STATUSES = new Set(['starting', 'complete', 'incomplete', 'failure']);
|
|
594
|
+
function normalizePersistedPhaseHistory(value) {
|
|
595
|
+
if (!Array.isArray(value))
|
|
596
|
+
return [];
|
|
597
|
+
return value
|
|
598
|
+
.map((entry) => {
|
|
599
|
+
if (!entry || typeof entry !== 'object')
|
|
600
|
+
return null;
|
|
601
|
+
const raw = entry;
|
|
602
|
+
if (typeof raw.phaseId !== 'string' || !raw.phaseId)
|
|
603
|
+
return null;
|
|
604
|
+
const latestStatus = typeof raw.latestStatus === 'string' && PHASE_STATUSES.has(raw.latestStatus)
|
|
605
|
+
? raw.latestStatus
|
|
606
|
+
: null;
|
|
607
|
+
return {
|
|
608
|
+
phaseId: raw.phaseId,
|
|
609
|
+
enteredAt: typeof raw.enteredAt === 'string' ? raw.enteredAt : new Date().toISOString(),
|
|
610
|
+
latestStatus,
|
|
611
|
+
latestText: typeof raw.latestText === 'string' ? raw.latestText : null,
|
|
612
|
+
};
|
|
613
|
+
})
|
|
614
|
+
.filter((entry) => Boolean(entry));
|
|
615
|
+
}
|
|
616
|
+
function readPersistedRunProjection(conversation) {
|
|
617
|
+
const rawRun = conversation?.run;
|
|
618
|
+
if (!rawRun || typeof rawRun !== 'object')
|
|
619
|
+
return null;
|
|
620
|
+
const value = rawRun;
|
|
621
|
+
return {
|
|
622
|
+
currentPhase: typeof value.currentPhase === 'string' ? value.currentPhase : null,
|
|
623
|
+
phaseHistory: normalizePersistedPhaseHistory(value.phaseHistory),
|
|
624
|
+
stages: Array.isArray(value.stages) ? value.stages : [],
|
|
625
|
+
totals: value.totals && typeof value.totals === 'object' ? value.totals : null,
|
|
626
|
+
runDiscriminant: typeof value.runDiscriminant === 'string' ? value.runDiscriminant : null,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
590
629
|
// Issue #347 — apply per-turn usage from the host stream into the
|
|
591
630
|
// run's tokenTotals. Idempotent on the same turn (we replace, not add)
|
|
592
631
|
// because Codex's `turn.completed.usage` is cumulative, not delta.
|
|
@@ -766,20 +805,17 @@ function deriveStages(run, projectPath) {
|
|
|
766
805
|
}
|
|
767
806
|
return declaredPath.map((phase, index) => {
|
|
768
807
|
let state;
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
else if (index < currentIndex) {
|
|
773
|
-
state = 'done';
|
|
774
|
-
}
|
|
775
|
-
else if (index === currentIndex) {
|
|
808
|
+
const entry = historyMap.get(phase.id);
|
|
809
|
+
if (index === currentIndex) {
|
|
776
810
|
// If the agent has already reported this phase as 'complete', advance
|
|
777
811
|
// its visual state to 'done' so the tracker doesn't look frozen while
|
|
778
812
|
// waiting for the agent to start the next phase (e.g. after
|
|
779
813
|
// implement-submission completes but before address-feedback starts).
|
|
780
|
-
const entry = historyMap.get(phase.id);
|
|
781
814
|
state = entry?.latestStatus === 'complete' ? 'done' : 'current';
|
|
782
815
|
}
|
|
816
|
+
else if (entry?.latestStatus === 'complete' || (currentIndex >= 0 && index < currentIndex && entry)) {
|
|
817
|
+
state = 'done';
|
|
818
|
+
}
|
|
783
819
|
else {
|
|
784
820
|
state = 'upcoming';
|
|
785
821
|
}
|
|
@@ -1399,6 +1435,7 @@ class AiHubServer {
|
|
|
1399
1435
|
}
|
|
1400
1436
|
conversationRecordFromRun(run) {
|
|
1401
1437
|
const lastUpdatedAt = run.updatedAt || new Date().toISOString();
|
|
1438
|
+
const stages = deriveStages(run, run.projectPath);
|
|
1402
1439
|
return {
|
|
1403
1440
|
id: run.conversationId || run.id,
|
|
1404
1441
|
projectPath: path_1.default.resolve(run.projectPath),
|
|
@@ -1433,11 +1470,27 @@ class AiHubServer {
|
|
|
1433
1470
|
compareRunId: run.compareRunId || null,
|
|
1434
1471
|
// Issue #578: preserve trigger source so the UI can render the chip.
|
|
1435
1472
|
sourceTrigger: run.sourceTrigger,
|
|
1473
|
+
// Issue #708: carry the invocation scope so the record lands in (and is keyed to)
|
|
1474
|
+
// the right bucket. Falls back to the legacy client `invokedArea` when present.
|
|
1475
|
+
scope: run.scope
|
|
1476
|
+
?? run.invokedArea
|
|
1477
|
+
?? 'project',
|
|
1478
|
+
run: {
|
|
1479
|
+
stages,
|
|
1480
|
+
currentPhase: run.currentPhase || null,
|
|
1481
|
+
phaseHistory: run.phaseHistory || [],
|
|
1482
|
+
totals: run.totals || null,
|
|
1483
|
+
runDiscriminant: run.runDiscriminant || null,
|
|
1484
|
+
},
|
|
1436
1485
|
};
|
|
1437
1486
|
}
|
|
1438
1487
|
persistRunConversation(run, activeId) {
|
|
1439
1488
|
try {
|
|
1440
|
-
|
|
1489
|
+
// Issue #708: route the record to its scope bucket (manager/company runs get a
|
|
1490
|
+
// project-independent home); project runs continue to key by project path.
|
|
1491
|
+
const record = this.conversationRecordFromRun(run);
|
|
1492
|
+
const key = (0, conversation_store_1.conversationScopeKey)(record.scope, run.projectPath);
|
|
1493
|
+
this.conversationStore.upsertConversation(key, record, activeId);
|
|
1441
1494
|
}
|
|
1442
1495
|
catch (error) {
|
|
1443
1496
|
console.warn('[ai-hub] conversation store write failed:', error instanceof Error ? error.message : error);
|
|
@@ -1544,8 +1597,10 @@ class AiHubServer {
|
|
|
1544
1597
|
}
|
|
1545
1598
|
if (event.agentIdentity)
|
|
1546
1599
|
applyAgentIdentitySignal(current, event.agentIdentity);
|
|
1600
|
+
if (event.fraimJob)
|
|
1601
|
+
this.applyFraimJobSignalToRun(current, event.fraimJob);
|
|
1547
1602
|
if (event.seekMentoring)
|
|
1548
|
-
|
|
1603
|
+
this.applySeekMentoringSignalToRun(current, event.seekMentoring);
|
|
1549
1604
|
if (event.usage)
|
|
1550
1605
|
applyUsageSignal(current, event.usage);
|
|
1551
1606
|
});
|
|
@@ -1567,7 +1622,7 @@ class AiHubServer {
|
|
|
1567
1622
|
}
|
|
1568
1623
|
this.runRegistry.dispose(childRun.id);
|
|
1569
1624
|
},
|
|
1570
|
-
}, childRun.
|
|
1625
|
+
}, startSessionSeedForHost(managerRun.hostId, childRun.id));
|
|
1571
1626
|
this.runRegistry.attachChildIfRunning(childRun.id, child);
|
|
1572
1627
|
}
|
|
1573
1628
|
notifyManagerOfDelegatedChild(managerRunId, childRun) {
|
|
@@ -1691,8 +1746,10 @@ class AiHubServer {
|
|
|
1691
1746
|
}
|
|
1692
1747
|
if (event.agentIdentity)
|
|
1693
1748
|
applyAgentIdentitySignal(current, event.agentIdentity);
|
|
1749
|
+
if (event.fraimJob)
|
|
1750
|
+
this.applyFraimJobSignalToRun(current, event.fraimJob);
|
|
1694
1751
|
if (event.seekMentoring)
|
|
1695
|
-
|
|
1752
|
+
this.applySeekMentoringSignalToRun(current, event.seekMentoring);
|
|
1696
1753
|
if (event.usage)
|
|
1697
1754
|
applyUsageSignal(current, event.usage);
|
|
1698
1755
|
});
|
|
@@ -1764,14 +1821,48 @@ class AiHubServer {
|
|
|
1764
1821
|
};
|
|
1765
1822
|
}
|
|
1766
1823
|
resolveHubJob(projectPath, jobId) {
|
|
1824
|
+
if (!jobId || jobId === '__freeform__')
|
|
1825
|
+
return null;
|
|
1767
1826
|
const employeeJob = (0, catalog_1.discoverEmployeeJobs)(projectPath).find((job) => job.id === jobId);
|
|
1768
|
-
if (employeeJob)
|
|
1769
|
-
return {
|
|
1827
|
+
if (employeeJob) {
|
|
1828
|
+
return {
|
|
1829
|
+
id: employeeJob.id,
|
|
1830
|
+
title: employeeJob.title,
|
|
1831
|
+
stubPath: employeeJob.stubPath,
|
|
1832
|
+
personaKey: employeeJob.requiredPersonaKey ?? getProtectedPersonaForHubJob(employeeJob.id),
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1770
1835
|
const managerTemplate = (0, catalog_1.discoverManagerTemplates)(projectPath).find((job) => job.id === jobId);
|
|
1771
|
-
if (managerTemplate)
|
|
1772
|
-
return {
|
|
1836
|
+
if (managerTemplate) {
|
|
1837
|
+
return {
|
|
1838
|
+
id: managerTemplate.id,
|
|
1839
|
+
title: managerTemplate.title,
|
|
1840
|
+
stubPath: managerTemplate.stubPath,
|
|
1841
|
+
personaKey: getProtectedPersonaForHubJob(managerTemplate.id),
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1773
1844
|
return null;
|
|
1774
1845
|
}
|
|
1846
|
+
applySeekMentoringSignalToRun(run, signal) {
|
|
1847
|
+
this.maybePromoteFreeformRunToJob(run, signal.jobId || signal.jobName);
|
|
1848
|
+
applySeekMentoringSignal(run, signal);
|
|
1849
|
+
}
|
|
1850
|
+
applyFraimJobSignalToRun(run, signal) {
|
|
1851
|
+
this.maybePromoteFreeformRunToJob(run, signal.jobId);
|
|
1852
|
+
}
|
|
1853
|
+
maybePromoteFreeformRunToJob(run, signalJobId) {
|
|
1854
|
+
const normalizedJobId = (signalJobId || '').trim().toLowerCase();
|
|
1855
|
+
if (run.jobId !== '__freeform__' || !normalizedJobId)
|
|
1856
|
+
return;
|
|
1857
|
+
const metadata = this.resolveHubJob(run.projectPath, normalizedJobId);
|
|
1858
|
+
if (!metadata)
|
|
1859
|
+
return;
|
|
1860
|
+
run.jobId = metadata.id;
|
|
1861
|
+
run.jobTitle = metadata.title;
|
|
1862
|
+
run.personaKey = metadata.personaKey;
|
|
1863
|
+
run.updatedAt = new Date().toISOString();
|
|
1864
|
+
run.events.push((0, hosts_1.createHubEvent)('system', `Recognized FRAIM job ${metadata.id} from structured telemetry.`));
|
|
1865
|
+
}
|
|
1775
1866
|
// Lightweight markdown → .docx. Shared by the GET (file path) and POST (inline
|
|
1776
1867
|
// content) export routes so a conversational deliverable with no on-disk file
|
|
1777
1868
|
// can still be downloaded for Word annotation.
|
|
@@ -1979,6 +2070,19 @@ class AiHubServer {
|
|
|
1979
2070
|
}
|
|
1980
2071
|
// Read API key from header — query-param API keys are prohibited (§3.14)
|
|
1981
2072
|
const apiKey = typeof req.headers['x-fraim-api-key'] === 'string' ? req.headers['x-fraim-api-key'] : undefined;
|
|
2073
|
+
// #719: a bare reload (no projectPath query) must land on the last-recorded
|
|
2074
|
+
// workspace project, not the launch folder. The bootstrap saves its resolved
|
|
2075
|
+
// current path back into the projects list, so defaulting to the launch
|
|
2076
|
+
// folder would resurrect a removed launch-folder project on every reload
|
|
2077
|
+
// (tfSwitchProjectFolder already documents the recorded folder as "the
|
|
2078
|
+
// default on next load"). The launch folder stays the fallback when nothing
|
|
2079
|
+
// is recorded or the recorded folder no longer exists.
|
|
2080
|
+
if (!projectPath) {
|
|
2081
|
+
const recorded = this.preferencesStore.load(this.projectPath).projectPath;
|
|
2082
|
+
if (recorded && path_1.default.resolve(recorded) !== path_1.default.resolve(this.projectPath) && fs_1.default.existsSync(recorded)) {
|
|
2083
|
+
projectPath = recorded;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
1982
2086
|
res.json(await this.bootstrapResponse(projectPath || this.projectPath, apiKey));
|
|
1983
2087
|
});
|
|
1984
2088
|
// Issue #512 (S3, R14) — Brain summary as a standalone route, returning the
|
|
@@ -2064,25 +2168,32 @@ class AiHubServer {
|
|
|
2064
2168
|
await this.remoteGateway.removeManagerTeam(apiKey, personaKey);
|
|
2065
2169
|
return res.status(204).end();
|
|
2066
2170
|
});
|
|
2171
|
+
// Issue #708: manager/company scopes resolve to a project-independent sentinel bucket
|
|
2172
|
+
// key (which must bypass path/dir resolution); everything else keys by project path.
|
|
2173
|
+
const scopeParam = (raw) => (raw === 'manager' || raw === 'company') ? raw : undefined;
|
|
2067
2174
|
this.app.get('/api/ai-hub/conversations', (req, res) => {
|
|
2068
|
-
const
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2175
|
+
const scope = scopeParam(req.query.scope);
|
|
2176
|
+
const key = scope
|
|
2177
|
+
? (0, conversation_store_1.conversationScopeKey)(scope, '')
|
|
2178
|
+
: (typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
|
|
2179
|
+
? path_1.default.resolve(req.query.projectPath)
|
|
2180
|
+
: this.projectPath);
|
|
2181
|
+
const loaded = this.conversationStore.loadProject(key);
|
|
2182
|
+
return res.json({ projectPath: key, scope: scope ?? 'project', ...loaded, source: 'disk' });
|
|
2073
2183
|
});
|
|
2074
2184
|
this.app.put('/api/ai-hub/conversations', (req, res) => {
|
|
2075
2185
|
try {
|
|
2076
2186
|
const body = (req.body ?? {});
|
|
2077
|
-
const
|
|
2187
|
+
const scope = scopeParam(body.scope);
|
|
2188
|
+
const key = scope ? (0, conversation_store_1.conversationScopeKey)(scope, '') : ensureDirectoryPath(body.projectPath || this.projectPath);
|
|
2078
2189
|
if (!Array.isArray(body.conversations)) {
|
|
2079
2190
|
return res.status(400).json({ error: 'conversations array required' });
|
|
2080
2191
|
}
|
|
2081
|
-
const saved = this.conversationStore.replaceProject(
|
|
2192
|
+
const saved = this.conversationStore.replaceProject(key, {
|
|
2082
2193
|
activeId: body.activeId ?? null,
|
|
2083
2194
|
conversations: body.conversations,
|
|
2084
2195
|
});
|
|
2085
|
-
return res.json({ projectPath, ...saved, source: 'disk' });
|
|
2196
|
+
return res.json({ projectPath: key, scope: scope ?? 'project', ...saved, source: 'disk' });
|
|
2086
2197
|
}
|
|
2087
2198
|
catch (error) {
|
|
2088
2199
|
return res.status(400).json({ error: error instanceof Error ? error.message : 'Could not persist conversations.' });
|
|
@@ -2091,7 +2202,8 @@ class AiHubServer {
|
|
|
2091
2202
|
this.app.patch('/api/ai-hub/conversations/:conversationId', (req, res) => {
|
|
2092
2203
|
try {
|
|
2093
2204
|
const body = (req.body ?? {});
|
|
2094
|
-
const
|
|
2205
|
+
const scope = scopeParam(body.scope);
|
|
2206
|
+
const projectPath = scope ? (0, conversation_store_1.conversationScopeKey)(scope, '') : ensureDirectoryPath(body.projectPath || this.projectPath);
|
|
2095
2207
|
const saved = this.conversationStore.patchConversation(projectPath, req.params.conversationId, body);
|
|
2096
2208
|
if (body.activeId !== undefined) {
|
|
2097
2209
|
const withActive = this.conversationStore.replaceProject(projectPath, { ...saved, activeId: body.activeId });
|
|
@@ -2123,6 +2235,46 @@ class AiHubServer {
|
|
|
2123
2235
|
return res.status(400).json({ error: error instanceof Error ? error.message : 'Could not persist projects.' });
|
|
2124
2236
|
}
|
|
2125
2237
|
});
|
|
2238
|
+
// Issue #719: DELETE /api/ai-hub/projects/:id — remove a project from the Hub.
|
|
2239
|
+
// PUT is a merge by design and cannot express removal, so removal gets its own
|
|
2240
|
+
// route (same convention as /schedules/:id, /webhooks/:id, /hosts/:id). Deletes
|
|
2241
|
+
// every server-side derivation layer — preferences entry, conversation-store
|
|
2242
|
+
// KEY, project-scoped deployments — so the project cannot resurrect (R3/R6/R9).
|
|
2243
|
+
// Never touches anything under the project's folderPath (R8).
|
|
2244
|
+
this.app.delete('/api/ai-hub/projects/:id', (req, res) => {
|
|
2245
|
+
const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.length > 0
|
|
2246
|
+
? path_1.default.resolve(req.query.projectPath)
|
|
2247
|
+
: this.projectPath;
|
|
2248
|
+
const known = this.knownProjects(projectPath);
|
|
2249
|
+
const entry = known.find((project) => project.id === req.params.id);
|
|
2250
|
+
if (!entry)
|
|
2251
|
+
return res.status(404).json({ error: 'Project not found.' });
|
|
2252
|
+
// The current projectPath is unconditionally re-injected on every load, so
|
|
2253
|
+
// deleting it would resurrect on the next request — reject as a sequencing
|
|
2254
|
+
// error; the client switches workspaces before deleting (R5/D4).
|
|
2255
|
+
if (sameDirectoryPath(entry.folderPath, projectPath)) {
|
|
2256
|
+
return res.status(409).json({ error: 'Cannot remove the current project. Switch to another project first.' });
|
|
2257
|
+
}
|
|
2258
|
+
// A live schedule/webhook would fire later, write a conversation under the
|
|
2259
|
+
// removed folderPath, and resurrect the project in the background (R6).
|
|
2260
|
+
for (const deployment of this.deploymentStore.load()) {
|
|
2261
|
+
if (!deploymentBelongsToProject(deployment, entry.folderPath, this.projectPath))
|
|
2262
|
+
continue;
|
|
2263
|
+
const task = this.cronHandles.get(deployment.id);
|
|
2264
|
+
if (task) {
|
|
2265
|
+
task.stop();
|
|
2266
|
+
this.cronHandles.delete(deployment.id);
|
|
2267
|
+
}
|
|
2268
|
+
this.deploymentStore.delete(deployment.id);
|
|
2269
|
+
}
|
|
2270
|
+
// Delete the conversation-store KEY — an empty list would still re-derive
|
|
2271
|
+
// the project via listProjectPaths (R9).
|
|
2272
|
+
this.conversationStore.removeProject(entry.folderPath);
|
|
2273
|
+
// Persist the filtered list directly: saveKnownProjects unions with the
|
|
2274
|
+
// derived list by design and cannot express removal.
|
|
2275
|
+
const projects = this.preferencesStore.saveProjects(projectPath, known.filter((project) => project.id !== entry.id));
|
|
2276
|
+
return res.json({ ok: true, projects });
|
|
2277
|
+
});
|
|
2126
2278
|
this.app.post('/api/ai-hub/api-key', (req, res) => {
|
|
2127
2279
|
const { apiKey } = req.body;
|
|
2128
2280
|
if (!apiKey || typeof apiKey !== 'string')
|
|
@@ -2355,11 +2507,15 @@ class AiHubServer {
|
|
|
2355
2507
|
throw new Error('Choose a FRAIM job before starting a run.');
|
|
2356
2508
|
}
|
|
2357
2509
|
const startTimestamp = new Date().toISOString();
|
|
2510
|
+
const jobMetadata = this.resolveHubJob(projectPath, jobId);
|
|
2511
|
+
const fallbackJobTitle = typeof req.body.jobTitle === 'string' && req.body.jobTitle.trim()
|
|
2512
|
+
? req.body.jobTitle.trim()
|
|
2513
|
+
: jobId;
|
|
2358
2514
|
const run = {
|
|
2359
2515
|
id: (0, crypto_1.randomUUID)(),
|
|
2360
2516
|
conversationId: typeof req.body.conversationId === 'string' && req.body.conversationId.trim() ? req.body.conversationId.trim() : undefined,
|
|
2361
2517
|
conversationTitle: typeof req.body.conversationTitle === 'string' && req.body.conversationTitle.trim() ? req.body.conversationTitle.trim() : undefined,
|
|
2362
|
-
jobTitle:
|
|
2518
|
+
jobTitle: jobMetadata?.title || fallbackJobTitle,
|
|
2363
2519
|
jobId,
|
|
2364
2520
|
hostId,
|
|
2365
2521
|
projectPath,
|
|
@@ -2373,7 +2529,7 @@ class AiHubServer {
|
|
|
2373
2529
|
phaseHistory: [],
|
|
2374
2530
|
totals: emptyTotals(),
|
|
2375
2531
|
lastStatusChangeAt: startTimestamp,
|
|
2376
|
-
personaKey: getProtectedPersonaForHubJob(jobId),
|
|
2532
|
+
personaKey: jobMetadata?.personaKey ?? getProtectedPersonaForHubJob(jobId),
|
|
2377
2533
|
// Issue #442: mark this as the FRAIM side of an A/B pair when applicable.
|
|
2378
2534
|
...(compareMode === 'ab' ? { runRole: 'fraim' } : {}),
|
|
2379
2535
|
// #0: trigger source — defaults to 'manager' when not provided by the caller.
|
|
@@ -2426,8 +2582,10 @@ class AiHubServer {
|
|
|
2426
2582
|
}
|
|
2427
2583
|
if (event.agentIdentity)
|
|
2428
2584
|
applyAgentIdentitySignal(current, event.agentIdentity);
|
|
2585
|
+
if (event.fraimJob)
|
|
2586
|
+
this.applyFraimJobSignalToRun(current, event.fraimJob);
|
|
2429
2587
|
if (event.seekMentoring)
|
|
2430
|
-
|
|
2588
|
+
this.applySeekMentoringSignalToRun(current, event.seekMentoring);
|
|
2431
2589
|
if (event.usage)
|
|
2432
2590
|
applyUsageSignal(current, event.usage);
|
|
2433
2591
|
});
|
|
@@ -2460,7 +2618,7 @@ class AiHubServer {
|
|
|
2460
2618
|
if (latest)
|
|
2461
2619
|
this.drainPendingDelegatedReviews(latest);
|
|
2462
2620
|
},
|
|
2463
|
-
}, run.
|
|
2621
|
+
}, startSessionSeedForHost(hostId, run.id));
|
|
2464
2622
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
2465
2623
|
// Issue #442: spawn the Direct run via startDirectRun so CliHostRuntime
|
|
2466
2624
|
// uses buildDirectStartPlan (--strict-mcp-config, raw stdin) rather than
|
|
@@ -2489,7 +2647,7 @@ class AiHubServer {
|
|
|
2489
2647
|
});
|
|
2490
2648
|
this.runRegistry.dispose(directId);
|
|
2491
2649
|
},
|
|
2492
|
-
}, directRun.
|
|
2650
|
+
}, startSessionSeedForHost(hostId, directRun.id));
|
|
2493
2651
|
this.runRegistry.attachChildIfRunning(directRun.id, directChild);
|
|
2494
2652
|
}
|
|
2495
2653
|
const existingPreferences = this.preferencesStore.load(projectPath);
|
|
@@ -2615,8 +2773,10 @@ class AiHubServer {
|
|
|
2615
2773
|
}
|
|
2616
2774
|
if (event.agentIdentity)
|
|
2617
2775
|
applyAgentIdentitySignal(current, event.agentIdentity);
|
|
2776
|
+
if (event.fraimJob)
|
|
2777
|
+
this.applyFraimJobSignalToRun(current, event.fraimJob);
|
|
2618
2778
|
if (event.seekMentoring)
|
|
2619
|
-
|
|
2779
|
+
this.applySeekMentoringSignalToRun(current, event.seekMentoring);
|
|
2620
2780
|
if (event.usage)
|
|
2621
2781
|
applyUsageSignal(current, event.usage);
|
|
2622
2782
|
});
|
|
@@ -2677,16 +2837,27 @@ class AiHubServer {
|
|
|
2677
2837
|
if (!employee?.available) {
|
|
2678
2838
|
throw new Error(`${employee?.label || 'Selected employee'} is not available on this machine.`);
|
|
2679
2839
|
}
|
|
2840
|
+
const conversationId = typeof body.conversationId === 'string' && body.conversationId.trim()
|
|
2841
|
+
? body.conversationId.trim()
|
|
2842
|
+
: undefined;
|
|
2843
|
+
const persistedConversation = conversationId
|
|
2844
|
+
? this.conversationStore.loadProject(projectPath).conversations.find((entry) => entry.id === conversationId)
|
|
2845
|
+
: undefined;
|
|
2846
|
+
const persistedRun = readPersistedRunProjection(persistedConversation);
|
|
2680
2847
|
const now = new Date().toISOString();
|
|
2681
2848
|
const run = {
|
|
2682
2849
|
id: (0, crypto_1.randomUUID)(),
|
|
2683
|
-
conversationId
|
|
2850
|
+
conversationId,
|
|
2684
2851
|
conversationTitle: typeof body.conversationTitle === 'string' && body.conversationTitle.trim() ? body.conversationTitle.trim() : undefined,
|
|
2685
2852
|
jobTitle: typeof body.jobTitle === 'string' && body.jobTitle.trim() ? body.jobTitle.trim() : jobId,
|
|
2686
2853
|
jobId, hostId, projectPath, status: 'running', sessionId,
|
|
2687
2854
|
createdAt: now, updatedAt: now, messages: [],
|
|
2688
2855
|
events: [(0, hosts_1.createHubEvent)('system', `Resuming ${hostId} session ${sessionId} in ${projectPath}`)],
|
|
2689
|
-
currentPhase:
|
|
2856
|
+
currentPhase: persistedRun?.currentPhase || null,
|
|
2857
|
+
phaseHistory: persistedRun?.phaseHistory || [],
|
|
2858
|
+
totals: persistedRun?.totals || emptyTotals(),
|
|
2859
|
+
lastStatusChangeAt: now,
|
|
2860
|
+
runDiscriminant: persistedRun?.runDiscriminant || undefined,
|
|
2690
2861
|
personaKey: getProtectedPersonaForHubJob(jobId),
|
|
2691
2862
|
};
|
|
2692
2863
|
// Continue-turn message (FRAIM invocation for the job + instructions) plus
|
|
@@ -2710,8 +2881,10 @@ class AiHubServer {
|
|
|
2710
2881
|
}
|
|
2711
2882
|
if (event.agentIdentity)
|
|
2712
2883
|
applyAgentIdentitySignal(current, event.agentIdentity);
|
|
2884
|
+
if (event.fraimJob)
|
|
2885
|
+
this.applyFraimJobSignalToRun(current, event.fraimJob);
|
|
2713
2886
|
if (event.seekMentoring)
|
|
2714
|
-
|
|
2887
|
+
this.applySeekMentoringSignalToRun(current, event.seekMentoring);
|
|
2715
2888
|
if (event.usage)
|
|
2716
2889
|
applyUsageSignal(current, event.usage);
|
|
2717
2890
|
});
|
|
@@ -3139,8 +3312,10 @@ class AiHubServer {
|
|
|
3139
3312
|
current.events.push((0, hosts_1.createHubEvent)(channel, event.raw));
|
|
3140
3313
|
if (event.agentIdentity)
|
|
3141
3314
|
applyAgentIdentitySignal(current, event.agentIdentity);
|
|
3315
|
+
if (event.fraimJob)
|
|
3316
|
+
this.applyFraimJobSignalToRun(current, event.fraimJob);
|
|
3142
3317
|
if (event.seekMentoring)
|
|
3143
|
-
|
|
3318
|
+
this.applySeekMentoringSignalToRun(current, event.seekMentoring);
|
|
3144
3319
|
if (event.usage)
|
|
3145
3320
|
applyUsageSignal(current, event.usage);
|
|
3146
3321
|
});
|
|
@@ -3153,7 +3328,7 @@ class AiHubServer {
|
|
|
3153
3328
|
});
|
|
3154
3329
|
this.runRegistry.dispose(run.id);
|
|
3155
3330
|
},
|
|
3156
|
-
}, run.
|
|
3331
|
+
}, startSessionSeedForHost(hostId, run.id));
|
|
3157
3332
|
// Update the registry entry with the real child process handle.
|
|
3158
3333
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
3159
3334
|
return res.json({ runId: run.id, status: 'started', employee: employeeId, job: jobName });
|
|
@@ -3259,8 +3434,10 @@ class AiHubServer {
|
|
|
3259
3434
|
}
|
|
3260
3435
|
if (event.agentIdentity)
|
|
3261
3436
|
applyAgentIdentitySignal(current, event.agentIdentity);
|
|
3437
|
+
if (event.fraimJob)
|
|
3438
|
+
this.applyFraimJobSignalToRun(current, event.fraimJob);
|
|
3262
3439
|
if (event.seekMentoring)
|
|
3263
|
-
|
|
3440
|
+
this.applySeekMentoringSignalToRun(current, event.seekMentoring);
|
|
3264
3441
|
if (event.usage)
|
|
3265
3442
|
applyUsageSignal(current, event.usage);
|
|
3266
3443
|
});
|
|
@@ -3280,7 +3457,7 @@ class AiHubServer {
|
|
|
3280
3457
|
this.deploymentStore.update(deployment.id, (d) => { d.activeRunId = undefined; });
|
|
3281
3458
|
this.runRegistry.dispose(run.id);
|
|
3282
3459
|
},
|
|
3283
|
-
});
|
|
3460
|
+
}, startSessionSeedForHost(deployment.hostId, run.id));
|
|
3284
3461
|
this.runRegistry.create(run, child);
|
|
3285
3462
|
this.deploymentStore.update(deployment.id, (d) => { d.activeRunId = run.id; });
|
|
3286
3463
|
return run;
|
|
@@ -281,6 +281,7 @@ const listSupportedIDEs = () => {
|
|
|
281
281
|
console.log(chalk_1.default.yellow(' Example: fraim add-ide --ide claude-code'));
|
|
282
282
|
console.log(chalk_1.default.yellow(' Anthropic aliases: claude, claude-code, claude-desktop, claude-cowork'));
|
|
283
283
|
console.log(chalk_1.default.yellow(' Gemini aliases: gemini, gemini-cli, gemini cli'));
|
|
284
|
+
console.log(chalk_1.default.yellow(' GitHub Copilot CLI aliases: copilot, copilot-cli, github copilot cli'));
|
|
284
285
|
};
|
|
285
286
|
const promptForIDESelection = async (availableIDEs, tokens) => {
|
|
286
287
|
console.log(chalk_1.default.green(`✅ Found ${availableIDEs.length} IDEs that can be configured:\n`));
|
|
@@ -385,7 +386,7 @@ const runAddIDE = async (options) => {
|
|
|
385
386
|
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
386
387
|
if (detectedIDEs.length === 0) {
|
|
387
388
|
console.log(chalk_1.default.yellow('⚠️ No supported IDEs detected on your system.'));
|
|
388
|
-
console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Gemini CLI, Kiro, Cursor, VSCode, Codex, Grok, Windsurf'));
|
|
389
|
+
console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Gemini CLI, GitHub Copilot CLI, Kiro, Cursor, VSCode, Codex, Grok, Windsurf'));
|
|
389
390
|
console.log(chalk_1.default.blue('\n💡 Install an IDE and run this command again.'));
|
|
390
391
|
return;
|
|
391
392
|
}
|
|
@@ -455,7 +456,7 @@ const runAddIDE = async (options) => {
|
|
|
455
456
|
exports.runAddIDE = runAddIDE;
|
|
456
457
|
exports.addIDECommand = new commander_1.Command('add-ide')
|
|
457
458
|
.description('Add FRAIM configuration to additional IDEs')
|
|
458
|
-
.option('--ide <name>', 'Configure specific IDE (claude, claude-code, claude-desktop, claude-cowork, antigravity, gemini, gemini-cli, kiro, cursor, vscode, codex, grok, windsurf)')
|
|
459
|
+
.option('--ide <name>', 'Configure specific IDE (claude, claude-code, claude-desktop, claude-cowork, antigravity, gemini, gemini-cli, copilot, copilot-cli, kiro, cursor, vscode, codex, grok, windsurf)')
|
|
459
460
|
.option('--all', 'Configure all detected IDEs')
|
|
460
461
|
.option('--list', 'List all supported IDEs and their detection status')
|
|
461
462
|
.action(exports.runAddIDE);
|
|
@@ -52,6 +52,10 @@ const forEachInstalledIDE = async (callback) => {
|
|
|
52
52
|
const writeOAuthProviderToIDEConfigs = async (provider, mcpUrl) => {
|
|
53
53
|
const urlOnlyEntry = { type: 'http', url: mcpUrl };
|
|
54
54
|
await forEachInstalledIDE(async (ide, configPath) => {
|
|
55
|
+
if (provider === 'github' && ide.configType === 'copilot-cli') {
|
|
56
|
+
console.log(chalk_1.default.gray(`Skipped ${ide.name} GitHub MCP entry (built into Copilot CLI)`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
55
59
|
if (ide.configFormat === 'json') {
|
|
56
60
|
let ideConfig = {};
|
|
57
61
|
if (fs_1.default.existsSync(configPath)) {
|
|
@@ -61,7 +65,9 @@ const writeOAuthProviderToIDEConfigs = async (provider, mcpUrl) => {
|
|
|
61
65
|
if (!ideConfig[serversKey]) {
|
|
62
66
|
ideConfig[serversKey] = {};
|
|
63
67
|
}
|
|
64
|
-
ideConfig[serversKey][provider] =
|
|
68
|
+
ideConfig[serversKey][provider] = ide.configType === 'copilot-cli'
|
|
69
|
+
? { ...urlOnlyEntry, tools: ['*'] }
|
|
70
|
+
: urlOnlyEntry;
|
|
65
71
|
fs_1.default.writeFileSync(configPath, JSON.stringify(ideConfig, null, 2));
|
|
66
72
|
console.log(chalk_1.default.green(`✅ Updated ${ide.name} config`));
|
|
67
73
|
}
|
|
@@ -135,7 +135,8 @@ async function cleanupStaleAdapterFiles(projectRoot, allowedConfigTypes) {
|
|
|
135
135
|
const { promisify } = await Promise.resolve().then(() => __importStar(require('util')));
|
|
136
136
|
const execFileAsync = promisify(execFile);
|
|
137
137
|
for (const [relPath, configType] of Object.entries((0, agent_adapters_1.getAdapterConfigTypes)())) {
|
|
138
|
-
|
|
138
|
+
const configTypes = Array.isArray(configType) ? configType : [configType];
|
|
139
|
+
if (configTypes.includes('standard') || configTypes.some((type) => allowedConfigTypes.includes(type))) {
|
|
139
140
|
continue;
|
|
140
141
|
}
|
|
141
142
|
const fullPath = path_1.default.join(projectRoot, relPath);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// IDE Format Adapters - transform logical server structure to IDE-specific formats
|
|
3
3
|
// Uses the centralized registry to determine server types
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.IDE_FORMATS = exports.GrokFormat = exports.CodexFormat = exports.WindsurfFormat = exports.ClaudeCodeFormat = exports.ClaudeFormat = exports.GeminiCliFormat = exports.VSCodeFormat = exports.KiroFormat = exports.StandardFormat = void 0;
|
|
5
|
+
exports.IDE_FORMATS = exports.GrokFormat = exports.CodexFormat = exports.WindsurfFormat = exports.ClaudeCodeFormat = exports.ClaudeFormat = exports.CopilotCliFormat = exports.GeminiCliFormat = exports.VSCodeFormat = exports.KiroFormat = exports.StandardFormat = void 0;
|
|
6
6
|
exports.getIDEFormat = getIDEFormat;
|
|
7
7
|
const mcp_server_registry_1 = require("./mcp-server-registry");
|
|
8
8
|
const provider_registry_1 = require("../providers/provider-registry");
|
|
@@ -120,6 +120,40 @@ class GeminiCliFormat {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
exports.GeminiCliFormat = GeminiCliFormat;
|
|
123
|
+
class CopilotCliFormat {
|
|
124
|
+
constructor() {
|
|
125
|
+
this.name = 'copilot-cli';
|
|
126
|
+
}
|
|
127
|
+
transform(servers) {
|
|
128
|
+
const mcpServers = {};
|
|
129
|
+
for (const [key, server] of servers) {
|
|
130
|
+
// GitHub MCP is built into Copilot CLI. Avoid a duplicate user-configured
|
|
131
|
+
// github entry and let Copilot handle its native GitHub auth path.
|
|
132
|
+
if (key === 'github') {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (server.url) {
|
|
136
|
+
mcpServers[key] = {
|
|
137
|
+
type: server.type === 'sse' ? 'sse' : 'http',
|
|
138
|
+
url: server.url,
|
|
139
|
+
...(server.headers && { headers: server.headers }),
|
|
140
|
+
tools: ['*']
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
mcpServers[key] = {
|
|
145
|
+
type: 'stdio',
|
|
146
|
+
command: server.command,
|
|
147
|
+
...(server.args && { args: server.args }),
|
|
148
|
+
...(server.env && { env: server.env }),
|
|
149
|
+
tools: ['*']
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { mcpServers };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.CopilotCliFormat = CopilotCliFormat;
|
|
123
157
|
// Claude Desktop format (excludes provider servers - Issue #132)
|
|
124
158
|
class ClaudeFormat {
|
|
125
159
|
constructor() {
|
|
@@ -267,6 +301,7 @@ exports.IDE_FORMATS = {
|
|
|
267
301
|
kiro: new KiroFormat(),
|
|
268
302
|
vscode: new VSCodeFormat(),
|
|
269
303
|
'gemini-cli': new GeminiCliFormat(),
|
|
304
|
+
'copilot-cli': new CopilotCliFormat(),
|
|
270
305
|
claude: new ClaudeFormat(),
|
|
271
306
|
'claude-code': new ClaudeCodeFormat(),
|
|
272
307
|
windsurf: new WindsurfFormat(),
|
|
@@ -233,7 +233,7 @@ const autoConfigureMCP = async (fraimKey, selectedIDEs) => {
|
|
|
233
233
|
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
234
234
|
if (detectedIDEs.length === 0 && (!selectedIDEs || selectedIDEs.length === 0)) {
|
|
235
235
|
console.log(chalk_1.default.yellow('⚠️ No supported IDEs detected.'));
|
|
236
|
-
console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Gemini CLI, Kiro, Cursor, VSCode, Codex, Grok, Windsurf'));
|
|
236
|
+
console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Gemini CLI, GitHub Copilot CLI, Kiro, Cursor, VSCode, Codex, Grok, Windsurf'));
|
|
237
237
|
console.log(chalk_1.default.blue('\n💡 You can install an IDE and run setup again later.'));
|
|
238
238
|
console.log(chalk_1.default.gray(' Or continue with manual MCP configuration.'));
|
|
239
239
|
if (process.env.FRAIM_NON_INTERACTIVE) {
|