tokrepo 3.6.0 → 3.8.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 +1117 -34
  2. package/package.json +1 -1
package/bin/tokrepo.js CHANGED
@@ -25,12 +25,13 @@ 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.6.0';
28
+ const CLI_VERSION = '3.8.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');
32
32
  const CODEX_TOKREPO_DIR = path.join(CODEX_DIR, 'tokrepo');
33
33
  const CODEX_MANIFEST_FILE = path.join(CODEX_TOKREPO_DIR, 'install-manifest.json');
34
+ const CODEX_SESSIONS_DIR = path.join(CODEX_TOKREPO_DIR, 'sessions');
34
35
  const SUPPORTED_INSTALL_TARGETS = ['gemini', 'codex'];
35
36
 
36
37
  // ─── Helpers ───
@@ -301,9 +302,10 @@ function parseArgs(argv) {
301
302
  }
302
303
 
303
304
  const valueFlags = new Set([
304
- 'title', 'desc', 'tag', 'target', 'targets', 'keyword', 'types',
305
+ 'title', 'desc', 'tag', 'target', 'targets', 'keyword', 'query', 'types',
305
306
  'kind', 'install-mode', 'install_mode', 'entrypoint', 'asset-kind', 'asset_kind',
306
- 'version',
307
+ 'version', 'uuid',
308
+ 'policy', 'session',
307
309
  'page', 'page-size', 'page_size', 'sort-by', 'sort_by',
308
310
  'time-window', 'time_window',
309
311
  ]);
@@ -544,14 +546,153 @@ function browserAuthFlow() {
544
546
  });
545
547
  }
546
548
 
547
- async function cmdPush() {
548
- const args = parseArgs(process.argv);
549
+ function inferAssetKindFromPushFiles(files = []) {
550
+ const normalized = files.map(file => ({
551
+ ...file,
552
+ lowerName: String(file.name || '').toLowerCase(),
553
+ content: String(file.content || ''),
554
+ }));
555
+ if (normalized.some(file => /"mcpServers"\s*:/.test(file.content) || /\bmcpServers\s*:/.test(file.content))) return 'mcp_config';
556
+ if (normalized.some(file => file.type === 'script' || /\.(sh|py|js|mjs|ts|rb|go|rs|lua)$/.test(file.lowerName) || /^#!\//.test(file.content))) return 'script';
557
+ if (normalized.some(file => file.lowerName === 'package.json' && /"bin"\s*:/.test(file.content))) return 'cli_tool';
558
+ if (normalized.some(file => file.type === 'skill' || isCodexSkillDocument(file))) return 'skill';
559
+ if (normalized.some(file => file.type === 'prompt')) return 'prompt';
560
+ if (normalized.some(file => file.type === 'config')) return 'config';
561
+ if (normalized.some(file => file.lowerName.endsWith('.md'))) return 'knowledge';
562
+ return 'other';
563
+ }
549
564
 
550
- const config = readConfig();
551
- if (!config || !config.token) {
552
- error(`Not logged in. Run: ${C.cyan}tokrepo login${C.reset}`);
565
+ function analyzePushMetadataQuality(files = [], metadata = {}) {
566
+ const issues = [];
567
+ const add = (severity, code, message, fix = '') => {
568
+ issues.push({ severity, code, message, fix });
569
+ };
570
+
571
+ const normalizedKind = normalizeToolName(metadata.kind || '');
572
+ const inferredKind = inferAssetKindFromPushFiles(files);
573
+ const targetTools = parseCsvList(metadata.targetTools).map(normalizeToolName);
574
+ const installMode = normalizeCodexInstallMode(metadata.installMode);
575
+ const rawInstallMode = String(metadata.installMode || '').trim();
576
+ const entrypoint = String(metadata.entrypoint || '').trim();
577
+ const fileNames = new Set(files.map(file => String(file.name || '').replace(/\\/g, '/')));
578
+ const lowerFileNames = new Set(Array.from(fileNames).map(name => name.toLowerCase()));
579
+ const multiFile = files.length > 1;
580
+ const riskFlags = Array.from(new Set(files.flatMap(file => analyzeInstallRisks(file.name, file.content, file.type))));
581
+ const containsCodex = targetTools.includes('codex');
582
+
583
+ if (!normalizedKind) {
584
+ add('warning', 'missing_asset_kind', `No asset_kind was provided; inferred "${inferredKind}".`, `Recommended: --kind ${inferredKind}`);
585
+ } else if (!['skill', 'prompt', 'knowledge', 'mcp_config', 'script', 'cli_tool', 'config', 'pack', 'other'].includes(normalizedKind)) {
586
+ add('warning', 'unknown_asset_kind', `Unknown asset_kind "${metadata.kind}".`, 'Recommended: use skill, prompt, knowledge, mcp_config, script, cli_tool, config, or pack.');
553
587
  }
554
588
 
589
+ if (targetTools.length === 0) {
590
+ add('warning', 'missing_target_tools', 'No target_tools metadata was provided.', 'Recommended: --target codex or --targets codex,claude_code,gemini_cli.');
591
+ }
592
+
593
+ const invalidTargets = targetTools.filter(tool => !['codex', 'claude_code', 'gemini_cli', 'cursor', 'windsurf', 'opencode'].includes(tool));
594
+ if (invalidTargets.length > 0) {
595
+ add('info', 'unknown_target_tool', `Unknown target tool(s): ${invalidTargets.join(', ')}.`, 'Use stable ids when possible: codex, claude_code, gemini_cli, cursor, windsurf.');
596
+ }
597
+
598
+ if (rawInstallMode && !installMode) {
599
+ add('warning', 'unknown_install_mode', `Unknown install_mode "${metadata.installMode}".`, 'Recommended: single, bundle, split, or stage_only.');
600
+ } else if (!rawInstallMode && (multiFile || containsCodex)) {
601
+ add('warning', 'missing_install_mode', `No install_mode was provided for ${multiFile ? 'a multi-file asset' : 'a Codex-targeted asset'}.`, `Recommended: --install-mode ${multiFile ? 'bundle' : 'single'}.`);
602
+ }
603
+
604
+ if (entrypoint && !fileNames.has(entrypoint) && !lowerFileNames.has(entrypoint.toLowerCase())) {
605
+ add('warning', 'entrypoint_missing', `Entrypoint "${entrypoint}" is not included in the pushed files.`, 'Recommended: set --entrypoint to an included file, usually SKILL.md.');
606
+ }
607
+
608
+ if (!entrypoint && (installMode === 'single' || installMode === 'bundle' || containsCodex)) {
609
+ add('warning', 'missing_entrypoint', 'No entrypoint metadata was provided.', 'Recommended: --entrypoint SKILL.md or the main markdown file.');
610
+ }
611
+
612
+ const effectiveKind = normalizedKind || inferredKind;
613
+ if (containsCodex && ['script', 'cli_tool', 'mcp_config'].includes(effectiveKind) && installMode !== 'stage_only') {
614
+ add('warning', 'high_risk_codex_activation', `${effectiveKind} assets targeted at Codex should stage by default.`, 'Recommended: --install-mode stage_only or publish a markdown skill wrapper.');
615
+ }
616
+ if (riskFlags.includes('executable')) {
617
+ add('warning', 'executes_code_detected', 'Executable code was detected in the asset files.', 'Recommended: declare script/cli_tool or use stage_only for agent installs.');
618
+ }
619
+ if (riskFlags.includes('mcp')) {
620
+ add('warning', 'mcp_config_detected', 'MCP config content was detected.', 'Recommended: --kind mcp_config --install-mode stage_only.');
621
+ }
622
+ if (riskFlags.includes('env')) {
623
+ add('info', 'secret_or_env_mentions', 'Environment variable or secret-like words were mentioned.', 'Check this is documentation only and no real secret is included.');
624
+ }
625
+ if (riskFlags.includes('absolute-path')) {
626
+ add('warning', 'absolute_path_detected', 'Absolute local paths were detected.', 'Recommended: replace machine-specific paths with placeholders.');
627
+ }
628
+
629
+ if (containsCodex && effectiveKind === 'skill') {
630
+ const skillDocs = files.filter(file => isCodexSkillDocument(file));
631
+ if (installMode === 'split' && skillDocs.length !== files.length) {
632
+ add('warning', 'split_requires_skill_docs', 'install_mode=split works best when every markdown file has skill frontmatter.', 'Recommended: use bundle or add name/description frontmatter to each split skill.');
633
+ }
634
+ if (skillDocs.length === 0) {
635
+ add('warning', 'missing_codex_skill_frontmatter', 'No Codex skill frontmatter was found.', 'Recommended: add YAML frontmatter with name and description.');
636
+ }
637
+ }
638
+
639
+ const severityPenalty = issues.reduce((sum, issue) => {
640
+ if (issue.severity === 'warning') return sum + 10;
641
+ return sum + 2;
642
+ }, 0);
643
+ const score = Math.max(0, 100 - severityPenalty);
644
+ const status = issues.some(issue => issue.severity === 'warning') ? 'warn' : 'pass';
645
+
646
+ return {
647
+ score,
648
+ status,
649
+ inferredAssetKind: inferredKind,
650
+ assetKind: normalizedKind || '',
651
+ targetTools,
652
+ installMode: installMode || '',
653
+ entrypoint,
654
+ riskFlags,
655
+ issueCount: issues.length,
656
+ issues,
657
+ };
658
+ }
659
+
660
+ function formatMetadataQualityLabel(report) {
661
+ const color = report.status === 'pass' ? C.green : C.yellow;
662
+ return `${color}${report.status}${C.reset} ${C.bold}${report.score}/100${C.reset}`;
663
+ }
664
+
665
+ function printMetadataQualityReport(report, opts = {}) {
666
+ const compact = Boolean(opts.compact);
667
+ if (!compact) {
668
+ log(`\n${C.bold}Agent metadata quality${C.reset}`);
669
+ log(` Status: ${formatMetadataQualityLabel(report)}`);
670
+ log(` Inferred kind: ${report.inferredAssetKind}`);
671
+ if (report.targetTools.length) log(` Targets: ${report.targetTools.join(', ')}`);
672
+ if (report.installMode) log(` Install mode: ${report.installMode}`);
673
+ if (report.riskFlags.length) log(` Risk flags: ${report.riskFlags.join(', ')}`);
674
+ } else {
675
+ log(`${C.bold}Agent metadata quality suggestions:${C.reset}`);
676
+ }
677
+
678
+ if (report.issues.length === 0) {
679
+ if (!compact) success('Metadata is agent-ready.');
680
+ return;
681
+ }
682
+ for (const issue of report.issues.slice(0, compact ? 8 : report.issues.length)) {
683
+ const color = issue.severity === 'warning' ? C.yellow : C.dim;
684
+ log(` ${color}${issue.severity.toUpperCase()}${C.reset} ${issue.code}: ${issue.message}`);
685
+ if (issue.fix) log(` ${C.dim}${issue.fix}${C.reset}`);
686
+ }
687
+ if (compact && report.issues.length > 8) {
688
+ log(` ${C.dim}...and ${report.issues.length - 8} more suggestion(s). Run --metadata-report for full detail.${C.reset}`);
689
+ }
690
+ if (!compact) log('');
691
+ }
692
+
693
+ async function cmdPush() {
694
+ const args = parseArgs(process.argv);
695
+
555
696
  const projectConfig = readProjectConfig();
556
697
  const baseDir = process.cwd();
557
698
 
@@ -621,6 +762,31 @@ async function cmdPush() {
621
762
  error('No readable text files found to push.');
622
763
  }
623
764
 
765
+ const metadataQuality = analyzePushMetadataQuality(pushFiles, {
766
+ kind,
767
+ targetTools,
768
+ installMode,
769
+ entrypoint,
770
+ title,
771
+ description,
772
+ });
773
+
774
+ if (args.flags.metadata_report || args.flags.metadataReport) {
775
+ if (args.flags.json) {
776
+ outputJson({ schemaVersion: 1, metadataQuality, files: pushFiles.map(file => ({ name: file.name, type: file.type, bytes: Buffer.byteLength(file.content || '') })) });
777
+ } else {
778
+ printMetadataQualityReport(metadataQuality);
779
+ }
780
+ return;
781
+ }
782
+
783
+ // Public registry policy: quality gates advise by default and never block user uploads.
784
+ // TokRepo-owned CI can opt into strict mode with TOKREPO_METADATA_STRICT=1.
785
+ if (process.env.TOKREPO_METADATA_STRICT === '1' && metadataQuality.issues.length > 0 && !args.flags.force) {
786
+ if (!args.flags.json) printMetadataQualityReport(metadataQuality);
787
+ error(`Internal metadata quality gate failed. Fix the issues or re-run with --force to push anyway.`);
788
+ }
789
+
624
790
  // Show summary
625
791
  log(`\n${C.bold}tokrepo push${C.reset}\n`);
626
792
  log(` ${C.bold}Title:${C.reset} ${title}`);
@@ -638,6 +804,7 @@ async function cmdPush() {
638
804
  if (metadataSummary.length > 0) {
639
805
  log(` ${C.bold}Agent meta:${C.reset} ${metadataSummary.join(' · ')}`);
640
806
  }
807
+ log(` ${C.bold}Agent quality:${C.reset} ${formatMetadataQualityLabel(metadataQuality)}`);
641
808
  log('');
642
809
 
643
810
  for (const f of pushFiles) {
@@ -648,9 +815,18 @@ async function cmdPush() {
648
815
 
649
816
  const totalChars = pushFiles.reduce((sum, f) => sum + f.content.length, 0);
650
817
 
818
+ if (metadataQuality.issues.length > 0) {
819
+ printMetadataQualityReport(metadataQuality, { compact: true });
820
+ }
821
+
651
822
  // Push
652
823
  info('Pushing...');
653
824
 
825
+ const config = readConfig();
826
+ if (!config || !config.token) {
827
+ error(`Not logged in. Run: ${C.cyan}tokrepo login${C.reset}`);
828
+ }
829
+
654
830
  try {
655
831
  const data = await apiRequest('POST', '/api/v1/tokenboard/push/upsert', {
656
832
  title,
@@ -841,8 +1017,23 @@ async function cmdSearch() {
841
1017
  data = { ...data, list };
842
1018
  }
843
1019
 
1020
+ const originalCount = (data.list || []).length;
1021
+ data = { ...data, list: applyAgentWorkflowFilters(data.list || [], args.flags) };
1022
+ const filters = {
1023
+ target: args.flags.target || undefined,
1024
+ kind: args.flags.kind || args.flags.assetKind || undefined,
1025
+ policy: args.flags.policy || undefined,
1026
+ };
1027
+
844
1028
  if (args.flags.json) {
845
- outputJson({ query, total: data.total || 0, count: (data.list || []).length, list: data.list || [] });
1029
+ outputJson({
1030
+ query,
1031
+ total: data.total || 0,
1032
+ fetched: originalCount,
1033
+ count: (data.list || []).length,
1034
+ filters,
1035
+ list: data.list || [],
1036
+ });
846
1037
  return;
847
1038
  }
848
1039
 
@@ -859,7 +1050,8 @@ async function cmdSearch() {
859
1050
  return;
860
1051
  }
861
1052
 
862
- log(` ${C.bold}${data.total}${C.reset} results:\n`);
1053
+ const filterText = [filters.target ? `target=${filters.target}` : '', filters.kind ? `kind=${filters.kind}` : '', filters.policy ? `policy=${filters.policy}` : ''].filter(Boolean).join(' · ');
1054
+ log(` ${C.bold}${data.list.length}${C.reset} shown${filterText ? ` ${C.dim}(${filterText})${C.reset}` : ''}${data.total ? ` ${C.dim}from ${data.total} result(s)${C.reset}` : ''}:\n`);
863
1055
 
864
1056
  for (let i = 0; i < data.list.length; i++) {
865
1057
  const wf = data.list[i];
@@ -874,6 +1066,10 @@ async function cmdSearch() {
874
1066
  log(` ${C.dim}${String(i + 1).padStart(2)}.${C.reset} ${C.bold}${wf.title}${C.reset}`);
875
1067
  if (desc) log(` ${desc}`);
876
1068
  if (tags) log(` ${C.cyan}${tags}${C.reset} ${C.dim}★${votes} 👁${views}${C.reset}`);
1069
+ if (wf.compatibility?.codex) {
1070
+ const c = wf.compatibility.codex;
1071
+ log(` ${C.dim}codex: ${c.status} · policy=${c.policyDecision.decision} · kind=${c.assetKind || 'unknown'}${C.reset}`);
1072
+ }
877
1073
  log(` ${C.dim}tokrepo install ${wf.uuid}${C.reset}`);
878
1074
  log('');
879
1075
  }
@@ -984,6 +1180,40 @@ function getWorkflowAssetType(workflow) {
984
1180
  return (workflow.tags[0].slug || workflow.tags[0].name || '').toLowerCase();
985
1181
  }
986
1182
 
1183
+ function workflowAgentMetadata(workflow) {
1184
+ return workflow?.agent_metadata || workflow?.agentMetadata || {};
1185
+ }
1186
+
1187
+ function normalizeCodexInstallMode(mode) {
1188
+ const normalized = String(mode || '').trim().toLowerCase().replace(/-/g, '_');
1189
+ return ['single', 'bundle', 'split', 'stage_only'].includes(normalized) ? normalized : '';
1190
+ }
1191
+
1192
+ function workflowAssetKind(workflow) {
1193
+ const metadata = workflowAgentMetadata(workflow);
1194
+ const explicit = workflow?.asset_kind || workflow?.assetKind || metadata.asset_kind || metadata.assetKind || '';
1195
+ if (explicit) return normalizeToolName(explicit);
1196
+ const assetType = getWorkflowAssetType(workflow);
1197
+ const aliases = {
1198
+ skills: 'skill',
1199
+ prompts: 'prompt',
1200
+ knowledge: 'knowledge',
1201
+ 'mcp-configs': 'mcp_config',
1202
+ mcp: 'mcp_config',
1203
+ scripts: 'script',
1204
+ configs: 'config',
1205
+ tools: 'cli_tool',
1206
+ };
1207
+ return aliases[assetType] || normalizeToolName(assetType);
1208
+ }
1209
+
1210
+ function workflowTargetTools(workflow) {
1211
+ const metadata = workflowAgentMetadata(workflow);
1212
+ return parseCsvList(workflow?.target_tools || workflow?.targetTools || metadata.target_tools || metadata.targetTools)
1213
+ .map(normalizeToolName)
1214
+ .filter(Boolean);
1215
+ }
1216
+
987
1217
  function extractInstallableContents(workflow, assetType) {
988
1218
  const contents = [];
989
1219
  const files = workflow.files || [];
@@ -1129,8 +1359,7 @@ function explicitInstallMode(workflow) {
1129
1359
  workflow?.metadata?.installMode,
1130
1360
  workflow?.metadata?.install_mode,
1131
1361
  ].filter(Boolean);
1132
- const mode = String(candidates[0] || '').toLowerCase();
1133
- return ['single', 'bundle', 'split', 'stage_only'].includes(mode) ? mode : '';
1362
+ return normalizeCodexInstallMode(candidates[0]);
1134
1363
  }
1135
1364
 
1136
1365
  function inferCodexInstallMode(workflow, contents) {
@@ -1187,8 +1416,12 @@ function addPlanFile(plan, destPath, content, sourceName, type) {
1187
1416
  }
1188
1417
 
1189
1418
  function buildCodexInstallPlan(workflow, contents, opts = {}) {
1190
- const installMode = opts.installMode || inferCodexInstallMode(workflow, contents);
1191
- const agentMetadata = workflow?.agent_metadata || workflow?.agentMetadata || {};
1419
+ const serverPlan = opts.serverPlan || null;
1420
+ const serverMetadata = serverPlan?.metadata || serverPlan?.agentMetadata || serverPlan?.agent_metadata || {};
1421
+ const installMode = normalizeCodexInstallMode(opts.installMode)
1422
+ || normalizeCodexInstallMode(metadataValue(serverPlan, 'install_mode', 'installMode', ''))
1423
+ || inferCodexInstallMode(workflow, contents);
1424
+ const agentMetadata = Object.keys(serverMetadata || {}).length > 0 ? serverMetadata : workflowAgentMetadata(workflow);
1192
1425
  const plan = {
1193
1426
  uuid: workflow.uuid,
1194
1427
  title: workflow.title,
@@ -1199,7 +1432,8 @@ function buildCodexInstallPlan(workflow, contents, opts = {}) {
1199
1432
  files: [],
1200
1433
  risks: [],
1201
1434
  agentMetadata,
1202
- contentHash: workflow.content_hash || workflow.contentHash || agentMetadata.content_hash || '',
1435
+ contentHash: workflow.content_hash || workflow.contentHash || agentMetadata.content_hash || agentMetadata.contentHash || metadataValue(serverPlan, 'content_hash', 'contentHash', ''),
1436
+ serverPlan,
1203
1437
  };
1204
1438
 
1205
1439
  if (installMode === 'stage_only') {
@@ -1313,7 +1547,36 @@ function mergedPlanRiskProfile(plan) {
1313
1547
  };
1314
1548
  }
1315
1549
 
1550
+ function policyDecisionFromServerPlan(plan) {
1551
+ const serverPlan = plan?.serverPlan;
1552
+ if (!serverPlan) return null;
1553
+ const raw = metadataValue(serverPlan, 'policy_decision', 'policyDecision', null);
1554
+ if (!raw) return null;
1555
+ if (typeof raw === 'string') {
1556
+ return {
1557
+ decision: raw,
1558
+ requiresConfirmation: raw === 'confirm',
1559
+ reasons: [],
1560
+ };
1561
+ }
1562
+ const decision = String(raw.decision || raw.action || 'allow').trim().toLowerCase();
1563
+ const requiresConfirmation = Boolean(
1564
+ raw.requires_confirmation
1565
+ || raw.requiresConfirmation
1566
+ || metadataValue(serverPlan, 'requires_confirmation', 'requiresConfirmation', false)
1567
+ );
1568
+ const reasons = raw.reasons || raw.reason || [];
1569
+ return {
1570
+ decision,
1571
+ requiresConfirmation,
1572
+ reasons: Array.isArray(reasons) ? reasons : [String(reasons)].filter(Boolean),
1573
+ };
1574
+ }
1575
+
1316
1576
  function decideCodexPolicy(plan) {
1577
+ const serverPolicy = policyDecisionFromServerPlan(plan);
1578
+ if (serverPolicy) return serverPolicy;
1579
+
1317
1580
  const metadata = plan.agentMetadata || {};
1318
1581
  const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
1319
1582
  const assetKind = normalizeToolName(metadataValue(metadata, 'asset_kind', 'assetKind', ''));
@@ -1367,6 +1630,9 @@ function decideCodexPolicy(plan) {
1367
1630
  }
1368
1631
 
1369
1632
  function buildPublicPlanActions(plan) {
1633
+ const serverActions = metadataValue(plan.serverPlan, 'actions', 'actions', null);
1634
+ if (serverConcretePlanMatchesLocal(plan) && Array.isArray(serverActions) && serverActions.length > 0) return serverActions;
1635
+
1370
1636
  const stage = plan.installMode === 'stage_only';
1371
1637
  return plan.files.map(file => ({
1372
1638
  type: stage ? 'stage_file' : 'write_file',
@@ -1380,7 +1646,23 @@ function buildPublicPlanActions(plan) {
1380
1646
  }));
1381
1647
  }
1382
1648
 
1649
+ function serverConcretePlanMatchesLocal(plan) {
1650
+ const serverActions = metadataValue(plan.serverPlan, 'actions', 'actions', null);
1651
+ if (!Array.isArray(serverActions) || serverActions.length !== (plan.files || []).length) return false;
1652
+ return serverActions.every((action, index) => {
1653
+ const file = plan.files[index];
1654
+ if (!file) return false;
1655
+ const serverPath = path.resolve(expandHomePath(action.path || ''));
1656
+ const localPath = path.resolve(file.path || '');
1657
+ const serverSha = action.sha256 || action.sha || '';
1658
+ return serverPath === localPath && (!serverSha || serverSha === file.sha256);
1659
+ });
1660
+ }
1661
+
1383
1662
  function buildPublicPlanPreconditions(plan, policyDecision) {
1663
+ const serverPreconditions = metadataValue(plan.serverPlan, 'preconditions', 'preconditions', null);
1664
+ if (Array.isArray(serverPreconditions) && serverPreconditions.length > 0) return serverPreconditions;
1665
+
1384
1666
  const metadata = plan.agentMetadata || {};
1385
1667
  const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
1386
1668
  const out = [
@@ -1405,6 +1687,9 @@ function buildPublicPlanPreconditions(plan, policyDecision) {
1405
1687
  }
1406
1688
 
1407
1689
  function buildPublicPlanRollback(plan) {
1690
+ const serverRollback = metadataValue(plan.serverPlan, 'rollback', 'rollback', null);
1691
+ if (serverConcretePlanMatchesLocal(plan) && Array.isArray(serverRollback) && serverRollback.length > 0) return serverRollback;
1692
+
1408
1693
  const seen = new Set();
1409
1694
  const rollback = [];
1410
1695
  for (const file of plan.files) {
@@ -1416,11 +1701,18 @@ function buildPublicPlanRollback(plan) {
1416
1701
  }
1417
1702
 
1418
1703
  function buildPublicPlanPostVerify(plan) {
1704
+ const serverPostVerify = metadataValue(plan.serverPlan, 'post_verify', 'postVerify', null);
1705
+ if (serverConcretePlanMatchesLocal(plan) && Array.isArray(serverPostVerify) && serverPostVerify.length > 0) return serverPostVerify;
1706
+
1419
1707
  const metadata = plan.agentMetadata || {};
1420
1708
  const verification = metadataValue(metadata, 'verification', 'verification', {}) || {};
1421
1709
  const out = plan.files.map(file => ({ type: 'file_sha256', path: file.path, sha256: file.sha256 }));
1710
+ const installedPaths = new Set(plan.files.map(file => path.resolve(file.path)));
1422
1711
  for (const expected of (verification.expected_files || verification.expectedFiles || [])) {
1423
- out.push({ type: 'expected_file', path: expected });
1712
+ const resolvedExpected = path.resolve(resolveVerifyPath(expected, { baseDir: plan.baseDir, files: plan.files }));
1713
+ if (installedPaths.has(resolvedExpected)) {
1714
+ out.push({ type: 'expected_file', path: expected });
1715
+ }
1424
1716
  }
1425
1717
  for (const command of (verification.commands || [])) {
1426
1718
  out.push({ type: 'command', command });
@@ -1431,8 +1723,11 @@ function buildPublicPlanPostVerify(plan) {
1431
1723
  function publicInstallPlan(plan) {
1432
1724
  const policyDecision = decideCodexPolicy(plan);
1433
1725
  const actions = buildPublicPlanActions(plan);
1726
+ const schemaVersion = Number(metadataValue(plan.serverPlan, 'schema_version', 'schemaVersion', 2)) || 2;
1434
1727
  return {
1435
- schemaVersion: 2,
1728
+ schemaVersion,
1729
+ sourceOfTruth: plan.serverPlan ? 'api_install_plan_v2' : 'local_fallback',
1730
+ concretePlanSource: serverConcretePlanMatchesLocal(plan) ? 'api_install_plan_v2' : 'local_fallback',
1436
1731
  uuid: plan.uuid,
1437
1732
  title: plan.title,
1438
1733
  sourceUrl: plan.sourceUrl,
@@ -1460,6 +1755,95 @@ function publicInstallPlan(plan) {
1460
1755
  };
1461
1756
  }
1462
1757
 
1758
+ function workflowCodexCompatibility(workflow) {
1759
+ const metadata = workflowAgentMetadata(workflow);
1760
+ const assetKind = workflowAssetKind(workflow);
1761
+ const targetTools = workflowTargetTools(workflow);
1762
+ const installMode = normalizeCodexInstallMode(metadata.install_mode || metadata.installMode || workflow.install_mode || workflow.installMode) || 'single';
1763
+ const policy = decideCodexPolicy({
1764
+ agentMetadata: {
1765
+ ...metadata,
1766
+ asset_kind: assetKind,
1767
+ target_tools: targetTools,
1768
+ install_mode: installMode,
1769
+ },
1770
+ risks: [],
1771
+ installMode,
1772
+ });
1773
+ const scores = { allow: 100, confirm: 70, stage_only: 40, deny: 0 };
1774
+ const statuses = {
1775
+ allow: 'native',
1776
+ confirm: 'requires_confirmation',
1777
+ stage_only: 'stage_only',
1778
+ deny: 'denied',
1779
+ };
1780
+ return {
1781
+ targetTool: 'codex',
1782
+ status: statuses[policy.decision] || 'unknown',
1783
+ score: scores[policy.decision] ?? 50,
1784
+ assetKind,
1785
+ targetTools,
1786
+ installMode,
1787
+ policyDecision: policy,
1788
+ };
1789
+ }
1790
+
1791
+ function workflowMatchesAgentFilters(workflow, flags = {}) {
1792
+ const target = normalizeInstallTarget(flags.target || '');
1793
+ const requestedKinds = parseCsvList(flags.kind || flags.assetKind || flags.asset_kind).map(normalizeToolName);
1794
+ const requestedPolicies = parseCsvList(flags.policy).map(s => String(s).trim().toLowerCase());
1795
+ const assetKind = workflowAssetKind(workflow);
1796
+ const targetTools = workflowTargetTools(workflow);
1797
+ const compatibility = workflowCodexCompatibility(workflow);
1798
+
1799
+ if (target === 'codex') {
1800
+ if (targetTools.length > 0 && !targetTools.includes('codex')) return false;
1801
+ } else if (target && targetTools.length > 0 && !targetTools.includes(target)) {
1802
+ return false;
1803
+ }
1804
+
1805
+ if (requestedKinds.length > 0) {
1806
+ const kindAliases = new Set([assetKind, `${assetKind}s`, assetKind.replace(/_/g, '-')]);
1807
+ const tags = (workflow.tags || []).flatMap(t => [t.slug, t.name]).filter(Boolean).map(normalizeToolName);
1808
+ const matchesKind = requestedKinds.some(kind => kindAliases.has(kind) || tags.includes(kind) || tags.includes(`${kind}s`));
1809
+ if (!matchesKind) return false;
1810
+ }
1811
+
1812
+ if (requestedPolicies.length > 0) {
1813
+ const decision = compatibility.policyDecision.decision;
1814
+ const aliases = {
1815
+ safe: 'allow',
1816
+ staged: 'stage_only',
1817
+ stage: 'stage_only',
1818
+ block: 'deny',
1819
+ blocked: 'deny',
1820
+ };
1821
+ const normalizedPolicies = requestedPolicies.map(policy => aliases[policy] || policy);
1822
+ if (!normalizedPolicies.includes(decision)) return false;
1823
+ }
1824
+
1825
+ return true;
1826
+ }
1827
+
1828
+ function enrichWorkflowForAgent(workflow) {
1829
+ const compatibility = workflowCodexCompatibility(workflow);
1830
+ return {
1831
+ ...workflow,
1832
+ assetKind: compatibility.assetKind,
1833
+ targetTools: compatibility.targetTools,
1834
+ compatibility: {
1835
+ codex: compatibility,
1836
+ },
1837
+ policyDecision: compatibility.policyDecision,
1838
+ };
1839
+ }
1840
+
1841
+ function applyAgentWorkflowFilters(list, flags = {}) {
1842
+ const shouldEnrich = flags.target || flags.kind || flags.assetKind || flags.asset_kind || flags.policy;
1843
+ const filtered = (list || []).filter(item => workflowMatchesAgentFilters(item, flags));
1844
+ return shouldEnrich ? filtered.map(enrichWorkflowForAgent) : filtered;
1845
+ }
1846
+
1463
1847
  function hasCodexInstallRisks(plan) {
1464
1848
  const decision = decideCodexPolicy(plan).decision;
1465
1849
  return decision === 'confirm' || decision === 'stage_only' || decision === 'deny';
@@ -1538,6 +1922,102 @@ function executeStageOnlyCodexPlan(plan) {
1538
1922
  return { dryRun: true, staged: true, stageOnly: true, stagePath, plan: publicInstallPlan(plan), installedFiles };
1539
1923
  }
1540
1924
 
1925
+ function expandHomePath(input) {
1926
+ const value = String(input || '');
1927
+ if (value === '~') return os.homedir();
1928
+ if (value.startsWith('~/')) return path.join(os.homedir(), value.slice(2));
1929
+ return value;
1930
+ }
1931
+
1932
+ function resolveVerifyPath(checkPath, publicPlan) {
1933
+ const expanded = expandHomePath(checkPath);
1934
+ if (path.isAbsolute(expanded)) return expanded;
1935
+ const baseDir = publicPlan.baseDir || path.dirname(publicPlan.files?.[0]?.path || CODEX_SKILLS_DIR);
1936
+ return path.join(baseDir, expanded);
1937
+ }
1938
+
1939
+ function runCodexPostVerify(publicPlan, opts = {}) {
1940
+ const checks = [];
1941
+ let ok = true;
1942
+ for (const check of (publicPlan.postVerify || [])) {
1943
+ if (check.type === 'file_sha256') {
1944
+ const filePath = resolveVerifyPath(check.path, publicPlan);
1945
+ const exists = fs.existsSync(filePath);
1946
+ const actualSha = exists ? currentFileSha(filePath) : '';
1947
+ const passed = Boolean(exists && actualSha === check.sha256);
1948
+ if (!passed) ok = false;
1949
+ checks.push({ ...check, path: filePath, status: passed ? 'pass' : 'fail', actualSha });
1950
+ } else if (check.type === 'expected_file') {
1951
+ const filePath = resolveVerifyPath(check.path, publicPlan);
1952
+ const passed = fs.existsSync(filePath);
1953
+ if (!passed) ok = false;
1954
+ checks.push({ ...check, path: filePath, status: passed ? 'pass' : 'fail' });
1955
+ } else if (check.type === 'command') {
1956
+ if (!opts.verifyCommands) {
1957
+ checks.push({ ...check, status: 'skipped', message: 'command verification is opt-in; re-run with --verify-commands' });
1958
+ continue;
1959
+ }
1960
+ try {
1961
+ const childProcess = require('child_process');
1962
+ childProcess.execSync(String(check.command || ''), { stdio: 'pipe', shell: true, timeout: 30000 });
1963
+ checks.push({ ...check, status: 'pass' });
1964
+ } catch (e) {
1965
+ ok = false;
1966
+ checks.push({ ...check, status: 'fail', message: e.message });
1967
+ }
1968
+ } else {
1969
+ checks.push({ ...check, status: 'skipped', message: 'unknown verification check type' });
1970
+ }
1971
+ }
1972
+ return { ok, checks };
1973
+ }
1974
+
1975
+ function createCodexSessionId(operation = 'session') {
1976
+ const stamp = new Date().toISOString().replace(/[-:.]/g, '').replace('T', '-').replace('Z', '');
1977
+ const random = crypto.randomBytes(4).toString('hex');
1978
+ return `${slugify(operation, 'session')}-${stamp}-${random}`;
1979
+ }
1980
+
1981
+ function writeCodexSession(record) {
1982
+ if (!fs.existsSync(CODEX_SESSIONS_DIR)) {
1983
+ fs.mkdirSync(CODEX_SESSIONS_DIR, { recursive: true, mode: 0o700 });
1984
+ }
1985
+ const sessionId = record.sessionId || createCodexSessionId(record.operation || 'session');
1986
+ const sessionPath = path.join(CODEX_SESSIONS_DIR, `${sessionId}.json`);
1987
+ const payload = {
1988
+ schemaVersion: 1,
1989
+ sessionId,
1990
+ createdAt: new Date().toISOString(),
1991
+ cliVersion: CLI_VERSION,
1992
+ argv: process.argv.slice(2),
1993
+ ...record,
1994
+ sessionId,
1995
+ };
1996
+ fs.writeFileSync(sessionPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
1997
+ return { sessionId, sessionPath };
1998
+ }
1999
+
2000
+ function readCodexSessions() {
2001
+ try {
2002
+ if (!fs.existsSync(CODEX_SESSIONS_DIR)) return [];
2003
+ return fs.readdirSync(CODEX_SESSIONS_DIR)
2004
+ .filter(name => name.endsWith('.json'))
2005
+ .map(name => {
2006
+ const sessionPath = path.join(CODEX_SESSIONS_DIR, name);
2007
+ try {
2008
+ const parsed = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
2009
+ return { ...parsed, sessionPath };
2010
+ } catch {
2011
+ return null;
2012
+ }
2013
+ })
2014
+ .filter(Boolean)
2015
+ .sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
2016
+ } catch {
2017
+ return [];
2018
+ }
2019
+ }
2020
+
1541
2021
  function readCodexManifest() {
1542
2022
  try {
1543
2023
  const parsed = JSON.parse(fs.readFileSync(CODEX_MANIFEST_FILE, 'utf8'));
@@ -1546,7 +2026,15 @@ function readCodexManifest() {
1546
2026
  return { schemaVersion: 1, installs: [] };
1547
2027
  }
1548
2028
 
1549
- function writeCodexManifestRecord(plan, installedFiles) {
2029
+ function writeCodexManifest(manifest) {
2030
+ if (!fs.existsSync(CODEX_TOKREPO_DIR)) {
2031
+ fs.mkdirSync(CODEX_TOKREPO_DIR, { recursive: true, mode: 0o700 });
2032
+ }
2033
+ manifest.updatedAt = new Date().toISOString();
2034
+ fs.writeFileSync(CODEX_MANIFEST_FILE, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
2035
+ }
2036
+
2037
+ function writeCodexManifestRecord(plan, installedFiles, sessionInfo = {}, verification = null) {
1550
2038
  if (!fs.existsSync(CODEX_TOKREPO_DIR)) {
1551
2039
  fs.mkdirSync(CODEX_TOKREPO_DIR, { recursive: true, mode: 0o700 });
1552
2040
  }
@@ -1561,6 +2049,9 @@ function writeCodexManifestRecord(plan, installedFiles) {
1561
2049
  installedAt,
1562
2050
  contentHash: plan.contentHash || '',
1563
2051
  agentMetadata: plan.agentMetadata || {},
2052
+ sessionId: sessionInfo.sessionId,
2053
+ sessionPath: sessionInfo.sessionPath,
2054
+ verification,
1564
2055
  installedFiles: installedFiles.map(file => ({
1565
2056
  path: file.path,
1566
2057
  sourceName: file.sourceName,
@@ -1573,16 +2064,58 @@ function writeCodexManifestRecord(plan, installedFiles) {
1573
2064
  manifest.installs = manifest.installs.filter(item => !(item.uuid === plan.uuid && item.targetTool === 'codex'));
1574
2065
  manifest.installs.push(record);
1575
2066
  manifest.updatedAt = installedAt;
1576
- fs.writeFileSync(CODEX_MANIFEST_FILE, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
2067
+ writeCodexManifest(manifest);
1577
2068
  return record;
1578
2069
  }
1579
2070
 
1580
2071
  function executeCodexInstallPlan(plan, opts = {}) {
1581
- if (opts.dryRun) return { dryRun: true, plan: publicInstallPlan(plan), installedFiles: [] };
1582
- if (plan.installMode === 'stage_only') return executeStageOnlyCodexPlan(plan);
2072
+ const publicPlan = publicInstallPlan(plan);
2073
+ if (opts.dryRun) {
2074
+ const session = writeCodexSession({
2075
+ operation: 'install',
2076
+ status: 'dry_run',
2077
+ targetTool: 'codex',
2078
+ uuid: plan.uuid,
2079
+ title: plan.title,
2080
+ sourceUrl: plan.sourceUrl,
2081
+ policyDecision: publicPlan.policyDecision,
2082
+ plan: publicPlan,
2083
+ result: { dryRun: true, installedFiles: [] },
2084
+ });
2085
+ return { dryRun: true, plan: publicPlan, installedFiles: [], ...session };
2086
+ }
2087
+ if (plan.installMode === 'stage_only') {
2088
+ const result = executeStageOnlyCodexPlan(plan);
2089
+ const verification = runCodexPostVerify(result.plan, opts);
2090
+ const session = writeCodexSession({
2091
+ operation: 'install',
2092
+ status: 'stage_only',
2093
+ targetTool: 'codex',
2094
+ uuid: plan.uuid,
2095
+ title: plan.title,
2096
+ sourceUrl: plan.sourceUrl,
2097
+ policyDecision: result.plan.policyDecision,
2098
+ plan: result.plan,
2099
+ installedFiles: result.installedFiles,
2100
+ verification,
2101
+ result: { staged: true, stageOnly: true, stagePath: result.stagePath },
2102
+ });
2103
+ return { ...result, verification, ...session };
2104
+ }
1583
2105
  if (opts.stage) {
1584
2106
  const stagePath = stageCodexInstallPlan(plan);
1585
- return { dryRun: true, staged: true, stagePath, plan: publicInstallPlan(plan), installedFiles: [] };
2107
+ const session = writeCodexSession({
2108
+ operation: 'install',
2109
+ status: 'staged',
2110
+ targetTool: 'codex',
2111
+ uuid: plan.uuid,
2112
+ title: plan.title,
2113
+ sourceUrl: plan.sourceUrl,
2114
+ policyDecision: publicPlan.policyDecision,
2115
+ plan: publicPlan,
2116
+ result: { staged: true, stagePath },
2117
+ });
2118
+ return { dryRun: true, staged: true, stagePath, plan: publicPlan, installedFiles: [], ...session };
1586
2119
  }
1587
2120
 
1588
2121
  const installedFiles = [];
@@ -1602,8 +2135,22 @@ function executeCodexInstallPlan(plan, opts = {}) {
1602
2135
  });
1603
2136
  }
1604
2137
 
1605
- const manifestRecord = writeCodexManifestRecord(plan, installedFiles);
1606
- return { dryRun: false, plan: publicInstallPlan(plan), installedFiles, manifestRecord };
2138
+ const verification = runCodexPostVerify(publicPlan, opts);
2139
+ const session = writeCodexSession({
2140
+ operation: 'install',
2141
+ status: 'installed',
2142
+ targetTool: 'codex',
2143
+ uuid: plan.uuid,
2144
+ title: plan.title,
2145
+ sourceUrl: plan.sourceUrl,
2146
+ policyDecision: publicPlan.policyDecision,
2147
+ plan: publicPlan,
2148
+ installedFiles,
2149
+ verification,
2150
+ result: { installedFiles },
2151
+ });
2152
+ const manifestRecord = writeCodexManifestRecord(plan, installedFiles, session, verification);
2153
+ return { dryRun: false, plan: publicPlan, installedFiles, manifestRecord, verification, ...session };
1607
2154
  }
1608
2155
 
1609
2156
  async function installCodexAsset(workflow, contents, opts = {}) {
@@ -1631,6 +2178,7 @@ async function cmdInstall() {
1631
2178
  dryRun: Boolean(args.flags.dryRun || args.flags.dry_run),
1632
2179
  stage: Boolean(args.flags.stage),
1633
2180
  approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
2181
+ verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
1634
2182
  json: Boolean(args.flags.json),
1635
2183
  manifest: Boolean(args.flags.manifest),
1636
2184
  };
@@ -1807,7 +2355,8 @@ async function installOneAsset(target, config, apiBase, opts) {
1807
2355
  if (targetTool === 'codex') {
1808
2356
  let result;
1809
2357
  try {
1810
- result = await installCodexAsset(workflow, contents, opts);
2358
+ const serverPlan = opts.serverPlan !== undefined ? opts.serverPlan : await fetchServerCodexInstallPlan(uuid, config, apiBase);
2359
+ result = await installCodexAsset(workflow, contents, { ...opts, serverPlan });
1811
2360
  } catch (e) {
1812
2361
  die(e.message);
1813
2362
  }
@@ -1821,6 +2370,7 @@ async function installOneAsset(target, config, apiBase, opts) {
1821
2370
  } else {
1822
2371
  info(`No Codex skill files were written. Re-run with --approve-mcp or --yes to install.`);
1823
2372
  }
2373
+ if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
1824
2374
  } else if (opts.dryRun) {
1825
2375
  info(`Dry run: ${plan.files.length} file(s) would be installed to ${CODEX_SKILLS_DIR}`);
1826
2376
  for (const file of plan.files) {
@@ -1828,6 +2378,7 @@ async function installOneAsset(target, config, apiBase, opts) {
1828
2378
  log(` ${C.dim}•${C.reset} ~/${rel}`);
1829
2379
  if (file.riskFlags.length) log(` ${C.yellow}${file.riskFlags.join(', ')}${C.reset}`);
1830
2380
  }
2381
+ if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
1831
2382
  } else {
1832
2383
  for (const file of result.installedFiles) {
1833
2384
  const relPath = path.relative(os.homedir(), file.path);
@@ -1836,6 +2387,8 @@ async function installOneAsset(target, config, apiBase, opts) {
1836
2387
  log('');
1837
2388
  success(`${result.installedFiles.length} file(s) installed from ${C.bold}${workflow.title}${C.reset}`);
1838
2389
  log(` ${C.dim}Manifest: ${CODEX_MANIFEST_FILE}${C.reset}`);
2390
+ if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
2391
+ if (result.verification && !result.verification.ok) log(` ${C.yellow}Verification: failed${C.reset}`);
1839
2392
  log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
1840
2393
  }
1841
2394
  }
@@ -1851,6 +2404,9 @@ async function installOneAsset(target, config, apiBase, opts) {
1851
2404
  installedFiles: result.installedFiles || [],
1852
2405
  plan: result.plan,
1853
2406
  manifestPath: CODEX_MANIFEST_FILE,
2407
+ sessionId: result.sessionId,
2408
+ sessionPath: result.sessionPath,
2409
+ verification: result.verification,
1854
2410
  };
1855
2411
  }
1856
2412
 
@@ -2012,8 +2568,16 @@ async function cmdList() {
2012
2568
  data = { ...data, list };
2013
2569
  }
2014
2570
 
2571
+ const originalCount = (data.list || []).length;
2572
+ data = { ...data, list: applyAgentWorkflowFilters(data.list || [], args.flags) };
2573
+ const filters = {
2574
+ target: args.flags.target || undefined,
2575
+ kind: args.flags.kind || args.flags.assetKind || undefined,
2576
+ policy: args.flags.policy || undefined,
2577
+ };
2578
+
2015
2579
  if (args.flags.json) {
2016
- outputJson({ total: data.total || 0, count: (data.list || []).length, list: data.list || [] });
2580
+ outputJson({ total: data.total || 0, fetched: originalCount, count: (data.list || []).length, filters, list: data.list || [] });
2017
2581
  return;
2018
2582
  }
2019
2583
 
@@ -2022,11 +2586,16 @@ async function cmdList() {
2022
2586
  return;
2023
2587
  }
2024
2588
 
2025
- log(` ${C.bold}${data.total}${C.reset} assets:\n`);
2589
+ const filterText = [filters.target ? `target=${filters.target}` : '', filters.kind ? `kind=${filters.kind}` : '', filters.policy ? `policy=${filters.policy}` : ''].filter(Boolean).join(' · ');
2590
+ log(` ${C.bold}${data.list.length}${C.reset} assets${filterText ? ` ${C.dim}(${filterText})${C.reset}` : ''}${data.total ? ` ${C.dim}from ${data.total}${C.reset}` : ''}:\n`);
2026
2591
 
2027
2592
  for (const wf of data.list) {
2028
2593
  const views = wf.view_count || 0;
2029
2594
  log(` ${C.cyan}${wf.uuid.substring(0,8)}${C.reset} ${C.bold}${wf.title}${C.reset}`);
2595
+ if (wf.compatibility?.codex) {
2596
+ const c = wf.compatibility.codex;
2597
+ log(` ${C.dim} codex=${c.status} · policy=${c.policyDecision.decision} · kind=${c.assetKind || 'unknown'}${C.reset}`);
2598
+ }
2030
2599
  log(` ${C.dim} ${views} views · https://tokrepo.com/en/workflows/${wf.uuid}${C.reset}\n`);
2031
2600
  }
2032
2601
  } catch (e) {
@@ -2236,13 +2805,16 @@ async function cmdClone() {
2236
2805
  const assetType = getWorkflowAssetType(workflow);
2237
2806
  const contents = extractInstallableContents(workflow, assetType);
2238
2807
  if (contents.length === 0) throw new Error('No installable content found');
2808
+ const serverPlan = await fetchServerCodexInstallPlan(workflow.uuid, config, apiBase);
2239
2809
  const result = await installCodexAsset(workflow, contents, {
2240
2810
  ...args.flags,
2241
2811
  dryRun,
2242
2812
  stage: Boolean(args.flags.stage),
2243
2813
  approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
2814
+ verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
2244
2815
  json: true,
2245
2816
  throwOnError: true,
2817
+ serverPlan,
2246
2818
  });
2247
2819
  if (!dryRun) installedCount += result.installedFiles.length;
2248
2820
  results.push({
@@ -2255,6 +2827,9 @@ async function cmdClone() {
2255
2827
  files: result.plan.files,
2256
2828
  installedFiles: result.installedFiles || [],
2257
2829
  risks: result.plan.risks,
2830
+ sessionId: result.sessionId,
2831
+ sessionPath: result.sessionPath,
2832
+ verification: result.verification,
2258
2833
  });
2259
2834
  if (!json) {
2260
2835
  const fileCount = (dryRun || args.flags.stage) ? result.plan.files.length : result.installedFiles.length;
@@ -2383,6 +2958,154 @@ async function fetchWorkflowForInstall(uuid, config, apiBase) {
2383
2958
  return { workflow, contents };
2384
2959
  }
2385
2960
 
2961
+ async function fetchServerCodexInstallPlan(uuid, config, apiBase) {
2962
+ try {
2963
+ const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/install-plan?uuid=${encodeURIComponent(uuid)}&target=codex`, null, config?.token, apiBase);
2964
+ return data?.plan || data || null;
2965
+ } catch {
2966
+ return null;
2967
+ }
2968
+ }
2969
+
2970
+ function runSelfCliJson(cliArgs, opts = {}) {
2971
+ const childProcess = require('child_process');
2972
+ const stdout = childProcess.execFileSync(process.execPath, [__filename, ...cliArgs], {
2973
+ env: { ...process.env, ...(opts.env || {}), TOKREPO_NONINTERACTIVE: '1' },
2974
+ cwd: opts.cwd || process.cwd(),
2975
+ encoding: 'utf8',
2976
+ maxBuffer: 50 * 1024 * 1024,
2977
+ });
2978
+ return JSON.parse(stdout);
2979
+ }
2980
+
2981
+ function makeEvalResult(name, ok, details = {}) {
2982
+ return {
2983
+ name,
2984
+ ok: Boolean(ok),
2985
+ status: ok ? 'pass' : 'fail',
2986
+ ...details,
2987
+ };
2988
+ }
2989
+
2990
+ function createTempDir(prefix) {
2991
+ return fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
2992
+ }
2993
+
2994
+ async function cmdEvalAgent() {
2995
+ const args = parseArgs(process.argv);
2996
+ const json = Boolean(args.flags.json);
2997
+ const sampleUuid = args.flags.uuid || '91aeb22d-eff0-4310-abc6-811d2394b420';
2998
+ const query = args.flags.keyword || args.flags.query || 'video';
2999
+ const keepTemp = Boolean(args.flags.keep_temp || args.flags.keepTemp);
3000
+ const startedAt = new Date().toISOString();
3001
+ const results = [];
3002
+ const tempRoots = [];
3003
+
3004
+ if (!json) log(`\n${C.bold}tokrepo eval-agent${C.reset}\n`);
3005
+
3006
+ const runScenario = async (name, fn) => {
3007
+ const start = Date.now();
3008
+ try {
3009
+ const details = await fn();
3010
+ const result = makeEvalResult(name, true, { durationMs: Date.now() - start, ...details });
3011
+ results.push(result);
3012
+ if (!json) success(`${name} (${result.durationMs}ms)`);
3013
+ } catch (e) {
3014
+ const result = makeEvalResult(name, false, { durationMs: Date.now() - start, error: e.message });
3015
+ results.push(result);
3016
+ if (!json) warn(`${name}: ${e.message}`);
3017
+ }
3018
+ };
3019
+
3020
+ await runScenario('search_filters_codex_allow_skill', async () => {
3021
+ const data = runSelfCliJson(['search', query, '--target', 'codex', '--kind', 'skill', '--policy', 'allow', '--json', '--page-size', '10']);
3022
+ if (!data.count || !Array.isArray(data.list)) throw new Error('filtered search returned no list');
3023
+ const bad = data.list.find(item => item.policyDecision?.decision && item.policyDecision.decision !== 'allow');
3024
+ if (bad) throw new Error(`search returned non-allow asset ${bad.uuid}`);
3025
+ return { count: data.count, firstUuid: data.list[0]?.uuid, firstTitle: data.list[0]?.title };
3026
+ });
3027
+
3028
+ await runScenario('install_plan_contract', async () => {
3029
+ const plan = runSelfCliJson(['plan', sampleUuid, '--target', 'codex']);
3030
+ if (plan.schemaVersion !== 2) throw new Error(`expected schemaVersion 2, got ${plan.schemaVersion}`);
3031
+ if (!plan.policyDecision?.decision) throw new Error('missing policyDecision');
3032
+ if (!Array.isArray(plan.actions) || plan.actions.length === 0) throw new Error('missing actions');
3033
+ if (!Array.isArray(plan.rollback) || plan.rollback.length === 0) throw new Error('missing rollback');
3034
+ if (!Array.isArray(plan.postVerify) || plan.postVerify.length === 0) throw new Error('missing postVerify');
3035
+ return {
3036
+ sourceOfTruth: plan.sourceOfTruth,
3037
+ concretePlanSource: plan.concretePlanSource,
3038
+ policy: plan.policyDecision.decision,
3039
+ actions: plan.actions.length,
3040
+ };
3041
+ });
3042
+
3043
+ await runScenario('metadata_quality_report_non_blocking', async () => {
3044
+ const tmp = createTempDir('tokrepo-eval-quality');
3045
+ tempRoots.push(tmp);
3046
+ const skillPath = path.join(tmp, 'SKILL.md');
3047
+ fs.writeFileSync(skillPath, `---\nname: eval-agent-sample\ndescription: \"Sample skill used by tokrepo eval-agent.\"\n---\n\n# Eval Agent Sample\n\nUse this to test metadata quality reporting.\n`);
3048
+ const report = runSelfCliJson(['push', skillPath, '--metadata-report', '--json', '--kind', 'skill', '--target', 'codex', '--install-mode', 'single', '--entrypoint', 'SKILL.md']);
3049
+ if (!report.metadataQuality) throw new Error('missing metadataQuality');
3050
+ if (report.metadataQuality.status !== 'pass') throw new Error(`expected pass, got ${report.metadataQuality.status}`);
3051
+ return { score: report.metadataQuality.score, status: report.metadataQuality.status };
3052
+ });
3053
+
3054
+ await runScenario('codex_install_verify_and_rollback', async () => {
3055
+ const tmpHome = createTempDir('tokrepo-eval-home');
3056
+ tempRoots.push(tmpHome);
3057
+ const env = { HOME: tmpHome };
3058
+ const install = runSelfCliJson(['install', sampleUuid, '--target', 'codex', '--yes', '--json'], { env });
3059
+ if (!install.sessionId) throw new Error('install did not create sessionId');
3060
+ if (!install.verification?.ok) throw new Error('install verification failed');
3061
+ if (!install.installedFiles?.length) throw new Error('no files installed');
3062
+ const installed = runSelfCliJson(['installed', '--target', 'codex', '--json'], { env });
3063
+ if (installed.count < 1) throw new Error('installed manifest did not record asset');
3064
+ const rollback = runSelfCliJson(['rollback', '--last', '--target', 'codex', '--json'], { env });
3065
+ if (!rollback.removedFiles?.length) throw new Error('rollback removed no files');
3066
+ const after = runSelfCliJson(['installed', '--target', 'codex', '--json'], { env });
3067
+ if (after.count !== 0) throw new Error(`manifest still has ${after.count} install(s) after rollback`);
3068
+ return {
3069
+ installedFiles: install.installedFiles.length,
3070
+ sessionId: install.sessionId,
3071
+ removedFiles: rollback.removedFiles.length,
3072
+ };
3073
+ });
3074
+
3075
+ if (!keepTemp) {
3076
+ for (const dir of tempRoots) {
3077
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
3078
+ }
3079
+ }
3080
+
3081
+ const failed = results.filter(result => !result.ok);
3082
+ const summary = {
3083
+ schemaVersion: 1,
3084
+ startedAt,
3085
+ finishedAt: new Date().toISOString(),
3086
+ cliVersion: CLI_VERSION,
3087
+ targetTool: 'codex',
3088
+ sampleUuid,
3089
+ query,
3090
+ status: failed.length === 0 ? 'pass' : 'fail',
3091
+ passed: results.length - failed.length,
3092
+ failed: failed.length,
3093
+ count: results.length,
3094
+ tempRoots: keepTemp ? tempRoots : [],
3095
+ results,
3096
+ };
3097
+
3098
+ if (json) {
3099
+ outputJson(summary);
3100
+ } else {
3101
+ log('');
3102
+ if (summary.status === 'pass') success(`Agent eval passed: ${summary.passed}/${summary.count}`);
3103
+ else warn(`Agent eval failed: ${summary.failed}/${summary.count}`);
3104
+ }
3105
+
3106
+ if (failed.length > 0) process.exitCode = 1;
3107
+ }
3108
+
2386
3109
  async function cmdSyncInstalled() {
2387
3110
  const args = parseArgs(process.argv);
2388
3111
  const targetTool = validateInstallTarget(args.flags.target || 'codex');
@@ -2417,7 +3140,8 @@ async function cmdSyncInstalled() {
2417
3140
 
2418
3141
  try {
2419
3142
  const { workflow, contents } = await fetchWorkflowForInstall(uuid, config, apiBase);
2420
- const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode });
3143
+ const serverPlan = await fetchServerCodexInstallPlan(uuid, config, apiBase);
3144
+ const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode, serverPlan });
2421
3145
  const diff = diffCodexPlanWithLocal(plan, record);
2422
3146
  const shouldWrite = force || diff.needsUpdate;
2423
3147
 
@@ -2455,8 +3179,10 @@ async function cmdSyncInstalled() {
2455
3179
  stage,
2456
3180
  installMode: record.installMode || record.install_mode,
2457
3181
  approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
3182
+ verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
2458
3183
  json: true,
2459
3184
  throwOnError: true,
3185
+ serverPlan,
2460
3186
  });
2461
3187
 
2462
3188
  results.push({
@@ -2468,6 +3194,9 @@ async function cmdSyncInstalled() {
2468
3194
  stagePath: installResult.stagePath,
2469
3195
  installedFiles: installResult.installedFiles || [],
2470
3196
  plan: installResult.plan,
3197
+ sessionId: installResult.sessionId,
3198
+ sessionPath: installResult.sessionPath,
3199
+ verification: installResult.verification,
2471
3200
  });
2472
3201
  if (!json) success(stage ? `Staged ${installResult.plan.files.length} file(s)` : `Updated ${(installResult.installedFiles || []).length} file(s)`);
2473
3202
  } catch (e) {
@@ -2526,6 +3255,8 @@ async function cmdInstalled() {
2526
3255
  installMode: record.installMode || record.install_mode,
2527
3256
  installedAt: record.installedAt || record.installed_at,
2528
3257
  contentHash: record.contentHash || record.content_hash || '',
3258
+ sessionId: record.sessionId || record.session_id,
3259
+ sessionPath: record.sessionPath || record.session_path,
2529
3260
  risks: record.risks || [],
2530
3261
  files,
2531
3262
  status: files.some(file => !file.exists) ? 'missing-files' : files.some(file => file.changed) ? 'local-changes' : 'installed',
@@ -2549,6 +3280,281 @@ async function cmdInstalled() {
2549
3280
  }
2550
3281
  }
2551
3282
 
3283
+ function isCodexManagedPath(filePath) {
3284
+ const resolved = path.resolve(expandHomePath(filePath));
3285
+ return ensureInside(CODEX_SKILLS_DIR, resolved) || ensureInside(path.join(CODEX_TOKREPO_DIR, 'staged'), resolved);
3286
+ }
3287
+
3288
+ function removeEmptyCodexDirs(startDir) {
3289
+ const roots = [CODEX_SKILLS_DIR, path.join(CODEX_TOKREPO_DIR, 'staged')].map(root => path.resolve(root));
3290
+ let dir = path.resolve(startDir);
3291
+ const root = roots.find(candidate => dir === candidate || dir.startsWith(candidate + path.sep));
3292
+ if (!root) return;
3293
+ while (dir !== root && dir.startsWith(root + path.sep)) {
3294
+ try {
3295
+ if (!fs.existsSync(dir) || fs.readdirSync(dir).length > 0) break;
3296
+ fs.rmdirSync(dir);
3297
+ } catch {
3298
+ break;
3299
+ }
3300
+ dir = path.dirname(dir);
3301
+ }
3302
+ }
3303
+
3304
+ function findCodexManifestRecord(selector) {
3305
+ const manifest = readCodexManifest();
3306
+ const records = (manifest.installs || []).filter(item => (item.targetTool || item.target_tool) === 'codex');
3307
+ const needle = String(selector || '').trim();
3308
+ if (!needle) return null;
3309
+ const lower = needle.toLowerCase();
3310
+ const exact = records.find(record => String(record.uuid || '').toLowerCase() === lower);
3311
+ if (exact) return exact;
3312
+
3313
+ const prefixMatches = /^[a-f0-9-]{8,}$/i.test(needle)
3314
+ ? records.filter(record => String(record.uuid || '').toLowerCase().startsWith(lower))
3315
+ : [];
3316
+ if (prefixMatches.length === 1) return prefixMatches[0];
3317
+ if (prefixMatches.length > 1) throw new Error(`Multiple installed assets match "${selector}". Use the full UUID.`);
3318
+
3319
+ const slugNeedle = slugify(needle, '');
3320
+ const titleMatches = records.filter(record => {
3321
+ const title = String(record.title || '').toLowerCase();
3322
+ const sourceUrl = String(record.sourceUrl || record.source_url || '').toLowerCase();
3323
+ return title === lower || slugify(record.title || '', '') === slugNeedle || sourceUrl.includes(lower);
3324
+ });
3325
+ if (titleMatches.length === 1) return titleMatches[0];
3326
+ if (titleMatches.length > 1) throw new Error(`Multiple installed assets match "${selector}". Use the UUID.`);
3327
+
3328
+ const fuzzy = records.filter(record => String(record.title || '').toLowerCase().includes(lower));
3329
+ if (fuzzy.length === 1) return fuzzy[0];
3330
+ if (fuzzy.length > 1) throw new Error(`Multiple installed assets match "${selector}". Use the UUID.`);
3331
+ return null;
3332
+ }
3333
+
3334
+ function buildCodexRemovalPlan(record, files, opts = {}) {
3335
+ const actions = (files || []).map(file => {
3336
+ const filePath = path.resolve(expandHomePath(file.path));
3337
+ const exists = fs.existsSync(filePath);
3338
+ const actualSha = exists ? currentFileSha(filePath) : '';
3339
+ const expectedSha = file.sha256 || '';
3340
+ const changed = Boolean(exists && expectedSha && actualSha !== expectedSha);
3341
+ const managed = isCodexManagedPath(filePath);
3342
+ const allowed = managed && (!changed || opts.force);
3343
+ const reason = !managed ? 'outside-managed-roots'
3344
+ : changed && !opts.force ? 'local-changes'
3345
+ : exists ? 'remove'
3346
+ : 'already-missing';
3347
+ return {
3348
+ type: 'remove_file',
3349
+ path: filePath,
3350
+ sourceName: file.sourceName || file.source_name,
3351
+ expectedSha,
3352
+ actualSha,
3353
+ exists,
3354
+ changed,
3355
+ allowed,
3356
+ reason,
3357
+ };
3358
+ });
3359
+ return {
3360
+ schemaVersion: 1,
3361
+ operation: opts.operation || 'uninstall',
3362
+ targetTool: 'codex',
3363
+ uuid: record.uuid,
3364
+ title: record.title,
3365
+ sourceUrl: record.sourceUrl || record.source_url,
3366
+ manifestPath: CODEX_MANIFEST_FILE,
3367
+ force: Boolean(opts.force),
3368
+ dryRun: Boolean(opts.dryRun),
3369
+ requiresConfirmation: actions.some(action => !action.allowed),
3370
+ actions,
3371
+ };
3372
+ }
3373
+
3374
+ function executeCodexRemovalPlan(plan, opts = {}) {
3375
+ const blocked = plan.actions.filter(action => !action.allowed);
3376
+ if (blocked.length > 0) {
3377
+ const first = blocked[0];
3378
+ throw new Error(`Refusing to remove ${first.path}: ${first.reason}. Use --force only if you want to remove local changes.`);
3379
+ }
3380
+
3381
+ const removedFiles = [];
3382
+ const skippedFiles = [];
3383
+ for (const action of plan.actions) {
3384
+ if (!action.exists) {
3385
+ skippedFiles.push({ path: action.path, reason: 'already-missing' });
3386
+ continue;
3387
+ }
3388
+ fs.unlinkSync(action.path);
3389
+ removedFiles.push({ path: action.path, sha256: action.actualSha || action.expectedSha });
3390
+ removeEmptyCodexDirs(path.dirname(action.path));
3391
+ }
3392
+
3393
+ const session = writeCodexSession({
3394
+ operation: plan.operation,
3395
+ status: plan.operation === 'rollback' ? 'rolled_back' : 'uninstalled',
3396
+ targetTool: 'codex',
3397
+ uuid: plan.uuid,
3398
+ title: plan.title,
3399
+ sourceUrl: plan.sourceUrl,
3400
+ plan,
3401
+ result: { removedFiles, skippedFiles },
3402
+ });
3403
+
3404
+ if (opts.removeManifest !== false && plan.uuid) {
3405
+ const manifest = readCodexManifest();
3406
+ manifest.installs = (manifest.installs || []).filter(item => !((item.targetTool || item.target_tool) === 'codex' && item.uuid === plan.uuid));
3407
+ writeCodexManifest(manifest);
3408
+ }
3409
+
3410
+ return { dryRun: false, plan, removedFiles, skippedFiles, ...session };
3411
+ }
3412
+
3413
+ async function cmdUninstall() {
3414
+ const args = parseArgs(process.argv);
3415
+ const target = args.positional[0];
3416
+ if (!target) {
3417
+ showUninstallHelp();
3418
+ process.exit(1);
3419
+ }
3420
+ const targetTool = validateInstallTarget(args.flags.target || 'codex');
3421
+ if (targetTool !== 'codex') error(`uninstall currently supports --target codex only`);
3422
+
3423
+ const json = Boolean(args.flags.json);
3424
+ const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
3425
+ const force = Boolean(args.flags.force);
3426
+ if (!json) log(`\n${C.bold}tokrepo uninstall${C.reset}\n`);
3427
+
3428
+ try {
3429
+ const record = findCodexManifestRecord(target);
3430
+ if (!record) error(`No installed Codex asset found for "${target}". Run: tokrepo installed --target codex`);
3431
+ const files = record.installedFiles || record.installed_files || [];
3432
+ const plan = buildCodexRemovalPlan(record, files, { operation: 'uninstall', dryRun, force });
3433
+ if (dryRun) {
3434
+ const session = writeCodexSession({
3435
+ operation: 'uninstall',
3436
+ status: 'dry_run',
3437
+ targetTool: 'codex',
3438
+ uuid: record.uuid,
3439
+ title: record.title,
3440
+ sourceUrl: record.sourceUrl || record.source_url,
3441
+ plan,
3442
+ result: { dryRun: true },
3443
+ });
3444
+ const response = { dryRun: true, plan, removedFiles: [], ...session };
3445
+ if (json) outputJson(response);
3446
+ else {
3447
+ info(`Dry run: ${plan.actions.length} file(s) would be removed`);
3448
+ for (const action of plan.actions) {
3449
+ const rel = path.relative(os.homedir(), action.path);
3450
+ log(` ${action.allowed ? C.dim : C.yellow}•${C.reset} ~/${rel} ${C.dim}${action.reason}${C.reset}`);
3451
+ }
3452
+ log(` ${C.dim}Session: ${session.sessionPath}${C.reset}`);
3453
+ }
3454
+ return;
3455
+ }
3456
+
3457
+ const result = executeCodexRemovalPlan(plan, { force });
3458
+ if (json) outputJson(result);
3459
+ else {
3460
+ for (const file of result.removedFiles) {
3461
+ success(`Removed: ~/${path.relative(os.homedir(), file.path)}`);
3462
+ }
3463
+ success(`Uninstalled ${record.title || record.uuid}`);
3464
+ log(` ${C.dim}Manifest: ${CODEX_MANIFEST_FILE}${C.reset}`);
3465
+ log(` ${C.dim}Session: ${result.sessionPath}${C.reset}\n`);
3466
+ }
3467
+ } catch (e) {
3468
+ error(`Uninstall failed: ${e.message}`);
3469
+ }
3470
+ }
3471
+
3472
+ function findRollbackSession(selector) {
3473
+ const sessions = readCodexSessions();
3474
+ if (selector === 'last') {
3475
+ return [...sessions].reverse().find(session => (
3476
+ session.operation === 'install'
3477
+ && ['installed', 'staged', 'stage_only'].includes(session.status)
3478
+ && (session.installedFiles?.length || session.result?.stagePath || session.plan?.rollback?.length)
3479
+ ));
3480
+ }
3481
+ const needle = String(selector || '').trim();
3482
+ if (!needle) return null;
3483
+ return sessions.find(session => session.sessionId === needle || String(session.sessionId || '').startsWith(needle));
3484
+ }
3485
+
3486
+ function filesFromRollbackSession(session) {
3487
+ if (!session) return [];
3488
+ if (session.status === 'staged' && session.result?.stagePath) {
3489
+ return [{ path: session.result.stagePath, sha256: currentFileSha(session.result.stagePath), sourceName: 'install-plan.json' }];
3490
+ }
3491
+ if (Array.isArray(session.installedFiles) && session.installedFiles.length > 0) return session.installedFiles;
3492
+ return (session.plan?.rollback || [])
3493
+ .filter(action => action.type === 'remove_file' && action.path)
3494
+ .map(action => ({ path: action.path, sha256: action.sha256 || '', sourceName: path.basename(action.path) }));
3495
+ }
3496
+
3497
+ async function cmdRollback() {
3498
+ const args = parseArgs(process.argv);
3499
+ const selector = args.flags.last ? 'last' : (args.flags.session || args.positional[0]);
3500
+ if (!selector) {
3501
+ showRollbackHelp();
3502
+ process.exit(1);
3503
+ }
3504
+ const targetTool = validateInstallTarget(args.flags.target || 'codex');
3505
+ if (targetTool !== 'codex') error(`rollback currently supports --target codex only`);
3506
+
3507
+ const json = Boolean(args.flags.json);
3508
+ const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
3509
+ const force = Boolean(args.flags.force);
3510
+ if (!json) log(`\n${C.bold}tokrepo rollback${C.reset}\n`);
3511
+
3512
+ try {
3513
+ const session = findRollbackSession(selector);
3514
+ if (!session) error(`No rollback session found for "${selector}". Run: tokrepo installed --target codex --json`);
3515
+ const files = filesFromRollbackSession(session);
3516
+ const plan = buildCodexRemovalPlan(session, files, { operation: 'rollback', dryRun, force });
3517
+ plan.rollbackSessionId = session.sessionId;
3518
+ plan.rollbackSessionPath = session.sessionPath;
3519
+
3520
+ if (dryRun) {
3521
+ const audit = writeCodexSession({
3522
+ operation: 'rollback',
3523
+ status: 'dry_run',
3524
+ targetTool: 'codex',
3525
+ uuid: session.uuid,
3526
+ title: session.title,
3527
+ sourceUrl: session.sourceUrl,
3528
+ plan,
3529
+ result: { dryRun: true },
3530
+ });
3531
+ const response = { dryRun: true, plan, removedFiles: [], ...audit };
3532
+ if (json) outputJson(response);
3533
+ else {
3534
+ info(`Dry run: rollback ${session.sessionId} would remove ${plan.actions.length} file(s)`);
3535
+ for (const action of plan.actions) {
3536
+ const rel = path.relative(os.homedir(), action.path);
3537
+ log(` ${action.allowed ? C.dim : C.yellow}•${C.reset} ~/${rel} ${C.dim}${action.reason}${C.reset}`);
3538
+ }
3539
+ log(` ${C.dim}Session: ${audit.sessionPath}${C.reset}`);
3540
+ }
3541
+ return;
3542
+ }
3543
+
3544
+ const result = executeCodexRemovalPlan(plan, { force, removeManifest: Boolean(session.uuid) });
3545
+ if (json) outputJson(result);
3546
+ else {
3547
+ for (const file of result.removedFiles) {
3548
+ success(`Removed: ~/${path.relative(os.homedir(), file.path)}`);
3549
+ }
3550
+ success(`Rolled back ${session.sessionId}`);
3551
+ log(` ${C.dim}Session: ${result.sessionPath}${C.reset}\n`);
3552
+ }
3553
+ } catch (e) {
3554
+ error(`Rollback failed: ${e.message}`);
3555
+ }
3556
+ }
3557
+
2552
3558
  async function cmdOutdated() {
2553
3559
  const args = parseArgs(process.argv);
2554
3560
  const targetTool = validateInstallTarget(args.flags.target || 'codex');
@@ -2574,7 +3580,8 @@ async function cmdOutdated() {
2574
3580
  for (const record of installed) {
2575
3581
  try {
2576
3582
  const { workflow, contents } = await fetchWorkflowForInstall(record.uuid, config, apiBase);
2577
- const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode });
3583
+ const serverPlan = await fetchServerCodexInstallPlan(record.uuid, config, apiBase);
3584
+ const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode, serverPlan });
2578
3585
  const diff = diffCodexPlanWithLocal(plan, record);
2579
3586
  if (diff.needsUpdate) {
2580
3587
  list.push({
@@ -2743,6 +3750,9 @@ ${C.bold}DISCOVER & INSTALL${C.reset}
2743
3750
  ${C.cyan}installed${C.reset} List installed Codex assets from manifest
2744
3751
  ${C.cyan}outdated${C.reset} Check installed Codex assets for updates
2745
3752
  ${C.cyan}sync-installed${C.reset} Update installed Codex assets from manifest
3753
+ ${C.cyan}uninstall${C.reset} <uuid> Remove a managed Codex install
3754
+ ${C.cyan}rollback${C.reset} --last Roll back the latest Codex install session
3755
+ ${C.cyan}eval-agent${C.reset} Run agent-native contract and lifecycle evals
2746
3756
 
2747
3757
  ${C.bold}PUBLISH${C.reset}
2748
3758
  ${C.cyan}push${C.reset} [files...] Push files/directory (idempotent upsert)
@@ -2768,6 +3778,7 @@ ${C.bold}PUSH OPTIONS${C.reset}
2768
3778
  ${C.cyan}--kind${C.reset} skill Set agent asset_kind
2769
3779
  ${C.cyan}--target${C.reset} codex Add target tool metadata on push
2770
3780
  ${C.cyan}--install-mode${C.reset} bundle Set install_mode metadata
3781
+ ${C.cyan}--metadata-report${C.reset} Print agent metadata quality suggestions without pushing
2771
3782
 
2772
3783
  ${C.bold}INSTALL BEHAVIOR${C.reset}
2773
3784
  Skills → .claude/skills/ (if .claude/ exists)
@@ -2780,7 +3791,7 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
2780
3791
 
2781
3792
  ${C.bold}EXAMPLES${C.reset}
2782
3793
  tokrepo search "mcp server" # Find MCP configs
2783
- tokrepo search video --json # Machine-readable search
3794
+ tokrepo search video --target codex --kind skill --policy allow --json
2784
3795
  tokrepo detail ca000374-f5d8-... --json # Machine-readable detail
2785
3796
  tokrepo plan 91aeb22d-eff0-4310-... # Install plan v2 for agents
2786
3797
  tokrepo install ca000374-f5d8-... # Install by UUID
@@ -2791,7 +3802,11 @@ ${C.bold}EXAMPLES${C.reset}
2791
3802
  tokrepo outdated --target codex --json
2792
3803
  tokrepo update --target codex --all
2793
3804
  tokrepo sync-installed --target codex --dry-run
3805
+ tokrepo uninstall 91aeb22d --target codex --dry-run
3806
+ tokrepo rollback --last --target codex --dry-run
3807
+ tokrepo eval-agent --json
2794
3808
  tokrepo push --private my-rules.md # Save one file privately
3809
+ tokrepo push . --metadata-report --json # Check agent metadata without uploading
2795
3810
  tokrepo push . --kind skill --target codex --install-mode bundle
2796
3811
  tokrepo push --public skill.md # Share one file publicly
2797
3812
  tokrepo push --private . # Push current dir as private
@@ -2818,11 +3833,12 @@ function showSearchHelp() {
2818
3833
  ${C.bold}tokrepo search${C.reset}
2819
3834
 
2820
3835
  USAGE
2821
- tokrepo search <query> [--json] [--all] [--page-size N] [--sort-by views|latest|stars|popular]
3836
+ tokrepo search <query> [--json] [--all] [--target codex] [--kind skill] [--policy allow|confirm|stage_only|deny] [--page-size N] [--sort-by views|latest|stars|popular]
2822
3837
 
2823
3838
  EXAMPLES
2824
3839
  tokrepo search video
2825
3840
  tokrepo search video --json
3841
+ tokrepo search video --target codex --kind skill --policy allow --json
2826
3842
  tokrepo search "mcp server" --json --all
2827
3843
  `);
2828
3844
  }
@@ -2845,7 +3861,7 @@ function showInstallHelp() {
2845
3861
  ${C.bold}tokrepo install${C.reset}
2846
3862
 
2847
3863
  USAGE
2848
- tokrepo install <uuid|url|name|pack/slug> [--target gemini|codex] [--yes] [--dry-run] [--stage] [--approve-mcp] [--json]
3864
+ tokrepo install <uuid|url|name|pack/slug> [--target gemini|codex] [--yes] [--dry-run] [--stage] [--approve-mcp] [--verify-commands] [--json]
2849
3865
 
2850
3866
  TARGETS
2851
3867
  codex Write Codex skills to ~/.codex/skills/<asset-slug>/SKILL.md
@@ -2884,11 +3900,11 @@ function showListHelp() {
2884
3900
  ${C.bold}tokrepo list${C.reset}
2885
3901
 
2886
3902
  USAGE
2887
- tokrepo list [--json] [--all] [--page-size N]
3903
+ tokrepo list [--json] [--all] [--target codex] [--kind skill] [--policy allow] [--page-size N]
2888
3904
 
2889
3905
  EXAMPLES
2890
3906
  tokrepo list
2891
- tokrepo list --json --all
3907
+ tokrepo list --json --all --target codex
2892
3908
  `);
2893
3909
  }
2894
3910
 
@@ -2931,6 +3947,61 @@ EXAMPLES
2931
3947
  `);
2932
3948
  }
2933
3949
 
3950
+ function showUninstallHelp() {
3951
+ log(`
3952
+ ${C.bold}tokrepo uninstall${C.reset}
3953
+
3954
+ USAGE
3955
+ tokrepo uninstall <uuid|uuid-prefix|title> --target codex [--dry-run] [--force] [--json]
3956
+
3957
+ BEHAVIOR
3958
+ Removes only files recorded in ~/.codex/tokrepo/install-manifest.json and only
3959
+ under ~/.codex/skills or ~/.codex/tokrepo/staged. Local changes are blocked
3960
+ unless --force is provided.
3961
+
3962
+ EXAMPLES
3963
+ tokrepo uninstall 91aeb22d --target codex --dry-run --json
3964
+ tokrepo uninstall 91aeb22d-eff0-4310-abc6-811d2394b420 --target codex
3965
+ `);
3966
+ }
3967
+
3968
+ function showRollbackHelp() {
3969
+ log(`
3970
+ ${C.bold}tokrepo rollback${C.reset}
3971
+
3972
+ USAGE
3973
+ tokrepo rollback --last --target codex [--dry-run] [--force] [--json]
3974
+ tokrepo rollback <session-id> --target codex [--dry-run] [--force] [--json]
3975
+
3976
+ BEHAVIOR
3977
+ Replays the rollback section from ~/.codex/tokrepo/sessions/<session-id>.json.
3978
+ Local changes are blocked unless --force is provided.
3979
+
3980
+ EXAMPLES
3981
+ tokrepo rollback --last --target codex --dry-run --json
3982
+ tokrepo rollback install-20260506-120000-abc123 --target codex
3983
+ `);
3984
+ }
3985
+
3986
+ function showEvalAgentHelp() {
3987
+ log(`
3988
+ ${C.bold}tokrepo eval-agent${C.reset}
3989
+
3990
+ USAGE
3991
+ tokrepo eval-agent [--json] [--uuid <asset-uuid>] [--keyword video] [--keep-temp]
3992
+
3993
+ BEHAVIOR
3994
+ Runs agent-native smoke evals against search filters, install-plan contracts,
3995
+ metadata quality reporting, Codex install verification, manifest state, and rollback.
3996
+ Lifecycle tests use a temporary HOME and do not touch your real ~/.codex.
3997
+
3998
+ EXAMPLES
3999
+ tokrepo eval-agent
4000
+ tokrepo eval-agent --json
4001
+ tokrepo eval-agent --uuid 91aeb22d-eff0-4310-abc6-811d2394b420 --keyword video --json
4002
+ `);
4003
+ }
4004
+
2934
4005
  function showCommandHelp(command) {
2935
4006
  switch (command) {
2936
4007
  case 'search':
@@ -2952,6 +4023,15 @@ function showCommandHelp(command) {
2952
4023
  case 'installed':
2953
4024
  case 'outdated':
2954
4025
  showSyncInstalledHelp(); break;
4026
+ case 'uninstall':
4027
+ case 'remove':
4028
+ case 'rm':
4029
+ showUninstallHelp(); break;
4030
+ case 'rollback':
4031
+ showRollbackHelp(); break;
4032
+ case 'eval-agent':
4033
+ case 'eval':
4034
+ showEvalAgentHelp(); break;
2955
4035
  default:
2956
4036
  showHelp(); break;
2957
4037
  }
@@ -2982,6 +4062,9 @@ async function main() {
2982
4062
  case 'delete': await cmdDelete(); break;
2983
4063
  case 'clone': await cmdClone(); break;
2984
4064
  case 'installed': await cmdInstalled(); break;
4065
+ case 'uninstall': case 'remove': case 'rm': await cmdUninstall(); break;
4066
+ case 'rollback': await cmdRollback(); break;
4067
+ case 'eval-agent': case 'eval': await cmdEvalAgent(); break;
2985
4068
  case 'outdated': await cmdOutdated(); break;
2986
4069
  case 'sync-installed': case 'sync': await cmdSyncInstalled(); break;
2987
4070
  case 'tags': await cmdTags(); break;