crewx 0.9.0-rc.136 → 0.9.0-rc.137
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.
|
@@ -33,6 +33,8 @@ const SESSION_FILE_NAME = 'session.json';
|
|
|
33
33
|
const DEV_DB_FILE_NAME = 'ui-instances.db';
|
|
34
34
|
const DEV_DB_FILE_PATTERN = /^ui-instances(?:-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})?\.db$/;
|
|
35
35
|
const SESSION_CALL_LIMIT = 50;
|
|
36
|
+
// A dev session that nobody has touched for this long gives its workspace snapshot back.
|
|
37
|
+
const APP_DEV_IDLE_RELEASE_MS = 10 * 60 * 1000;
|
|
36
38
|
const REGISTERED_APP_ENTRY_PATH = 'ui/index.html';
|
|
37
39
|
const APP_DEV_AGENT_ACTOR_ID = 'network:runtime:app-dev';
|
|
38
40
|
const APP_DEV_APPROVAL_REASON = 'Workspace-source App code only; no conversation data; same power as the agent shell.';
|
|
@@ -42,6 +44,8 @@ let AppDevService = class AppDevService {
|
|
|
42
44
|
workspaceAssetCatalogResolver;
|
|
43
45
|
appServices = new Map();
|
|
44
46
|
locks = new Map();
|
|
47
|
+
latestDevCaptures = new Map();
|
|
48
|
+
idleReleases = new Map();
|
|
45
49
|
contentChannel;
|
|
46
50
|
constructor(projectRepository, workspaceEventBus, contentChannel, workspaceAssetCatalogResolver = mcp_app_workspace_asset_catalog_resolver_js_1.mcpAppWorkspaceAssetCatalogResolver) {
|
|
47
51
|
this.projectRepository = projectRepository;
|
|
@@ -66,7 +70,7 @@ let AppDevService = class AppDevService {
|
|
|
66
70
|
?? this.newSession(workspace, appId);
|
|
67
71
|
document.workspaceSlug = workspace.slug;
|
|
68
72
|
const context = this.contextFor(paths, workspace, document);
|
|
69
|
-
const capture = await
|
|
73
|
+
const capture = await this.captureLatestDev(context);
|
|
70
74
|
const registration = this.requireWorkspaceSourceApp(context, capture, true);
|
|
71
75
|
if (registration !== null) {
|
|
72
76
|
await this.prepareApp(context, capture);
|
|
@@ -83,9 +87,9 @@ let AppDevService = class AppDevService {
|
|
|
83
87
|
}
|
|
84
88
|
async getApp(workspace, appId) {
|
|
85
89
|
const context = await this.requireSession(workspace, appId);
|
|
86
|
-
return this.withLock(context.appDirectory, async () => {
|
|
90
|
+
return this.withLock(context.appDirectory, async (arrivedAt) => {
|
|
87
91
|
const current = await this.requireSession(workspace, appId);
|
|
88
|
-
const prepared = await this.prepareApp(current);
|
|
92
|
+
const prepared = await this.prepareApp(current, undefined, undefined, arrivedAt);
|
|
89
93
|
return {
|
|
90
94
|
appId,
|
|
91
95
|
sessionId: current.document.sessionId,
|
|
@@ -102,9 +106,9 @@ let AppDevService = class AppDevService {
|
|
|
102
106
|
}
|
|
103
107
|
async putContent(workspace, appId, actionId, requestId, mediaType, bytes) {
|
|
104
108
|
const context = await this.requireSession(workspace, appId);
|
|
105
|
-
return this.withLock(context.appDirectory, async () => {
|
|
109
|
+
return this.withLock(context.appDirectory, async (arrivedAt) => {
|
|
106
110
|
const current = await this.requireSession(workspace, appId);
|
|
107
|
-
const prepared = await this.prepareApp(current);
|
|
111
|
+
const prepared = await this.prepareApp(current, undefined, undefined, arrivedAt);
|
|
108
112
|
const actions = prepared.contentActions;
|
|
109
113
|
if (!actions || !(0, sdk_1.contentActionsMatch)(actions, prepared.instanceId, prepared.resolved.instance.projection.descriptor.definition)
|
|
110
114
|
|| actions.actions.find((action) => action.actionId === actionId)?.in?.mediaType !== mediaType) {
|
|
@@ -123,9 +127,9 @@ let AppDevService = class AppDevService {
|
|
|
123
127
|
}
|
|
124
128
|
async getContent(workspace, appId, requestId) {
|
|
125
129
|
const context = await this.requireSession(workspace, appId);
|
|
126
|
-
return this.withLock(context.appDirectory, async () => {
|
|
130
|
+
return this.withLock(context.appDirectory, async (arrivedAt) => {
|
|
127
131
|
const current = await this.requireSession(workspace, appId);
|
|
128
|
-
const prepared = await this.prepareApp(current);
|
|
132
|
+
const prepared = await this.prepareApp(current, undefined, undefined, arrivedAt);
|
|
129
133
|
const store = new sdk_1.UiInstanceStore(current.databasePath);
|
|
130
134
|
let receipt;
|
|
131
135
|
try {
|
|
@@ -160,9 +164,9 @@ let AppDevService = class AppDevService {
|
|
|
160
164
|
}
|
|
161
165
|
async getResource(workspace, appId) {
|
|
162
166
|
const context = await this.requireSession(workspace, appId);
|
|
163
|
-
return this.withLock(context.appDirectory, async () => {
|
|
167
|
+
return this.withLock(context.appDirectory, async (arrivedAt) => {
|
|
164
168
|
const current = await this.requireSession(workspace, appId);
|
|
165
|
-
const prepared = await this.prepareApp(current);
|
|
169
|
+
const prepared = await this.prepareApp(current, undefined, undefined, arrivedAt);
|
|
166
170
|
const resource = prepared.service.getInstanceResourceForSnapshot(current.workspace.id, devThreadId(current.document.sessionId), prepared.instanceId, prepared.capture);
|
|
167
171
|
if (!resource)
|
|
168
172
|
throw new common_1.NotFoundException({ code: 'MCP_APP_RESOURCE_NOT_FOUND' });
|
|
@@ -268,9 +272,9 @@ let AppDevService = class AppDevService {
|
|
|
268
272
|
}
|
|
269
273
|
async getState(workspace, appId) {
|
|
270
274
|
const context = await this.requireSession(workspace, appId);
|
|
271
|
-
return this.withLock(context.appDirectory, async () => {
|
|
275
|
+
return this.withLock(context.appDirectory, async (arrivedAt) => {
|
|
272
276
|
const current = await this.requireSession(workspace, appId);
|
|
273
|
-
const prepared = await this.prepareApp(current);
|
|
277
|
+
const prepared = await this.prepareApp(current, undefined, undefined, arrivedAt);
|
|
274
278
|
const projections = await prepared.service.buildAgentContextProjectionsForSnapshot(current.workspace.id, devThreadId(current.document.sessionId), prepared.instanceId, prepared.capture);
|
|
275
279
|
const projection = projections?.find((candidate) => candidate.instanceId === prepared.instanceId);
|
|
276
280
|
if (!projection)
|
|
@@ -298,9 +302,9 @@ let AppDevService = class AppDevService {
|
|
|
298
302
|
}
|
|
299
303
|
async getDebug(workspace, appId, probe, after) {
|
|
300
304
|
const context = await this.requireSession(workspace, appId);
|
|
301
|
-
return this.withLock(context.appDirectory, async () => {
|
|
305
|
+
return this.withLock(context.appDirectory, async (arrivedAt) => {
|
|
302
306
|
const current = await this.requireSession(workspace, appId);
|
|
303
|
-
const prepared = await this.prepareApp(current);
|
|
307
|
+
const prepared = await this.prepareApp(current, undefined, undefined, arrivedAt);
|
|
304
308
|
const logPath = (0, app_debug_log_js_1.appDebugLogPath)(current.workspacePath, appId);
|
|
305
309
|
if (logPath === null)
|
|
306
310
|
throw new common_1.NotFoundException({ code: 'DEBUG_UNAVAILABLE' });
|
|
@@ -368,7 +372,7 @@ let AppDevService = class AppDevService {
|
|
|
368
372
|
const context = await this.requireSession(workspace, appId);
|
|
369
373
|
return this.withLock(context.appDirectory, async () => {
|
|
370
374
|
const current = await this.requireSession(workspace, appId);
|
|
371
|
-
const capture = await
|
|
375
|
+
const capture = await this.captureLatestDev(current);
|
|
372
376
|
const registration = this.requireWorkspaceSourceApp(current, capture, true);
|
|
373
377
|
const items = [];
|
|
374
378
|
const diagnostic = this.findDiagnostic(capture, appId);
|
|
@@ -444,7 +448,7 @@ let AppDevService = class AppDevService {
|
|
|
444
448
|
const context = await this.requireSession(workspace, appId);
|
|
445
449
|
return this.withLock(context.appDirectory, async () => {
|
|
446
450
|
const current = await this.requireSession(workspace, appId);
|
|
447
|
-
const capture = await
|
|
451
|
+
const capture = await this.captureLatestDev(current);
|
|
448
452
|
this.requireWorkspaceSourceApp(current, capture, false);
|
|
449
453
|
this.closeAppService(current.databasePath);
|
|
450
454
|
assertSafeAppDirectoryForDeletion(current.devRoot, current.appDirectory);
|
|
@@ -471,13 +475,33 @@ let AppDevService = class AppDevService {
|
|
|
471
475
|
});
|
|
472
476
|
}
|
|
473
477
|
onModuleDestroy() {
|
|
478
|
+
for (const { timer } of this.idleReleases.values())
|
|
479
|
+
clearTimeout(timer);
|
|
480
|
+
this.idleReleases.clear();
|
|
481
|
+
this.latestDevCaptures.clear();
|
|
474
482
|
for (const service of this.appServices.values())
|
|
475
483
|
service.onModuleDestroy();
|
|
476
484
|
this.appServices.clear();
|
|
477
485
|
}
|
|
478
|
-
|
|
486
|
+
/**
|
|
487
|
+
* Takes the latest-dev capture for a session. A request that queued behind
|
|
488
|
+
* others reuses a capture that began after it arrived: that capture already
|
|
489
|
+
* saw every edit made before the request, so a burst of polls costs one scan.
|
|
490
|
+
*/
|
|
491
|
+
async captureLatestDev(context, arrivedAt) {
|
|
492
|
+
const shared = this.latestDevCaptures.get(context.databasePath);
|
|
493
|
+
if (arrivedAt !== undefined && shared !== undefined && shared.startedAt >= arrivedAt)
|
|
494
|
+
return shared.capture;
|
|
495
|
+
const startedAt = process.hrtime.bigint();
|
|
496
|
+
const capture = await context.service.captureWorkspaceSnapshot(context.workspacePath, { definitionPolicy: 'latest-dev' });
|
|
497
|
+
this.latestDevCaptures.set(context.databasePath, { startedAt, capture });
|
|
498
|
+
return capture;
|
|
499
|
+
}
|
|
500
|
+
async prepareApp(context, suppliedCapture, captureOptions, arrivedAt) {
|
|
479
501
|
const capture = suppliedCapture
|
|
480
|
-
??
|
|
502
|
+
?? (captureOptions === undefined
|
|
503
|
+
? await this.captureLatestDev(context, arrivedAt)
|
|
504
|
+
: await context.service.captureWorkspaceSnapshot(context.workspacePath, captureOptions));
|
|
481
505
|
const registration = this.requireWorkspaceSourceApp(context, capture, false);
|
|
482
506
|
if (registration === null || !registration.definition)
|
|
483
507
|
throw new common_1.HttpException({ code: 'APP_INVALID' }, 409);
|
|
@@ -651,8 +675,33 @@ let AppDevService = class AppDevService {
|
|
|
651
675
|
service.markBootReceiptsRecovered();
|
|
652
676
|
this.appServices.set(databasePath, service);
|
|
653
677
|
}
|
|
678
|
+
this.armIdleRelease(paths.appDirectory, databasePath);
|
|
654
679
|
return { ...paths, databasePath, workspace, document, service };
|
|
655
680
|
}
|
|
681
|
+
/**
|
|
682
|
+
* Nothing tells the server that a dev tab or an agent is done with a session,
|
|
683
|
+
* so a session left alone gives back its workspace snapshot (the bulk of its
|
|
684
|
+
* memory). The next request rebuilds it the way a restarted server would.
|
|
685
|
+
*/
|
|
686
|
+
armIdleRelease(appDirectory, databasePath) {
|
|
687
|
+
const previous = this.idleReleases.get(appDirectory);
|
|
688
|
+
if (previous)
|
|
689
|
+
clearTimeout(previous.timer);
|
|
690
|
+
const timer = setTimeout(() => this.releaseIdleSession(appDirectory), APP_DEV_IDLE_RELEASE_MS);
|
|
691
|
+
timer.unref();
|
|
692
|
+
this.idleReleases.set(appDirectory, { timer, databasePath });
|
|
693
|
+
}
|
|
694
|
+
releaseIdleSession(appDirectory) {
|
|
695
|
+
const entry = this.idleReleases.get(appDirectory);
|
|
696
|
+
if (!entry)
|
|
697
|
+
return;
|
|
698
|
+
if (this.locks.has(appDirectory)) {
|
|
699
|
+
this.armIdleRelease(appDirectory, entry.databasePath);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
this.idleReleases.delete(appDirectory);
|
|
703
|
+
this.closeAppService(entry.databasePath);
|
|
704
|
+
}
|
|
656
705
|
newSession(workspace, appId) {
|
|
657
706
|
return {
|
|
658
707
|
schemaVersion: 1,
|
|
@@ -715,15 +764,17 @@ let AppDevService = class AppDevService {
|
|
|
715
764
|
closeAppService(databasePath) {
|
|
716
765
|
this.appServices.get(databasePath)?.onModuleDestroy();
|
|
717
766
|
this.appServices.delete(databasePath);
|
|
767
|
+
this.latestDevCaptures.delete(databasePath);
|
|
718
768
|
}
|
|
719
769
|
async withLock(key, operation) {
|
|
770
|
+
const arrivedAt = process.hrtime.bigint();
|
|
720
771
|
const previous = this.locks.get(key) ?? Promise.resolve();
|
|
721
772
|
let release;
|
|
722
773
|
const current = new Promise((resolveLock) => { release = resolveLock; });
|
|
723
774
|
this.locks.set(key, current);
|
|
724
775
|
await previous;
|
|
725
776
|
try {
|
|
726
|
-
return await operation();
|
|
777
|
+
return await operation(arrivedAt);
|
|
727
778
|
}
|
|
728
779
|
finally {
|
|
729
780
|
release();
|
|
@@ -19,6 +19,7 @@ exports.resolveSnapshotDispatchCapabilities = resolveSnapshotDispatchCapabilitie
|
|
|
19
19
|
exports.isUserLaunchableRegistration = isUserLaunchableRegistration;
|
|
20
20
|
exports.isMcpAppLauncherLaunchable = isMcpAppLauncherLaunchable;
|
|
21
21
|
exports.getLaunchableMcpAppIds = getLaunchableMcpAppIds;
|
|
22
|
+
exports.actionInputSchemasFromTools = actionInputSchemasFromTools;
|
|
22
23
|
exports.defaultAppButtons = defaultAppButtons;
|
|
23
24
|
exports.createCrewxMcpAppCatalog = createCrewxMcpAppCatalog;
|
|
24
25
|
exports.createCrewxMcpAppRegistry = createCrewxMcpAppRegistry;
|
|
@@ -259,6 +260,10 @@ function createBundleAwareMcpAppRegistry(coreRegistry) {
|
|
|
259
260
|
},
|
|
260
261
|
};
|
|
261
262
|
}
|
|
263
|
+
/** Agent-facing App description: single line, bounded. */
|
|
264
|
+
function promptAppDescription(description) {
|
|
265
|
+
return description.replace(/\s+/g, ' ').trim().slice(0, 300);
|
|
266
|
+
}
|
|
262
267
|
function hasNoRequestedCspOrigins(csp) {
|
|
263
268
|
return csp.connectDomains.length === 0
|
|
264
269
|
&& csp.resourceDomains.length === 0
|
|
@@ -466,12 +471,33 @@ function actionLabel(actionId) {
|
|
|
466
471
|
.replace(/[_-]+/g, ' ')
|
|
467
472
|
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
468
473
|
}
|
|
469
|
-
|
|
474
|
+
/**
|
|
475
|
+
* Per-action input schemas carried by a registration's tools. Dispatcher tools
|
|
476
|
+
* (several action ids behind one wrapper schema) name no single action and are
|
|
477
|
+
* skipped, so their actions keep the keys-only fallback in `defaultAppButtons`.
|
|
478
|
+
*/
|
|
479
|
+
function actionInputSchemasFromTools(tools) {
|
|
480
|
+
const schemas = new Map();
|
|
481
|
+
for (const tool of tools) {
|
|
482
|
+
if (tool.kind !== 'action' || typeof tool.actionId !== 'string')
|
|
483
|
+
continue;
|
|
484
|
+
if (tool.actionIds !== undefined && (tool.actionIds.length !== 1 || tool.actionIds[0] !== tool.actionId))
|
|
485
|
+
continue;
|
|
486
|
+
schemas.set(tool.actionId, tool.inputSchema);
|
|
487
|
+
}
|
|
488
|
+
return schemas;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Buttons derived from the definition's actions. `inputSchemas` supplies each
|
|
492
|
+
* action's real schema (types, enum, required, bounds); an action without one
|
|
493
|
+
* falls back to the keys-only string schema this function always produced.
|
|
494
|
+
*/
|
|
495
|
+
function defaultAppButtons(definition, inputSchemas) {
|
|
470
496
|
return Object.entries(definition.actions).map(([id, action]) => ({
|
|
471
497
|
id,
|
|
472
498
|
label: actionLabel(id),
|
|
473
499
|
enabled: true,
|
|
474
|
-
inputSchema: {
|
|
500
|
+
inputSchema: inputSchemas?.get(id) ?? {
|
|
475
501
|
type: 'object',
|
|
476
502
|
properties: Object.fromEntries((action.inputKeys ?? []).map((key) => [key, { type: 'string' }])),
|
|
477
503
|
additionalProperties: false,
|
|
@@ -767,7 +793,13 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
|
|
|
767
793
|
const registeredProductBundle = await this.ensureBundleCatalog(workspaceRoot);
|
|
768
794
|
const skillBackend = this.workspaceSkillBackendForCapture(workspaceRoot, options.onSkillBackendInspection, productBundle);
|
|
769
795
|
const current = this.snapshotHolders.get(key);
|
|
770
|
-
|
|
796
|
+
// latest-dev still sees every edit: reconfirm re-stats the scanned files and
|
|
797
|
+
// returns a new generation when any fingerprint moved, but hands back the
|
|
798
|
+
// held snapshot untouched otherwise. Only an inspecting capture must be
|
|
799
|
+
// rebuilt, because the skill backend port is baked into the snapshot.
|
|
800
|
+
const needsFreshDiscovery = options.definitionPolicy === 'latest-dev'
|
|
801
|
+
&& options.onSkillBackendInspection !== undefined;
|
|
802
|
+
if (!current || needsFreshDiscovery) {
|
|
771
803
|
const [snapshot] = await Promise.all([
|
|
772
804
|
(0, sdk_1.discoverWorkspaceAppSnapshot)(workspaceRoot, {
|
|
773
805
|
builtins: this.catalog.coreRegistry,
|
|
@@ -966,6 +998,10 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
|
|
|
966
998
|
content: candidate.definition.actions[actionId]?.content,
|
|
967
999
|
backend: candidate.normalized.manifest.actions.find((action) => action.actionId === actionId)?.backend,
|
|
968
1000
|
}, [], verifiedBundle) === 'allowed'));
|
|
1001
|
+
const bundleInputSchemas = new Map(candidate.normalized.manifest.actions.map((action) => [
|
|
1002
|
+
action.actionId,
|
|
1003
|
+
action.inputSchema,
|
|
1004
|
+
]));
|
|
969
1005
|
const canOpen = candidate.definition.usage !== 'card'
|
|
970
1006
|
&& (candidate.initialState !== undefined || candidate.normalized.manifest.actions.length > 0);
|
|
971
1007
|
// Publish the descriptor only after every server-side action adapter is ready.
|
|
@@ -982,7 +1018,7 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
|
|
|
982
1018
|
} : {}),
|
|
983
1019
|
actions,
|
|
984
1020
|
modelActions,
|
|
985
|
-
buttons: () => defaultAppButtons(candidate.definition),
|
|
1021
|
+
buttons: () => defaultAppButtons(candidate.definition, bundleInputSchemas),
|
|
986
1022
|
summaryAllow: candidate.definition.summaryProjection?.allow ?? [],
|
|
987
1023
|
...(candidate.definition.usage === 'card' ? { openPolicy: 'anchored_only' } : {}),
|
|
988
1024
|
});
|
|
@@ -1332,6 +1368,44 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
|
|
|
1332
1368
|
return null;
|
|
1333
1369
|
return registration;
|
|
1334
1370
|
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Effective agent class of one App in this snapshot, folded from the same
|
|
1373
|
+
* per-action decision the action path applies (`getTaskAgentAccess`):
|
|
1374
|
+
* `direct` when any action runs without human approval, `approval` when
|
|
1375
|
+
* agent-callable actions exist but all need approval, else `none`.
|
|
1376
|
+
*/
|
|
1377
|
+
classifyAppAgentClassForSnapshot(appId, capture) {
|
|
1378
|
+
const definition = capture.snapshot.registry.getRegistration(appId)?.definition
|
|
1379
|
+
?? this.getServerRegistrationForSnapshot(appId, capture)?.descriptor.definition;
|
|
1380
|
+
if (!definition)
|
|
1381
|
+
return 'none';
|
|
1382
|
+
let approval = false;
|
|
1383
|
+
for (const actionId of Object.keys(definition.actions)) {
|
|
1384
|
+
const access = this.getTaskAgentAccess(appId, actionId, capture);
|
|
1385
|
+
if (access === 'allowed')
|
|
1386
|
+
return 'direct';
|
|
1387
|
+
if (access === 'approval_required')
|
|
1388
|
+
approval = true;
|
|
1389
|
+
}
|
|
1390
|
+
return approval ? 'approval' : 'none';
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Every launchable App of the snapshot (the `MCP.APP.LIST` set) with its
|
|
1394
|
+
* effective agent class. Read-only: it reuses the caller's capture and never
|
|
1395
|
+
* captures a second snapshot.
|
|
1396
|
+
*/
|
|
1397
|
+
listAvailableAppsForSnapshot(capture) {
|
|
1398
|
+
return this.listLaunchersForSnapshot(capture).flatMap((launcher) => {
|
|
1399
|
+
if (!launcher.definition)
|
|
1400
|
+
return [];
|
|
1401
|
+
return [{
|
|
1402
|
+
id: launcher.appId,
|
|
1403
|
+
name: launcher.name,
|
|
1404
|
+
description: promptAppDescription(launcher.description),
|
|
1405
|
+
agent: this.classifyAppAgentClassForSnapshot(launcher.appId, capture),
|
|
1406
|
+
}];
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1335
1409
|
getTaskAgentActions(appId, capture) {
|
|
1336
1410
|
const bundleCandidate = capture
|
|
1337
1411
|
? this.bundleCandidateForSnapshot(appId, capture)
|
|
@@ -2141,7 +2215,8 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
|
|
|
2141
2215
|
source: 'server',
|
|
2142
2216
|
instance,
|
|
2143
2217
|
definition,
|
|
2144
|
-
buttons: registration.buttons?.(instance.state)
|
|
2218
|
+
buttons: registration.buttons?.(instance.state)
|
|
2219
|
+
?? defaultAppButtons(definition, actionInputSchemasFromTools(registration.descriptor.tools)),
|
|
2145
2220
|
});
|
|
2146
2221
|
return {
|
|
2147
2222
|
kind: 'app',
|
|
@@ -2243,6 +2318,41 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
|
|
|
2243
2318
|
const capture = await this.captureWorkspaceSnapshot((0, workspace_context_store_js_1.getWorkspacePath)());
|
|
2244
2319
|
return this.buildAgentContextProjectionsForSnapshot(workspaceId, threadId, focusedUiInstanceId, capture);
|
|
2245
2320
|
}
|
|
2321
|
+
/**
|
|
2322
|
+
* The open-App projections and the launchable-App list for one MSG.SEND, both
|
|
2323
|
+
* read from a single workspace snapshot capture.
|
|
2324
|
+
*/
|
|
2325
|
+
async buildAgentContext(workspaceId, threadId, focusedUiInstanceId) {
|
|
2326
|
+
if (!isMcpAppsEnabled())
|
|
2327
|
+
return { projections: undefined, available: [] };
|
|
2328
|
+
assertWorkspaceThreadScope(workspaceId, threadId);
|
|
2329
|
+
const capture = await this.captureWorkspaceSnapshot((0, workspace_context_store_js_1.getWorkspacePath)());
|
|
2330
|
+
const projections = await this.buildAgentContextProjectionsForSnapshot(workspaceId, threadId, focusedUiInstanceId, capture);
|
|
2331
|
+
let available = [];
|
|
2332
|
+
try {
|
|
2333
|
+
available = this.listAvailableAppsForSnapshot(capture);
|
|
2334
|
+
}
|
|
2335
|
+
catch (error) {
|
|
2336
|
+
// The list is additive prompt context; a failure must not drop the open-App projections.
|
|
2337
|
+
mcpAppsLogger.warn(`Available App list skipped [thread=${threadId}]: ${error instanceof Error ? error.message : String(error)}`);
|
|
2338
|
+
}
|
|
2339
|
+
return { projections, available };
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Agent-context view of a descriptor: the App identity plus, for each button,
|
|
2343
|
+
* the effective agent access the action path applies (`getTaskAgentAccess`,
|
|
2344
|
+
* the same value `crewx app state` reports).
|
|
2345
|
+
*/
|
|
2346
|
+
withAgentPromptData(descriptor, app, capture) {
|
|
2347
|
+
return {
|
|
2348
|
+
...descriptor,
|
|
2349
|
+
app: { id: app.appId, name: app.name, description: promptAppDescription(app.description) },
|
|
2350
|
+
buttons: descriptor.buttons.map((button) => ({
|
|
2351
|
+
...button,
|
|
2352
|
+
agentAccess: this.getTaskAgentAccess(app.appId, button.id, capture),
|
|
2353
|
+
})),
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2246
2356
|
async buildAgentContextProjectionsForSnapshot(workspaceId, threadId, focusedUiInstanceId, capture) {
|
|
2247
2357
|
if (!isMcpAppsEnabled())
|
|
2248
2358
|
return undefined;
|
|
@@ -2268,11 +2378,15 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
|
|
|
2268
2378
|
if (!resolved)
|
|
2269
2379
|
continue;
|
|
2270
2380
|
const projections = resolved.appProjection;
|
|
2381
|
+
const workflowApp = this.catalog.serverApps.get(exports.MCP_APP_WORKFLOW_ID);
|
|
2382
|
+
const aiDescriptor = workflowApp
|
|
2383
|
+
? this.withAgentPromptData(projections.ai.descriptor, workflowApp.descriptor, capture)
|
|
2384
|
+
: projections.ai.descriptor;
|
|
2271
2385
|
if (focusedUiInstanceId !== undefined && focusedUiInstanceId === instance.id) {
|
|
2272
|
-
output.push(
|
|
2386
|
+
output.push(aiDescriptor);
|
|
2273
2387
|
}
|
|
2274
2388
|
else {
|
|
2275
|
-
output.push(summarizeDescriptor(
|
|
2389
|
+
output.push(summarizeDescriptor(aiDescriptor, workflowApp?.summaryAllow ?? []));
|
|
2276
2390
|
}
|
|
2277
2391
|
}
|
|
2278
2392
|
catch {
|
|
@@ -2289,14 +2403,15 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
|
|
|
2289
2403
|
const projections = await this.projectSnapshotInstance(instance, registration, capture, context);
|
|
2290
2404
|
if (!projections)
|
|
2291
2405
|
continue;
|
|
2406
|
+
const aiDescriptor = this.withAgentPromptData(projections.ai.descriptor, registration, capture);
|
|
2292
2407
|
if (focusedUiInstanceId !== undefined && focusedUiInstanceId === instance.id) {
|
|
2293
|
-
output.push(
|
|
2408
|
+
output.push(aiDescriptor);
|
|
2294
2409
|
}
|
|
2295
2410
|
else {
|
|
2296
2411
|
const allow = registration.resourceBinding.kind === 'mcp'
|
|
2297
2412
|
? this.catalog.serverApps.get(registration.appId)?.summaryAllow ?? []
|
|
2298
2413
|
: registration.definition?.summaryProjection?.allow ?? [];
|
|
2299
|
-
output.push(summarizeDescriptor(
|
|
2414
|
+
output.push(summarizeDescriptor(aiDescriptor, allow));
|
|
2300
2415
|
}
|
|
2301
2416
|
}
|
|
2302
2417
|
catch {
|
|
@@ -2418,7 +2533,7 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
|
|
|
2418
2533
|
source: 'server',
|
|
2419
2534
|
instance,
|
|
2420
2535
|
definition,
|
|
2421
|
-
buttons: defaultAppButtons(definition),
|
|
2536
|
+
buttons: defaultAppButtons(definition, actionInputSchemasFromTools(registration.tools)),
|
|
2422
2537
|
});
|
|
2423
2538
|
}
|
|
2424
2539
|
readThreadInstances(workspaceId, threadId, capture, previousDefinition) {
|
|
@@ -2590,7 +2705,8 @@ let McpAppWorkspaceService = class McpAppWorkspaceService {
|
|
|
2590
2705
|
source: 'server',
|
|
2591
2706
|
instance,
|
|
2592
2707
|
definition,
|
|
2593
|
-
buttons: registration.buttons?.(instance.state)
|
|
2708
|
+
buttons: registration.buttons?.(instance.state)
|
|
2709
|
+
?? defaultAppButtons(definition, actionInputSchemasFromTools(registration.descriptor.tools)),
|
|
2594
2710
|
});
|
|
2595
2711
|
}
|
|
2596
2712
|
return await registration.projectionFactory({
|
|
@@ -389,7 +389,7 @@ let MessageService = MessageService_1 = class MessageService {
|
|
|
389
389
|
// MSG.SEND owns the server-side context boundary for normal agent input.
|
|
390
390
|
// The UI never supplies this projection: resolveMcpApp rereads the current
|
|
391
391
|
// instance, workflow run, definition, and anchor before returning it.
|
|
392
|
-
const mcpAppProjections = await this.
|
|
392
|
+
const { projections: mcpAppProjections, available: mcpAppAvailable } = await this.resolveMcpAppContext(threadId, ensureWorkspaceId, focusedUiInstanceId);
|
|
393
393
|
const frontAppModelContext = focusedUiInstanceId
|
|
394
394
|
&& mcpAppProjections?.some((descriptor) => (descriptor.instanceId === focusedUiInstanceId && descriptor.status === 'active'))
|
|
395
395
|
? this.mcpAppWorkspaceService?.getModelContext(ensureWorkspaceId, threadId, focusedUiInstanceId)
|
|
@@ -475,6 +475,7 @@ let MessageService = MessageService_1 = class MessageService {
|
|
|
475
475
|
metadata,
|
|
476
476
|
cwd: workspaceRoot,
|
|
477
477
|
...(mcpAppProjections ? { mcpAppProjections } : {}),
|
|
478
|
+
...(mcpAppAvailable.length > 0 ? { mcpAppAvailable } : {}),
|
|
478
479
|
...(frontAppModelContext ? { mcpAppModelContext: frontAppModelContext } : {}),
|
|
479
480
|
...(model ? { model } : {}),
|
|
480
481
|
...(effort !== undefined ? { effort } : {}),
|
|
@@ -524,19 +525,22 @@ let MessageService = MessageService_1 = class MessageService {
|
|
|
524
525
|
}
|
|
525
526
|
}
|
|
526
527
|
/**
|
|
527
|
-
* Resolve server-owned MCP App
|
|
528
|
-
*
|
|
529
|
-
*
|
|
528
|
+
* Resolve server-owned MCP App context for this thread: the open-App
|
|
529
|
+
* projections and the launchable-App list, from one workspace snapshot. The
|
|
530
|
+
* normal Nest path reads the SDK ledger; the task-metadata branch only
|
|
531
|
+
* preserves behavior for older direct MessageService hosts that do not
|
|
532
|
+
* provide the adapter (and have no App list to offer).
|
|
530
533
|
*/
|
|
531
|
-
async
|
|
534
|
+
async resolveMcpAppContext(threadId, workspaceId, focusedUiInstanceId) {
|
|
535
|
+
const none = { projections: undefined, available: [] };
|
|
532
536
|
if (!(0, mcp_apps_js_1.isMcpAppsEnabled)())
|
|
533
|
-
return
|
|
537
|
+
return none;
|
|
534
538
|
try {
|
|
535
539
|
if (this.mcpAppWorkspaceService) {
|
|
536
|
-
return await this.mcpAppWorkspaceService.
|
|
540
|
+
return await this.mcpAppWorkspaceService.buildAgentContext(workspaceId, threadId, focusedUiInstanceId);
|
|
537
541
|
}
|
|
538
542
|
if (!this.workflowApprovalBroker)
|
|
539
|
-
return
|
|
543
|
+
return none;
|
|
540
544
|
const candidates = this.threadRepo
|
|
541
545
|
.findAllTasks(threadId, workspaceId)
|
|
542
546
|
.map(parseWorkflowUiInstanceTaskCandidate)
|
|
@@ -557,7 +561,7 @@ let MessageService = MessageService_1 = class MessageService {
|
|
|
557
561
|
actor: { kind: 'authenticated', id: 'network:session' },
|
|
558
562
|
});
|
|
559
563
|
if (resolved)
|
|
560
|
-
return [resolved.appProjection.ai.descriptor];
|
|
564
|
+
return { projections: [resolved.appProjection.ai.descriptor], available: [] };
|
|
561
565
|
}
|
|
562
566
|
}
|
|
563
567
|
catch (error) {
|
|
@@ -565,7 +569,7 @@ let MessageService = MessageService_1 = class MessageService {
|
|
|
565
569
|
// send fail. The legacy prompt remains the safe fallback.
|
|
566
570
|
this.logger.warn(`MCP App projection skipped [thread=${threadId}]: ${error.message}`);
|
|
567
571
|
}
|
|
568
|
-
return
|
|
572
|
+
return none;
|
|
569
573
|
}
|
|
570
574
|
saveImages(threadId, images, preparedImages) {
|
|
571
575
|
return this.savePreparedImages(threadId, preparedImages ?? this.prepareImages(threadId, images));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crewx",
|
|
3
|
-
"version": "0.9.0-rc.
|
|
3
|
+
"version": "0.9.0-rc.137",
|
|
4
4
|
"description": "CrewX — AI agent team dashboard with Electron UI and CLI (Web + Electron + Global CLI)",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -109,20 +109,22 @@
|
|
|
109
109
|
"ws": "8.20.0",
|
|
110
110
|
"yargs": "17.7.0",
|
|
111
111
|
"zod": "3.25.76",
|
|
112
|
-
"@crewx/
|
|
113
|
-
"@crewx/
|
|
114
|
-
"@crewx/cli": "0.9.0-rc.
|
|
115
|
-
"@crewx/
|
|
116
|
-
"@crewx/
|
|
117
|
-
"@crewx/
|
|
118
|
-
"@crewx/
|
|
119
|
-
"@crewx/
|
|
120
|
-
"@crewx/
|
|
121
|
-
"@crewx/
|
|
122
|
-
"@crewx/
|
|
123
|
-
"@crewx/
|
|
124
|
-
"@crewx/
|
|
125
|
-
"@crewx/
|
|
112
|
+
"@crewx/app": "0.1.0-rc.18",
|
|
113
|
+
"@crewx/chromex": "0.1.0-rc.170",
|
|
114
|
+
"@crewx/cli": "0.9.0-rc.137",
|
|
115
|
+
"@crewx/cron": "0.1.10-rc.172",
|
|
116
|
+
"@crewx/doc": "0.1.9-rc.130",
|
|
117
|
+
"@crewx/dreaming": "0.1.0-rc.163",
|
|
118
|
+
"@crewx/knowledge-core": "0.1.17-rc.147",
|
|
119
|
+
"@crewx/sdk": "0.9.0-rc.137",
|
|
120
|
+
"@crewx/memory": "0.1.23-rc.154",
|
|
121
|
+
"@crewx/skill": "0.1.20-rc.31",
|
|
122
|
+
"@crewx/notify": "0.1.0-rc.108",
|
|
123
|
+
"@crewx/wbs": "0.1.10-rc.163",
|
|
124
|
+
"@crewx/workflow": "0.3.22-rc.183",
|
|
125
|
+
"@crewx/search": "0.1.10-rc.133",
|
|
126
|
+
"@crewx/wi": "0.1.10-rc.157",
|
|
127
|
+
"@crewx/shared": "0.0.6-rc.52"
|
|
126
128
|
},
|
|
127
129
|
"devDependencies": {
|
|
128
130
|
"@anthropic-ai/claude-agent-sdk": "0.2.132",
|
|
@@ -390,6 +392,8 @@
|
|
|
390
392
|
"test:grok-poc": "node --test scripts/grok-poc/capture.test.mjs scripts/grok-poc/resume-proof.test.mjs",
|
|
391
393
|
"test:server": "vitest run --config vitest.server.config.ts",
|
|
392
394
|
"test:bundled-apps": "node --test scripts/build-bundled-apps.spec.mjs",
|
|
395
|
+
"test:app-build": "node --test scripts/check-sdk-app-imports.test.mjs scripts/build-app.test.mjs",
|
|
396
|
+
"check:sdk-app-imports": "node scripts/check-sdk-app-imports.mjs",
|
|
393
397
|
"type-check": "tsc --noEmit",
|
|
394
398
|
"build:manual": "node scripts/build-manual.mjs"
|
|
395
399
|
}
|