@crewx/app 0.1.0-rc.17 → 0.1.0-rc.19

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.
@@ -42,6 +42,8 @@ const https = __importStar(require("node:https"));
42
42
  const path = __importStar(require("node:path"));
43
43
  const node_url_1 = require("node:url");
44
44
  const crewx_api_credential_1 = require("@crewx/shared/crewx-api-credential");
45
+ const receipt_wait_js_1 = require("./receipt-wait.js");
46
+ const usage_js_1 = require("./usage.js");
45
47
  const REQUEST_TIMEOUT_MS = 15000;
46
48
  const RUN_REQUEST_TIMEOUT_MS = 30000;
47
49
  const DEV_ACTION_TIMEOUT_MS = 60000;
@@ -97,7 +99,7 @@ function parseCommand(args) {
97
99
  const separator = arg.indexOf('=');
98
100
  const key = separator >= 0 ? arg.slice(2, separator) : arg.slice(2);
99
101
  const inlineValue = separator >= 0 ? arg.slice(separator + 1) : undefined;
100
- if ((key !== 'input' && key !== 'request-id') || options.has(key))
102
+ if ((key !== 'input' && key !== 'request-id' && key !== 'wait') || options.has(key))
101
103
  return null;
102
104
  const value = inlineValue ?? args[index + 1];
103
105
  if (value === undefined)
@@ -115,6 +117,8 @@ function parseCommand(args) {
115
117
  }
116
118
  if (command !== 'run' && (options.has('input') || options.has('request-id')))
117
119
  return null;
120
+ if (command !== 'receipt' && options.has('wait'))
121
+ return null;
118
122
  let input = {};
119
123
  if (options.has('input')) {
120
124
  const raw = options.get('input');
@@ -131,11 +135,15 @@ function parseCommand(args) {
131
135
  const requestId = options.get('request-id');
132
136
  if (requestId !== undefined && !REQUEST_ID_PATTERN.test(requestId))
133
137
  return null;
138
+ const waitSeconds = options.has('wait') ? (0, receipt_wait_js_1.parseWaitSeconds)(options.get('wait')) : undefined;
139
+ if (waitSeconds === null)
140
+ return null;
134
141
  return {
135
142
  command,
136
143
  positional,
137
144
  input,
138
145
  ...(requestId !== undefined ? { requestId } : {}),
146
+ ...(waitSeconds !== undefined ? { waitSeconds } : {}),
139
147
  };
140
148
  }
141
149
  function getEndpoint(raw) {
@@ -388,15 +396,36 @@ function resolveWorkspaceRoot(startDirectory = process.cwd()) {
388
396
  current = parent;
389
397
  }
390
398
  }
391
- function readAppManifest(directory) {
399
+ const APP_MANIFEST_FILE = 'manifest.json';
400
+ const LEGACY_APP_MANIFEST_FILE = 'ui-plugin.json';
401
+ const CREWX_META_NAMESPACE = 'dev.crewx';
402
+ function readJsonObject(filePath) {
392
403
  try {
393
- const parsed = JSON.parse(fs.readFileSync(path.join(directory, 'ui-plugin.json'), 'utf8'));
404
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
394
405
  return isRecord(parsed) ? parsed : undefined;
395
406
  }
396
407
  catch {
397
408
  return undefined;
398
409
  }
399
410
  }
411
+ function readAppManifest(directory) {
412
+ const legacy = readJsonObject(path.join(directory, LEGACY_APP_MANIFEST_FILE));
413
+ const modernPath = path.join(directory, APP_MANIFEST_FILE);
414
+ if (!isFile(modernPath))
415
+ return legacy;
416
+ const modern = readJsonObject(modernPath);
417
+ if (!modern)
418
+ return undefined;
419
+ const meta = modern['_meta'];
420
+ const crewx = isRecord(meta) ? meta[CREWX_META_NAMESPACE] : undefined;
421
+ const view = {
422
+ pluginId: modern['name'],
423
+ entry: isRecord(crewx) ? crewx['entry'] : undefined,
424
+ };
425
+ if (legacy && legacy['pluginId'] !== view['pluginId'])
426
+ return undefined;
427
+ return view;
428
+ }
400
429
  function isPathInside(parentDirectory, candidate) {
401
430
  const relative = path.relative(parentDirectory, candidate);
402
431
  return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
@@ -584,14 +613,27 @@ function projectLauncher(value) {
584
613
  if (!appId || !definitionHash)
585
614
  return undefined;
586
615
  const actions = value['agentActions'];
616
+ const agentAccess = projectAgentAccess(value['agentAccess']);
587
617
  return {
588
618
  appId,
589
619
  name: typeof value['name'] === 'string' ? value['name'] : '',
590
620
  description: typeof value['description'] === 'string' ? value['description'] : '',
591
621
  definitionHash,
592
622
  agentActions: Array.isArray(actions) ? actions : [],
623
+ ...(agentAccess === undefined ? {} : { agentAccess }),
593
624
  };
594
625
  }
626
+ const AGENT_ACCESS_LEVELS = new Set(['allowed', 'approval_required', 'denied']);
627
+ function projectAgentAccess(value) {
628
+ if (!isRecord(value))
629
+ return undefined;
630
+ const levels = new Map();
631
+ for (const [actionId, level] of Object.entries(value)) {
632
+ if (AGENT_ACCESS_LEVELS.has(level))
633
+ levels.set(actionId, level);
634
+ }
635
+ return levels.size > 0 ? levels : undefined;
636
+ }
595
637
  async function loadLaunchers(context, timeoutMs = REQUEST_TIMEOUT_MS) {
596
638
  const response = await performRequest(context, 'GET', pathFor(context, 'ui-instances', 'apps'), undefined, timeoutMs);
597
639
  if (!response.ok)
@@ -732,6 +774,41 @@ async function runAction(context, instanceId, actionId, requestId, input, timeou
732
774
  },
733
775
  };
734
776
  }
777
+ function isRetryablePollFailure(outcome) {
778
+ const error = outcome.payload['error'];
779
+ return outcome.exitCode === 14
780
+ || (isRecord(error) && error['status'] === 429);
781
+ }
782
+ async function readReceipt(context, receiptId) {
783
+ const response = await performRequest(context, 'GET', pathFor(context, 'ui-action-receipts', receiptId));
784
+ if (!response.ok) {
785
+ return { state: 'failed', retryable: isRetryablePollFailure(response.outcome), value: response.outcome };
786
+ }
787
+ const receipt = response.value['receipt'];
788
+ if (!isRecord(receipt))
789
+ return { state: 'failed', retryable: false, value: failure(1, 'ERROR', response.status) };
790
+ return {
791
+ state: receipt['status'] === 'requested' ? 'pending' : 'settled',
792
+ value: success({
793
+ ok: true,
794
+ receipt,
795
+ outcome: Object.prototype.hasOwnProperty.call(receipt, 'outcome') ? receipt['outcome'] : null,
796
+ ...(typeof receipt['errorCode'] === 'string' ? { errorCode: receipt['errorCode'] } : {}),
797
+ ...(Object.prototype.hasOwnProperty.call(response.value, 'result') ? { result: response.value['result'] } : {}),
798
+ ...(Object.prototype.hasOwnProperty.call(response.value, 'resultExpired')
799
+ ? { resultExpired: response.value['resultExpired'] }
800
+ : {}),
801
+ }),
802
+ };
803
+ }
804
+ function receiptStillPending(receiptId, waitedSeconds) {
805
+ return {
806
+ exitCode: 124,
807
+ payload: { ok: false, exit: 124, error: { code: 'WAIT_TIMEOUT', status: null }, receiptId },
808
+ suppressOutput: true,
809
+ stderr: `→ Receipt ${receiptId} is still pending after ${waitedSeconds}s. Do not end your turn: run crewx app receipt ${receiptId} --wait=${(0, receipt_wait_js_1.nextWaitSeconds)(waitedSeconds)} again.`,
810
+ };
811
+ }
735
812
  function contextFromEnvironment() {
736
813
  const ticket = process.env['CREWX_TASK_TICKET'] ?? '';
737
814
  const workspaceSlug = process.env['CREWX_WORKSPACE_SLUG'] ?? '';
@@ -753,6 +830,95 @@ function contextFromEnvironment() {
753
830
  function isAppCommandOutcome(value) {
754
831
  return 'exitCode' in value;
755
832
  }
833
+ function padTime(value) {
834
+ return String(value).padStart(2, '0');
835
+ }
836
+ function localExpiryTime(expiresAt) {
837
+ const date = typeof expiresAt === 'number'
838
+ ? new Date(expiresAt)
839
+ : typeof expiresAt === 'string'
840
+ ? new Date(/^\d+$/.test(expiresAt) ? Number(expiresAt) : expiresAt)
841
+ : undefined;
842
+ if (!date || Number.isNaN(date.getTime()))
843
+ return undefined;
844
+ return `${padTime(date.getHours())}:${padTime(date.getMinutes())}`;
845
+ }
846
+ function withApprovalHint(outcome) {
847
+ if (outcome.exitCode !== 8)
848
+ return outcome;
849
+ const time = localExpiryTime(outcome.payload['expiresAt']);
850
+ const receiptId = outcome.payload['receiptId'];
851
+ const lines = [
852
+ `→ Waiting for the user's approval in the App window${time ? ` (expires ${time})` : ''}. Do not end your turn: ending it cancels the request.`,
853
+ ...(typeof receiptId === 'string' && receiptId.length > 0
854
+ ? [`→ Wait: crewx app receipt ${receiptId} --wait=${receipt_wait_js_1.RECEIPT_WAIT_DEFAULT_SECONDS} (repeat on exit 124 until it settles)`]
855
+ : []),
856
+ '→ Report success only if it settles as succeeded.',
857
+ ];
858
+ return { ...outcome, stderr: [outcome.stderr, ...lines].filter(Boolean).join('\n') };
859
+ }
860
+ const MANUAL_READ_LIMIT_BYTES = 256 * 1024;
861
+ function readAppManual(appDirectory) {
862
+ try {
863
+ const realDirectory = fs.realpathSync(appDirectory);
864
+ const realFile = fs.realpathSync(path.join(realDirectory, 'SKILL.md'));
865
+ if (!isPathInside(realDirectory, realFile) || !isFile(realFile))
866
+ return undefined;
867
+ const descriptor = fs.openSync(realFile, 'r');
868
+ try {
869
+ const bytes = new Uint8Array(MANUAL_READ_LIMIT_BYTES);
870
+ const bytesRead = fs.readSync(descriptor, bytes, 0, bytes.length, 0);
871
+ return (0, usage_js_1.manualBody)(Buffer.from(bytes.buffer, 0, bytesRead).toString('utf8'));
872
+ }
873
+ finally {
874
+ fs.closeSync(descriptor);
875
+ }
876
+ }
877
+ catch {
878
+ return undefined;
879
+ }
880
+ }
881
+ async function findLocalApp(appId) {
882
+ const root = resolveWorkspaceRoot();
883
+ if (!root)
884
+ return undefined;
885
+ try {
886
+ const sdk = await Promise.resolve().then(() => __importStar(require('@crewx/sdk')));
887
+ const discovered = await sdk.discoverWorkspaceApps(root);
888
+ const candidate = discovered.apps.find((app) => app.appId === appId)
889
+ ?? discovered.apps.find((app) => app.pluginId === appId);
890
+ if (!candidate)
891
+ return undefined;
892
+ return {
893
+ candidate,
894
+ directory: path.join(root, candidate.relativePath),
895
+ resolveAccess: sdk.resolveAgentAccess,
896
+ };
897
+ }
898
+ catch {
899
+ return undefined;
900
+ }
901
+ }
902
+ async function executeAppUsage(args) {
903
+ if (args.length !== 2 || args[1].startsWith('--'))
904
+ return usageFailure();
905
+ const appId = args[1];
906
+ const context = contextFromEnvironment();
907
+ if (isAppCommandOutcome(context))
908
+ return context;
909
+ const list = await loadLaunchers(context);
910
+ if (!list.ok)
911
+ return list.outcome;
912
+ const launcher = findLauncher(list.value, appId);
913
+ if (!launcher)
914
+ return failure(6, 'MCP_APP_NOT_FOUND', null);
915
+ const serverActions = launcher.agentActions.filter((item) => typeof item === 'string');
916
+ const local = await findLocalApp(appId);
917
+ const text = local
918
+ ? (0, usage_js_1.renderUsage)(readAppManual(local.directory), (0, usage_js_1.renderActionsSection)(appId, local.candidate, (0, usage_js_1.buildActionRows)(local.candidate, new Set(serverActions), (action) => local.resolveAccess(action), launcher.agentAccess)), local.candidate.manifest.csp?.connectDomains)
919
+ : (0, usage_js_1.renderUsage)(undefined, (0, usage_js_1.renderServerOnlyActions)(appId, serverActions));
920
+ return { exitCode: 0, payload: { ok: true }, stdout: text };
921
+ }
756
922
  function parseSingleAppCommand(args, command) {
757
923
  if (args[0] !== command)
758
924
  return null;
@@ -778,6 +944,16 @@ function parseSingleAppCommand(args, command) {
778
944
  function standardJsonOutput(outcome) {
779
945
  return `${JSON.stringify(outcome.payload)}\n`;
780
946
  }
947
+ function manifestReasonSuffix(item) {
948
+ const detail = item['detail'];
949
+ if (item['rule'] !== 'manifest' || !isRecord(detail))
950
+ return '';
951
+ const path = typeof detail['path'] === 'string' ? detail['path'] : '';
952
+ const rule = typeof detail['rule'] === 'string' ? detail['rule'] : '';
953
+ if (!rule)
954
+ return '';
955
+ return ` ${path ? `${path} ` : ''}(${rule})`;
956
+ }
781
957
  async function executeAppCheck(args) {
782
958
  const parsed = parseSingleAppCommand(args, 'check');
783
959
  if (!parsed)
@@ -816,7 +992,7 @@ async function executeAppCheck(args) {
816
992
  const marker = severity === 'error' ? '✕' : severity === 'ok' ? '✓' : '!';
817
993
  const where = typeof item['where'] === 'string' ? item['where'] : '';
818
994
  const code = typeof item['code'] === 'string' ? item['code'] : '';
819
- return [`${marker} ${where} ${code}`];
995
+ return [`${marker} ${where} ${code}${manifestReasonSuffix(item)}`];
820
996
  });
821
997
  lines.push(`Summary: ${errors} errors, ${warnings} warnings`);
822
998
  return {
@@ -921,7 +1097,10 @@ function hasBuildScript(appDirectory) {
921
1097
  }
922
1098
  }
923
1099
  function buildOutputPaths(appDirectory) {
924
- const result = new Set([path.normalize('ui-plugin.json')]);
1100
+ const result = new Set([
1101
+ path.normalize(APP_MANIFEST_FILE),
1102
+ path.normalize(LEGACY_APP_MANIFEST_FILE),
1103
+ ]);
925
1104
  const manifest = readAppManifest(appDirectory);
926
1105
  const entry = manifest?.['entry'];
927
1106
  if (typeof entry === 'string' && entry.length > 0 && !path.isAbsolute(entry)) {
@@ -1172,13 +1351,122 @@ async function executeAppDev(args) {
1172
1351
  }
1173
1352
  return runAppDevWatcher(prepared.context, hasBuildScript(prepared.context.appDirectory));
1174
1353
  }
1175
- async function executeAppCommand(args) {
1354
+ const BUILD_USAGE = 'usage: crewx app build [<appDir>] [--check | --watch]';
1355
+ const APP_HELP = [
1356
+ 'crewx app <command>',
1357
+ '',
1358
+ ' list List Apps available to this task',
1359
+ ' open <appId> Open an App in this conversation',
1360
+ ' state <instanceId> Read AI-visible state, buttons, and agent access',
1361
+ ' run <instanceId|appId> <actionId> [--input <JSON>|-] [--request-id <id>] Run one available action',
1362
+ ' receipt <receiptId> [--wait=<seconds>] Read a safe action receipt',
1363
+ ' build [<appDir>] [--check | --watch] Build a @Tool App: server.ts -> dist/ and the manifest.json actions',
1364
+ ' dev <app> [--no-open] Start a foreground App dev session',
1365
+ ' check <app> [--json] Run the App dev checks',
1366
+ ' run --dev <app> <actionId> [--input <JSON>|-] Run an App action through the dev checks',
1367
+ ' usage [<appId>] Show the guide, or one App\'s manual and action table',
1368
+ '',
1369
+ ].join('\n');
1370
+ const BUILD_HELP = [
1371
+ BUILD_USAGE,
1372
+ '',
1373
+ ' Builds <appDir> (default: the current directory) from its server.ts: dist/server.mjs, dist/entry.mjs,',
1374
+ ' and the actions of manifest.json. --check writes nothing and exits 1 when a generated file is stale.',
1375
+ ' --watch rebuilds on every change until Ctrl-C.',
1376
+ '',
1377
+ ].join('\n');
1378
+ function loadAppBuilder() {
1379
+ const candidates = [
1380
+ path.join(__dirname, '..', '..', 'build-app', 'load.cjs'),
1381
+ path.join(__dirname, '..', 'build-app', 'load.cjs'),
1382
+ ];
1383
+ const shim = candidates.find((candidate) => fs.existsSync(candidate));
1384
+ if (!shim)
1385
+ return Promise.reject(new Error('the App build is not part of this install'));
1386
+ return require(shim).loadAppBuilder();
1387
+ }
1388
+ function describeError(error) {
1389
+ return error instanceof Error ? error.message : String(error);
1390
+ }
1391
+ async function executeAppBuild(args) {
1392
+ if (args.slice(1).some((arg) => arg === '--help' || arg === '-h')) {
1393
+ return { exitCode: 0, payload: { ok: true }, stdout: BUILD_HELP };
1394
+ }
1395
+ let builder;
1396
+ try {
1397
+ builder = await loadAppBuilder();
1398
+ }
1399
+ catch (error) {
1400
+ return failure(1, 'BUILD_UNAVAILABLE', null, describeError(error));
1401
+ }
1402
+ let options;
1403
+ try {
1404
+ options = builder.parseArguments(args.slice(1));
1405
+ }
1406
+ catch (error) {
1407
+ return failure(2, 'USAGE', null, `${describeError(error)} → ${BUILD_USAGE}`);
1408
+ }
1409
+ const { appDirectory } = options;
1410
+ const shownDirectory = path.relative(process.cwd(), appDirectory);
1411
+ const rebuildCommand = shownDirectory === '' ? 'crewx app build' : `crewx app build ${shownDirectory}`;
1412
+ if (options.watch) {
1413
+ const controller = new AbortController();
1414
+ const stop = () => controller.abort();
1415
+ process.once('SIGINT', stop);
1416
+ process.once('SIGTERM', stop);
1417
+ try {
1418
+ await builder.watchApp(appDirectory, (line) => process.stdout.write(`${line}\n`), { signal: controller.signal });
1419
+ }
1420
+ finally {
1421
+ process.removeListener('SIGINT', stop);
1422
+ process.removeListener('SIGTERM', stop);
1423
+ }
1424
+ return { exitCode: 0, payload: {}, suppressOutput: true };
1425
+ }
1426
+ try {
1427
+ const result = await builder.buildApp({ appDirectory, check: options.check });
1428
+ if (options.check && result.staleFiles.length > 0) {
1429
+ return {
1430
+ ...failure(1, 'BUILD_STALE', null, `Stale generated files: ${result.staleFiles.join(', ')} → Fix: ${rebuildCommand}`),
1431
+ payload: {
1432
+ ok: false,
1433
+ exit: 1,
1434
+ error: { code: 'BUILD_STALE', status: null },
1435
+ appDir: appDirectory,
1436
+ stale: result.staleFiles,
1437
+ },
1438
+ };
1439
+ }
1440
+ if (options.check) {
1441
+ return success({ ok: true, appDir: appDirectory, check: true, inSync: true, files: result.files, actions: result.actions });
1442
+ }
1443
+ const name = path.basename(appDirectory);
1444
+ const inWorkspaceApps = path.basename(path.dirname(appDirectory)) === 'apps';
1445
+ return {
1446
+ ...success({ ok: true, appDir: appDirectory, files: result.files, actions: result.actions }),
1447
+ stderr: inWorkspaceApps
1448
+ ? `→ Next: crewx app check ${name}`
1449
+ : `→ Next: put ${name} in the workspace apps/ folder, then crewx app check ${name}`,
1450
+ };
1451
+ }
1452
+ catch (error) {
1453
+ return failure(1, 'BUILD_FAILED', null, describeError(error));
1454
+ }
1455
+ }
1456
+ async function executeAppCommand(args, { clock = receipt_wait_js_1.realClock } = {}) {
1457
+ if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
1458
+ return { exitCode: 0, payload: { ok: true }, stdout: APP_HELP };
1459
+ }
1460
+ if (args[0] === 'build')
1461
+ return executeAppBuild(args);
1176
1462
  if (args[0] === 'dev')
1177
1463
  return executeAppDev(args);
1178
1464
  if (args[0] === 'check')
1179
1465
  return executeAppCheck(args);
1180
1466
  if (args[0] === 'run' && args[1] === '--dev')
1181
1467
  return executeAppDevAction(args);
1468
+ if (args[0] === 'usage')
1469
+ return executeAppUsage(args);
1182
1470
  const parsed = parseCommand(args);
1183
1471
  if (!parsed)
1184
1472
  return usageFailure();
@@ -1193,22 +1481,13 @@ async function executeAppCommand(args) {
1193
1481
  }
1194
1482
  if (parsed.command === 'receipt') {
1195
1483
  const receiptId = parsed.positional[0];
1196
- const response = await performRequest(context, 'GET', pathFor(context, 'ui-action-receipts', receiptId));
1197
- if (!response.ok)
1198
- return response.outcome;
1199
- const receipt = response.value['receipt'];
1200
- if (!isRecord(receipt))
1201
- return failure(1, 'ERROR', response.status);
1202
- return success({
1203
- ok: true,
1204
- receipt,
1205
- outcome: Object.prototype.hasOwnProperty.call(receipt, 'outcome') ? receipt['outcome'] : null,
1206
- ...(typeof receipt['errorCode'] === 'string' ? { errorCode: receipt['errorCode'] } : {}),
1207
- ...(Object.prototype.hasOwnProperty.call(response.value, 'result') ? { result: response.value['result'] } : {}),
1208
- ...(Object.prototype.hasOwnProperty.call(response.value, 'resultExpired')
1209
- ? { resultExpired: response.value['resultExpired'] }
1210
- : {}),
1211
- });
1484
+ const waitSeconds = (0, receipt_wait_js_1.clampWaitSeconds)(parsed.waitSeconds ?? 0);
1485
+ if (waitSeconds === 0)
1486
+ return (await readReceipt(context, receiptId)).value;
1487
+ const last = await (0, receipt_wait_js_1.pollUntilSettled)(() => readReceipt(context, receiptId), waitSeconds, clock);
1488
+ if (last.state !== 'pending')
1489
+ return last.value;
1490
+ return receiptStillPending(receiptId, waitSeconds);
1212
1491
  }
1213
1492
  if (parsed.command === 'state') {
1214
1493
  const result = await readInstanceState(context, parsed.positional[0]);
@@ -1246,7 +1525,10 @@ async function executeAppCommand(args) {
1246
1525
  }
1247
1526
  const action = await runAction(context, instanceId, actionId, requestId, parsed.input, RUN_REQUEST_TIMEOUT_MS);
1248
1527
  if (!action.ok)
1249
- return withRunTimeoutGuidance(action.outcome, requestId);
1250
- return success({ ok: true, ...action.value });
1528
+ return withApprovalHint(withRunTimeoutGuidance(action.outcome, requestId));
1529
+ return {
1530
+ ...success({ ok: true, ...action.value }),
1531
+ stderr: `→ Check: crewx app state ${action.value.instanceId}`,
1532
+ };
1251
1533
  }
1252
1534
  //# sourceMappingURL=engine.js.map