tokrepo 3.5.0 → 3.6.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.
Files changed (2) hide show
  1. package/bin/tokrepo.js +219 -7
  2. package/package.json +1 -1
package/bin/tokrepo.js CHANGED
@@ -25,7 +25,7 @@ const CONFIG_DIR = path.join(os.homedir(), '.tokrepo');
25
25
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
26
26
  const PROJECT_CONFIG = '.tokrepo.json';
27
27
  const DEFAULT_API = 'https://api.tokrepo.com';
28
- const CLI_VERSION = '3.5.0';
28
+ const CLI_VERSION = '3.6.0';
29
29
  const VERSION_CHECK_FILE = path.join(os.homedir(), '.tokrepo', '.version-check');
30
30
  const CODEX_DIR = path.join(os.homedir(), '.codex');
31
31
  const CODEX_SKILLS_DIR = path.join(CODEX_DIR, 'skills');
@@ -1278,8 +1278,161 @@ function buildCodexInstallPlan(workflow, contents, opts = {}) {
1278
1278
  return plan;
1279
1279
  }
1280
1280
 
1281
+ function metadataValue(metadata, snakeName, camelName, fallback) {
1282
+ if (!metadata) return fallback;
1283
+ if (metadata[snakeName] !== undefined) return metadata[snakeName];
1284
+ if (metadata[camelName] !== undefined) return metadata[camelName];
1285
+ return fallback;
1286
+ }
1287
+
1288
+ function normalizeToolName(value) {
1289
+ return String(value || '').trim().toLowerCase().replace(/-/g, '_');
1290
+ }
1291
+
1292
+ function riskProfileFromFlags(flags = []) {
1293
+ const set = new Set(flags || []);
1294
+ return {
1295
+ executes_code: set.has('executable'),
1296
+ modifies_global_config: set.has('mcp'),
1297
+ requires_secrets: set.has('env') ? ['ENV'] : [],
1298
+ uses_absolute_paths: set.has('absolute-path'),
1299
+ network_access: set.has('network'),
1300
+ };
1301
+ }
1302
+
1303
+ function mergedPlanRiskProfile(plan) {
1304
+ const metadata = plan.agentMetadata || {};
1305
+ const rp = metadataValue(metadata, 'risk_profile', 'riskProfile', {}) || {};
1306
+ const flags = new Set(plan.risks || []);
1307
+ return {
1308
+ executes_code: Boolean(rp.executes_code || rp.executesCode || flags.has('executable')),
1309
+ modifies_global_config: Boolean(rp.modifies_global_config || rp.modifiesGlobalConfig || flags.has('mcp')),
1310
+ requires_secrets: rp.requires_secrets || rp.requiresSecrets || (flags.has('env') ? ['ENV'] : []),
1311
+ uses_absolute_paths: Boolean(rp.uses_absolute_paths || rp.usesAbsolutePaths || flags.has('absolute-path')),
1312
+ network_access: Boolean(rp.network_access || rp.networkAccess || flags.has('network')),
1313
+ };
1314
+ }
1315
+
1316
+ function decideCodexPolicy(plan) {
1317
+ const metadata = plan.agentMetadata || {};
1318
+ const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
1319
+ const assetKind = normalizeToolName(metadataValue(metadata, 'asset_kind', 'assetKind', ''));
1320
+ const risk = mergedPlanRiskProfile(plan);
1321
+ let decision = 'allow';
1322
+ const reasons = [];
1323
+ const raise = (next) => {
1324
+ const rank = { allow: 0, confirm: 1, stage_only: 2, deny: 3 };
1325
+ if ((rank[next] || 0) > (rank[decision] || 0)) decision = next;
1326
+ };
1327
+
1328
+ if (targetTools.length && !targetTools.map(normalizeToolName).includes('codex')) {
1329
+ raise('confirm');
1330
+ reasons.push('metadata target_tools does not include codex');
1331
+ }
1332
+ if (['script', 'cli_tool', 'mcp_config'].includes(assetKind)) {
1333
+ raise('stage_only');
1334
+ reasons.push(`asset_kind ${assetKind} is not activated directly for Codex`);
1335
+ }
1336
+ if (plan.installMode === 'stage_only') {
1337
+ raise('stage_only');
1338
+ reasons.push('install_mode is stage_only');
1339
+ }
1340
+ if (risk.executes_code) {
1341
+ raise('stage_only');
1342
+ reasons.push('risk_profile.executes_code is true');
1343
+ }
1344
+ if (risk.modifies_global_config) {
1345
+ raise('stage_only');
1346
+ reasons.push('risk_profile.modifies_global_config is true');
1347
+ }
1348
+ if ((risk.requires_secrets || []).length) {
1349
+ raise('stage_only');
1350
+ reasons.push('risk_profile.requires_secrets is not empty');
1351
+ }
1352
+ if (risk.uses_absolute_paths) {
1353
+ raise('confirm');
1354
+ reasons.push('risk_profile.uses_absolute_paths is true');
1355
+ }
1356
+ if (risk.network_access) {
1357
+ raise('confirm');
1358
+ reasons.push('risk_profile.network_access is true');
1359
+ }
1360
+ if (reasons.length === 0) reasons.push('safe markdown-only Codex install');
1361
+
1362
+ return {
1363
+ decision,
1364
+ requiresConfirmation: decision === 'confirm',
1365
+ reasons: Array.from(new Set(reasons)),
1366
+ };
1367
+ }
1368
+
1369
+ function buildPublicPlanActions(plan) {
1370
+ const stage = plan.installMode === 'stage_only';
1371
+ return plan.files.map(file => ({
1372
+ type: stage ? 'stage_file' : 'write_file',
1373
+ path: file.path,
1374
+ sourceName: file.sourceName,
1375
+ sha256: file.sha256,
1376
+ bytes: file.bytes,
1377
+ ifExists: 'overwrite',
1378
+ entrypoint: path.basename(file.path).toLowerCase() === 'skill.md',
1379
+ risk: riskProfileFromFlags(file.riskFlags || []),
1380
+ }));
1381
+ }
1382
+
1383
+ function buildPublicPlanPreconditions(plan, policyDecision) {
1384
+ const metadata = plan.agentMetadata || {};
1385
+ const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
1386
+ const out = [
1387
+ { type: 'target_supported', status: 'pass', message: 'codex install target is supported' },
1388
+ { type: 'install_root', status: 'pass', message: '~/.codex/skills for activated skills; ~/.codex/tokrepo/staged for staged assets' },
1389
+ ];
1390
+ if (!targetTools.length || targetTools.map(normalizeToolName).includes('codex')) {
1391
+ out.push({ type: 'target_tool_metadata', status: 'pass', message: 'metadata allows codex' });
1392
+ } else {
1393
+ out.push({ type: 'target_tool_metadata', status: 'warn', message: 'metadata target_tools does not include codex' });
1394
+ }
1395
+ out.push({
1396
+ type: 'content_hash',
1397
+ status: plan.contentHash ? 'pass' : 'warn',
1398
+ message: plan.contentHash ? 'asset metadata includes content_hash' : 'asset metadata does not include content_hash',
1399
+ });
1400
+ const policyStatus = policyDecision.decision === 'deny' ? 'block'
1401
+ : policyDecision.decision === 'allow' ? 'pass'
1402
+ : 'warn';
1403
+ out.push({ type: 'policy_decision', status: policyStatus, message: `${policyDecision.decision} for ${plan.uuid}` });
1404
+ return out;
1405
+ }
1406
+
1407
+ function buildPublicPlanRollback(plan) {
1408
+ const seen = new Set();
1409
+ const rollback = [];
1410
+ for (const file of plan.files) {
1411
+ if (!file.path || seen.has(file.path)) continue;
1412
+ seen.add(file.path);
1413
+ rollback.push({ type: 'remove_file', path: file.path });
1414
+ }
1415
+ return rollback;
1416
+ }
1417
+
1418
+ function buildPublicPlanPostVerify(plan) {
1419
+ const metadata = plan.agentMetadata || {};
1420
+ const verification = metadataValue(metadata, 'verification', 'verification', {}) || {};
1421
+ const out = plan.files.map(file => ({ type: 'file_sha256', path: file.path, sha256: file.sha256 }));
1422
+ for (const expected of (verification.expected_files || verification.expectedFiles || [])) {
1423
+ out.push({ type: 'expected_file', path: expected });
1424
+ }
1425
+ for (const command of (verification.commands || [])) {
1426
+ out.push({ type: 'command', command });
1427
+ }
1428
+ return out;
1429
+ }
1430
+
1281
1431
  function publicInstallPlan(plan) {
1432
+ const policyDecision = decideCodexPolicy(plan);
1433
+ const actions = buildPublicPlanActions(plan);
1282
1434
  return {
1435
+ schemaVersion: 2,
1283
1436
  uuid: plan.uuid,
1284
1437
  title: plan.title,
1285
1438
  sourceUrl: plan.sourceUrl,
@@ -1288,7 +1441,12 @@ function publicInstallPlan(plan) {
1288
1441
  manifestPath: plan.manifestPath,
1289
1442
  baseDir: plan.baseDir,
1290
1443
  risks: plan.risks,
1291
- requiresConfirmation: hasCodexInstallRisks(plan),
1444
+ preconditions: buildPublicPlanPreconditions(plan, policyDecision),
1445
+ actions,
1446
+ policyDecision,
1447
+ requiresConfirmation: policyDecision.requiresConfirmation,
1448
+ rollback: buildPublicPlanRollback(plan),
1449
+ postVerify: buildPublicPlanPostVerify(plan),
1292
1450
  contentHash: plan.contentHash || '',
1293
1451
  agentMetadata: plan.agentMetadata || {},
1294
1452
  files: plan.files.map(file => ({
@@ -1303,7 +1461,8 @@ function publicInstallPlan(plan) {
1303
1461
  }
1304
1462
 
1305
1463
  function hasCodexInstallRisks(plan) {
1306
- return (plan.risks || []).some(risk => ['mcp', 'executable', 'env', 'absolute-path'].includes(risk));
1464
+ const decision = decideCodexPolicy(plan).decision;
1465
+ return decision === 'confirm' || decision === 'stage_only' || decision === 'deny';
1307
1466
  }
1308
1467
 
1309
1468
  function formatRiskLine(file) {
@@ -1312,15 +1471,19 @@ function formatRiskLine(file) {
1312
1471
  }
1313
1472
 
1314
1473
  async function confirmCodexInstallRisks(plan, opts = {}) {
1474
+ const policy = decideCodexPolicy(plan);
1475
+ if (policy.decision === 'deny') {
1476
+ throw new Error(`Install policy denied this asset: ${policy.reasons.join('; ')}`);
1477
+ }
1315
1478
  if (plan.installMode === 'stage_only') return;
1316
- if (opts.dryRun || opts.stage || !hasCodexInstallRisks(plan)) return;
1479
+ if (opts.dryRun || opts.stage || policy.decision === 'allow') return;
1317
1480
  if (opts.approveMcp || opts.approve_mcp || opts.yes) return;
1318
1481
 
1319
1482
  if (opts.json || opts.throwOnError || process.env.TOKREPO_NONINTERACTIVE === '1') {
1320
- throw new Error(`Install plan includes risky content (${plan.risks.join(', ')}). Re-run with --dry-run to inspect or --approve-mcp to approve writing the Codex skill bundle.`);
1483
+ throw new Error(`Install policy is ${policy.decision}: ${policy.reasons.join('; ')}. Re-run with --dry-run to inspect, --stage to stage the plan, or --approve-mcp to approve writing the Codex skill bundle.`);
1321
1484
  }
1322
1485
 
1323
- warn(`This asset contains ${plan.risks.join(', ')} content.`);
1486
+ warn(`Install policy is ${policy.decision}: ${policy.reasons.join('; ')}`);
1324
1487
  log(` ${C.dim}TokRepo will only write files under ${CODEX_SKILLS_DIR}; it will not merge MCP configs, modify PATH, or execute scripts.${C.reset}`);
1325
1488
  const riskyFiles = plan.files
1326
1489
  .map(formatRiskLine)
@@ -1488,6 +1651,33 @@ async function cmdInstall() {
1488
1651
  }
1489
1652
  }
1490
1653
 
1654
+ async function cmdPlan() {
1655
+ const args = parseArgs(process.argv);
1656
+ const target = args.positional[0];
1657
+ if (!target) {
1658
+ showPlanHelp();
1659
+ process.exit(1);
1660
+ }
1661
+
1662
+ const config = readConfig();
1663
+ const apiBase = config?.api || DEFAULT_API;
1664
+ const targetTool = validateInstallTarget(args.flags.target || 'codex');
1665
+ if (targetTool !== 'codex') {
1666
+ error(`plan currently supports --target codex only`);
1667
+ }
1668
+ const result = await installOneAsset(target, config, apiBase, {
1669
+ targetTool,
1670
+ dryRun: true,
1671
+ stage: Boolean(args.flags.stage),
1672
+ installMode: args.flags.installMode,
1673
+ json: true,
1674
+ manifest: true,
1675
+ throwOnError: true,
1676
+ });
1677
+
1678
+ outputJson(result.plan);
1679
+ }
1680
+
1491
1681
  // Install all assets in a theme pack — sequentially, continue past per-item errors
1492
1682
  async function installPack(slug, config, apiBase, opts) {
1493
1683
  info(`Fetching pack ${C.bold}${slug}${C.reset}...`);
@@ -2546,6 +2736,7 @@ ${C.bold}USAGE${C.reset}
2546
2736
  ${C.bold}DISCOVER & INSTALL${C.reset}
2547
2737
  ${C.cyan}search${C.reset} <query> Search assets by keyword
2548
2738
  ${C.cyan}detail${C.reset} <name|uuid> Show full asset metadata
2739
+ ${C.cyan}plan${C.reset} <name|uuid> Print agent-native Codex install plan
2549
2740
  ${C.cyan}install${C.reset} <name|uuid> Smart install (auto-detects type & placement)
2550
2741
  ${C.cyan}pull${C.reset} <url|uuid|@u/n> Download raw asset files
2551
2742
  ${C.cyan}clone${C.reset} @username Clone all assets from a user
@@ -2591,6 +2782,7 @@ ${C.bold}EXAMPLES${C.reset}
2591
2782
  tokrepo search "mcp server" # Find MCP configs
2592
2783
  tokrepo search video --json # Machine-readable search
2593
2784
  tokrepo detail ca000374-f5d8-... --json # Machine-readable detail
2785
+ tokrepo plan 91aeb22d-eff0-4310-... # Install plan v2 for agents
2594
2786
  tokrepo install ca000374-f5d8-... # Install by UUID
2595
2787
  tokrepo install ca000374-f5d8-... --target codex
2596
2788
  tokrepo install c4b18aeb --target gemini # Install for Gemini CLI
@@ -2670,6 +2862,23 @@ EXAMPLES
2670
2862
  `);
2671
2863
  }
2672
2864
 
2865
+ function showPlanHelp() {
2866
+ log(`
2867
+ ${C.bold}tokrepo plan${C.reset}
2868
+
2869
+ USAGE
2870
+ tokrepo plan <uuid|url|name> [--target codex] [--stage]
2871
+
2872
+ OUTPUT
2873
+ Machine-readable install plan v2 with preconditions, actions, policyDecision,
2874
+ rollback, postVerify, risk metadata, and destination file hashes.
2875
+
2876
+ EXAMPLES
2877
+ tokrepo plan 91aeb22d-eff0-4310-abc6-811d2394b420
2878
+ tokrepo plan https://tokrepo.com/en/workflows/91aeb22d-eff0-4310-abc6-811d2394b420
2879
+ `);
2880
+ }
2881
+
2673
2882
  function showListHelp() {
2674
2883
  log(`
2675
2884
  ${C.bold}tokrepo list${C.reset}
@@ -2729,6 +2938,8 @@ function showCommandHelp(command) {
2729
2938
  showSearchHelp(); break;
2730
2939
  case 'detail':
2731
2940
  showDetailHelp(); break;
2941
+ case 'plan':
2942
+ showPlanHelp(); break;
2732
2943
  case 'install':
2733
2944
  case 'i':
2734
2945
  showInstallHelp(); break;
@@ -2764,6 +2975,7 @@ async function main() {
2764
2975
  case 'pull': await cmdPull(); break;
2765
2976
  case 'search': case 'find': await cmdSearch(); break;
2766
2977
  case 'detail': await cmdDetail(); break;
2978
+ case 'plan': await cmdPlan(); break;
2767
2979
  case 'install': case 'i': await cmdInstall(); break;
2768
2980
  case 'list': await cmdList(); break;
2769
2981
  case 'update': await cmdUpdate(); break;
@@ -2784,7 +2996,7 @@ async function main() {
2784
2996
  }
2785
2997
 
2786
2998
  // Non-blocking update check after command completes
2787
- if (!wantsJson(process.argv) && !args.flags.help) {
2999
+ if (command !== 'plan' && !wantsJson(process.argv) && !args.flags.help) {
2788
3000
  checkForUpdate();
2789
3001
  }
2790
3002
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokrepo",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "AI assets for humans and agents — search, install, push. Like GitHub, for AI experience.",
5
5
  "bin": {
6
6
  "tokrepo": "bin/tokrepo.js"