tokrepo 3.6.0 → 3.7.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 +770 -27
  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.7.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 ───
@@ -304,6 +305,7 @@ function parseArgs(argv) {
304
305
  'title', 'desc', 'tag', 'target', 'targets', 'keyword', 'types',
305
306
  'kind', 'install-mode', 'install_mode', 'entrypoint', 'asset-kind', 'asset_kind',
306
307
  'version',
308
+ 'policy', 'session',
307
309
  'page', 'page-size', 'page_size', 'sort-by', 'sort_by',
308
310
  'time-window', 'time_window',
309
311
  ]);
@@ -841,8 +843,23 @@ async function cmdSearch() {
841
843
  data = { ...data, list };
842
844
  }
843
845
 
846
+ const originalCount = (data.list || []).length;
847
+ data = { ...data, list: applyAgentWorkflowFilters(data.list || [], args.flags) };
848
+ const filters = {
849
+ target: args.flags.target || undefined,
850
+ kind: args.flags.kind || args.flags.assetKind || undefined,
851
+ policy: args.flags.policy || undefined,
852
+ };
853
+
844
854
  if (args.flags.json) {
845
- outputJson({ query, total: data.total || 0, count: (data.list || []).length, list: data.list || [] });
855
+ outputJson({
856
+ query,
857
+ total: data.total || 0,
858
+ fetched: originalCount,
859
+ count: (data.list || []).length,
860
+ filters,
861
+ list: data.list || [],
862
+ });
846
863
  return;
847
864
  }
848
865
 
@@ -859,7 +876,8 @@ async function cmdSearch() {
859
876
  return;
860
877
  }
861
878
 
862
- log(` ${C.bold}${data.total}${C.reset} results:\n`);
879
+ const filterText = [filters.target ? `target=${filters.target}` : '', filters.kind ? `kind=${filters.kind}` : '', filters.policy ? `policy=${filters.policy}` : ''].filter(Boolean).join(' · ');
880
+ 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
881
 
864
882
  for (let i = 0; i < data.list.length; i++) {
865
883
  const wf = data.list[i];
@@ -874,6 +892,10 @@ async function cmdSearch() {
874
892
  log(` ${C.dim}${String(i + 1).padStart(2)}.${C.reset} ${C.bold}${wf.title}${C.reset}`);
875
893
  if (desc) log(` ${desc}`);
876
894
  if (tags) log(` ${C.cyan}${tags}${C.reset} ${C.dim}★${votes} 👁${views}${C.reset}`);
895
+ if (wf.compatibility?.codex) {
896
+ const c = wf.compatibility.codex;
897
+ log(` ${C.dim}codex: ${c.status} · policy=${c.policyDecision.decision} · kind=${c.assetKind || 'unknown'}${C.reset}`);
898
+ }
877
899
  log(` ${C.dim}tokrepo install ${wf.uuid}${C.reset}`);
878
900
  log('');
879
901
  }
@@ -984,6 +1006,40 @@ function getWorkflowAssetType(workflow) {
984
1006
  return (workflow.tags[0].slug || workflow.tags[0].name || '').toLowerCase();
985
1007
  }
986
1008
 
1009
+ function workflowAgentMetadata(workflow) {
1010
+ return workflow?.agent_metadata || workflow?.agentMetadata || {};
1011
+ }
1012
+
1013
+ function normalizeCodexInstallMode(mode) {
1014
+ const normalized = String(mode || '').trim().toLowerCase().replace(/-/g, '_');
1015
+ return ['single', 'bundle', 'split', 'stage_only'].includes(normalized) ? normalized : '';
1016
+ }
1017
+
1018
+ function workflowAssetKind(workflow) {
1019
+ const metadata = workflowAgentMetadata(workflow);
1020
+ const explicit = workflow?.asset_kind || workflow?.assetKind || metadata.asset_kind || metadata.assetKind || '';
1021
+ if (explicit) return normalizeToolName(explicit);
1022
+ const assetType = getWorkflowAssetType(workflow);
1023
+ const aliases = {
1024
+ skills: 'skill',
1025
+ prompts: 'prompt',
1026
+ knowledge: 'knowledge',
1027
+ 'mcp-configs': 'mcp_config',
1028
+ mcp: 'mcp_config',
1029
+ scripts: 'script',
1030
+ configs: 'config',
1031
+ tools: 'cli_tool',
1032
+ };
1033
+ return aliases[assetType] || normalizeToolName(assetType);
1034
+ }
1035
+
1036
+ function workflowTargetTools(workflow) {
1037
+ const metadata = workflowAgentMetadata(workflow);
1038
+ return parseCsvList(workflow?.target_tools || workflow?.targetTools || metadata.target_tools || metadata.targetTools)
1039
+ .map(normalizeToolName)
1040
+ .filter(Boolean);
1041
+ }
1042
+
987
1043
  function extractInstallableContents(workflow, assetType) {
988
1044
  const contents = [];
989
1045
  const files = workflow.files || [];
@@ -1129,8 +1185,7 @@ function explicitInstallMode(workflow) {
1129
1185
  workflow?.metadata?.installMode,
1130
1186
  workflow?.metadata?.install_mode,
1131
1187
  ].filter(Boolean);
1132
- const mode = String(candidates[0] || '').toLowerCase();
1133
- return ['single', 'bundle', 'split', 'stage_only'].includes(mode) ? mode : '';
1188
+ return normalizeCodexInstallMode(candidates[0]);
1134
1189
  }
1135
1190
 
1136
1191
  function inferCodexInstallMode(workflow, contents) {
@@ -1187,8 +1242,12 @@ function addPlanFile(plan, destPath, content, sourceName, type) {
1187
1242
  }
1188
1243
 
1189
1244
  function buildCodexInstallPlan(workflow, contents, opts = {}) {
1190
- const installMode = opts.installMode || inferCodexInstallMode(workflow, contents);
1191
- const agentMetadata = workflow?.agent_metadata || workflow?.agentMetadata || {};
1245
+ const serverPlan = opts.serverPlan || null;
1246
+ const serverMetadata = serverPlan?.metadata || serverPlan?.agentMetadata || serverPlan?.agent_metadata || {};
1247
+ const installMode = normalizeCodexInstallMode(opts.installMode)
1248
+ || normalizeCodexInstallMode(metadataValue(serverPlan, 'install_mode', 'installMode', ''))
1249
+ || inferCodexInstallMode(workflow, contents);
1250
+ const agentMetadata = Object.keys(serverMetadata || {}).length > 0 ? serverMetadata : workflowAgentMetadata(workflow);
1192
1251
  const plan = {
1193
1252
  uuid: workflow.uuid,
1194
1253
  title: workflow.title,
@@ -1199,7 +1258,8 @@ function buildCodexInstallPlan(workflow, contents, opts = {}) {
1199
1258
  files: [],
1200
1259
  risks: [],
1201
1260
  agentMetadata,
1202
- contentHash: workflow.content_hash || workflow.contentHash || agentMetadata.content_hash || '',
1261
+ contentHash: workflow.content_hash || workflow.contentHash || agentMetadata.content_hash || agentMetadata.contentHash || metadataValue(serverPlan, 'content_hash', 'contentHash', ''),
1262
+ serverPlan,
1203
1263
  };
1204
1264
 
1205
1265
  if (installMode === 'stage_only') {
@@ -1313,7 +1373,36 @@ function mergedPlanRiskProfile(plan) {
1313
1373
  };
1314
1374
  }
1315
1375
 
1376
+ function policyDecisionFromServerPlan(plan) {
1377
+ const serverPlan = plan?.serverPlan;
1378
+ if (!serverPlan) return null;
1379
+ const raw = metadataValue(serverPlan, 'policy_decision', 'policyDecision', null);
1380
+ if (!raw) return null;
1381
+ if (typeof raw === 'string') {
1382
+ return {
1383
+ decision: raw,
1384
+ requiresConfirmation: raw === 'confirm',
1385
+ reasons: [],
1386
+ };
1387
+ }
1388
+ const decision = String(raw.decision || raw.action || 'allow').trim().toLowerCase();
1389
+ const requiresConfirmation = Boolean(
1390
+ raw.requires_confirmation
1391
+ || raw.requiresConfirmation
1392
+ || metadataValue(serverPlan, 'requires_confirmation', 'requiresConfirmation', false)
1393
+ );
1394
+ const reasons = raw.reasons || raw.reason || [];
1395
+ return {
1396
+ decision,
1397
+ requiresConfirmation,
1398
+ reasons: Array.isArray(reasons) ? reasons : [String(reasons)].filter(Boolean),
1399
+ };
1400
+ }
1401
+
1316
1402
  function decideCodexPolicy(plan) {
1403
+ const serverPolicy = policyDecisionFromServerPlan(plan);
1404
+ if (serverPolicy) return serverPolicy;
1405
+
1317
1406
  const metadata = plan.agentMetadata || {};
1318
1407
  const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
1319
1408
  const assetKind = normalizeToolName(metadataValue(metadata, 'asset_kind', 'assetKind', ''));
@@ -1367,6 +1456,9 @@ function decideCodexPolicy(plan) {
1367
1456
  }
1368
1457
 
1369
1458
  function buildPublicPlanActions(plan) {
1459
+ const serverActions = metadataValue(plan.serverPlan, 'actions', 'actions', null);
1460
+ if (serverConcretePlanMatchesLocal(plan) && Array.isArray(serverActions) && serverActions.length > 0) return serverActions;
1461
+
1370
1462
  const stage = plan.installMode === 'stage_only';
1371
1463
  return plan.files.map(file => ({
1372
1464
  type: stage ? 'stage_file' : 'write_file',
@@ -1380,7 +1472,23 @@ function buildPublicPlanActions(plan) {
1380
1472
  }));
1381
1473
  }
1382
1474
 
1475
+ function serverConcretePlanMatchesLocal(plan) {
1476
+ const serverActions = metadataValue(plan.serverPlan, 'actions', 'actions', null);
1477
+ if (!Array.isArray(serverActions) || serverActions.length !== (plan.files || []).length) return false;
1478
+ return serverActions.every((action, index) => {
1479
+ const file = plan.files[index];
1480
+ if (!file) return false;
1481
+ const serverPath = path.resolve(expandHomePath(action.path || ''));
1482
+ const localPath = path.resolve(file.path || '');
1483
+ const serverSha = action.sha256 || action.sha || '';
1484
+ return serverPath === localPath && (!serverSha || serverSha === file.sha256);
1485
+ });
1486
+ }
1487
+
1383
1488
  function buildPublicPlanPreconditions(plan, policyDecision) {
1489
+ const serverPreconditions = metadataValue(plan.serverPlan, 'preconditions', 'preconditions', null);
1490
+ if (Array.isArray(serverPreconditions) && serverPreconditions.length > 0) return serverPreconditions;
1491
+
1384
1492
  const metadata = plan.agentMetadata || {};
1385
1493
  const targetTools = metadataValue(metadata, 'target_tools', 'targetTools', []) || [];
1386
1494
  const out = [
@@ -1405,6 +1513,9 @@ function buildPublicPlanPreconditions(plan, policyDecision) {
1405
1513
  }
1406
1514
 
1407
1515
  function buildPublicPlanRollback(plan) {
1516
+ const serverRollback = metadataValue(plan.serverPlan, 'rollback', 'rollback', null);
1517
+ if (serverConcretePlanMatchesLocal(plan) && Array.isArray(serverRollback) && serverRollback.length > 0) return serverRollback;
1518
+
1408
1519
  const seen = new Set();
1409
1520
  const rollback = [];
1410
1521
  for (const file of plan.files) {
@@ -1416,11 +1527,18 @@ function buildPublicPlanRollback(plan) {
1416
1527
  }
1417
1528
 
1418
1529
  function buildPublicPlanPostVerify(plan) {
1530
+ const serverPostVerify = metadataValue(plan.serverPlan, 'post_verify', 'postVerify', null);
1531
+ if (serverConcretePlanMatchesLocal(plan) && Array.isArray(serverPostVerify) && serverPostVerify.length > 0) return serverPostVerify;
1532
+
1419
1533
  const metadata = plan.agentMetadata || {};
1420
1534
  const verification = metadataValue(metadata, 'verification', 'verification', {}) || {};
1421
1535
  const out = plan.files.map(file => ({ type: 'file_sha256', path: file.path, sha256: file.sha256 }));
1536
+ const installedPaths = new Set(plan.files.map(file => path.resolve(file.path)));
1422
1537
  for (const expected of (verification.expected_files || verification.expectedFiles || [])) {
1423
- out.push({ type: 'expected_file', path: expected });
1538
+ const resolvedExpected = path.resolve(resolveVerifyPath(expected, { baseDir: plan.baseDir, files: plan.files }));
1539
+ if (installedPaths.has(resolvedExpected)) {
1540
+ out.push({ type: 'expected_file', path: expected });
1541
+ }
1424
1542
  }
1425
1543
  for (const command of (verification.commands || [])) {
1426
1544
  out.push({ type: 'command', command });
@@ -1431,8 +1549,11 @@ function buildPublicPlanPostVerify(plan) {
1431
1549
  function publicInstallPlan(plan) {
1432
1550
  const policyDecision = decideCodexPolicy(plan);
1433
1551
  const actions = buildPublicPlanActions(plan);
1552
+ const schemaVersion = Number(metadataValue(plan.serverPlan, 'schema_version', 'schemaVersion', 2)) || 2;
1434
1553
  return {
1435
- schemaVersion: 2,
1554
+ schemaVersion,
1555
+ sourceOfTruth: plan.serverPlan ? 'api_install_plan_v2' : 'local_fallback',
1556
+ concretePlanSource: serverConcretePlanMatchesLocal(plan) ? 'api_install_plan_v2' : 'local_fallback',
1436
1557
  uuid: plan.uuid,
1437
1558
  title: plan.title,
1438
1559
  sourceUrl: plan.sourceUrl,
@@ -1460,6 +1581,95 @@ function publicInstallPlan(plan) {
1460
1581
  };
1461
1582
  }
1462
1583
 
1584
+ function workflowCodexCompatibility(workflow) {
1585
+ const metadata = workflowAgentMetadata(workflow);
1586
+ const assetKind = workflowAssetKind(workflow);
1587
+ const targetTools = workflowTargetTools(workflow);
1588
+ const installMode = normalizeCodexInstallMode(metadata.install_mode || metadata.installMode || workflow.install_mode || workflow.installMode) || 'single';
1589
+ const policy = decideCodexPolicy({
1590
+ agentMetadata: {
1591
+ ...metadata,
1592
+ asset_kind: assetKind,
1593
+ target_tools: targetTools,
1594
+ install_mode: installMode,
1595
+ },
1596
+ risks: [],
1597
+ installMode,
1598
+ });
1599
+ const scores = { allow: 100, confirm: 70, stage_only: 40, deny: 0 };
1600
+ const statuses = {
1601
+ allow: 'native',
1602
+ confirm: 'requires_confirmation',
1603
+ stage_only: 'stage_only',
1604
+ deny: 'denied',
1605
+ };
1606
+ return {
1607
+ targetTool: 'codex',
1608
+ status: statuses[policy.decision] || 'unknown',
1609
+ score: scores[policy.decision] ?? 50,
1610
+ assetKind,
1611
+ targetTools,
1612
+ installMode,
1613
+ policyDecision: policy,
1614
+ };
1615
+ }
1616
+
1617
+ function workflowMatchesAgentFilters(workflow, flags = {}) {
1618
+ const target = normalizeInstallTarget(flags.target || '');
1619
+ const requestedKinds = parseCsvList(flags.kind || flags.assetKind || flags.asset_kind).map(normalizeToolName);
1620
+ const requestedPolicies = parseCsvList(flags.policy).map(s => String(s).trim().toLowerCase());
1621
+ const assetKind = workflowAssetKind(workflow);
1622
+ const targetTools = workflowTargetTools(workflow);
1623
+ const compatibility = workflowCodexCompatibility(workflow);
1624
+
1625
+ if (target === 'codex') {
1626
+ if (targetTools.length > 0 && !targetTools.includes('codex')) return false;
1627
+ } else if (target && targetTools.length > 0 && !targetTools.includes(target)) {
1628
+ return false;
1629
+ }
1630
+
1631
+ if (requestedKinds.length > 0) {
1632
+ const kindAliases = new Set([assetKind, `${assetKind}s`, assetKind.replace(/_/g, '-')]);
1633
+ const tags = (workflow.tags || []).flatMap(t => [t.slug, t.name]).filter(Boolean).map(normalizeToolName);
1634
+ const matchesKind = requestedKinds.some(kind => kindAliases.has(kind) || tags.includes(kind) || tags.includes(`${kind}s`));
1635
+ if (!matchesKind) return false;
1636
+ }
1637
+
1638
+ if (requestedPolicies.length > 0) {
1639
+ const decision = compatibility.policyDecision.decision;
1640
+ const aliases = {
1641
+ safe: 'allow',
1642
+ staged: 'stage_only',
1643
+ stage: 'stage_only',
1644
+ block: 'deny',
1645
+ blocked: 'deny',
1646
+ };
1647
+ const normalizedPolicies = requestedPolicies.map(policy => aliases[policy] || policy);
1648
+ if (!normalizedPolicies.includes(decision)) return false;
1649
+ }
1650
+
1651
+ return true;
1652
+ }
1653
+
1654
+ function enrichWorkflowForAgent(workflow) {
1655
+ const compatibility = workflowCodexCompatibility(workflow);
1656
+ return {
1657
+ ...workflow,
1658
+ assetKind: compatibility.assetKind,
1659
+ targetTools: compatibility.targetTools,
1660
+ compatibility: {
1661
+ codex: compatibility,
1662
+ },
1663
+ policyDecision: compatibility.policyDecision,
1664
+ };
1665
+ }
1666
+
1667
+ function applyAgentWorkflowFilters(list, flags = {}) {
1668
+ const shouldEnrich = flags.target || flags.kind || flags.assetKind || flags.asset_kind || flags.policy;
1669
+ const filtered = (list || []).filter(item => workflowMatchesAgentFilters(item, flags));
1670
+ return shouldEnrich ? filtered.map(enrichWorkflowForAgent) : filtered;
1671
+ }
1672
+
1463
1673
  function hasCodexInstallRisks(plan) {
1464
1674
  const decision = decideCodexPolicy(plan).decision;
1465
1675
  return decision === 'confirm' || decision === 'stage_only' || decision === 'deny';
@@ -1538,6 +1748,102 @@ function executeStageOnlyCodexPlan(plan) {
1538
1748
  return { dryRun: true, staged: true, stageOnly: true, stagePath, plan: publicInstallPlan(plan), installedFiles };
1539
1749
  }
1540
1750
 
1751
+ function expandHomePath(input) {
1752
+ const value = String(input || '');
1753
+ if (value === '~') return os.homedir();
1754
+ if (value.startsWith('~/')) return path.join(os.homedir(), value.slice(2));
1755
+ return value;
1756
+ }
1757
+
1758
+ function resolveVerifyPath(checkPath, publicPlan) {
1759
+ const expanded = expandHomePath(checkPath);
1760
+ if (path.isAbsolute(expanded)) return expanded;
1761
+ const baseDir = publicPlan.baseDir || path.dirname(publicPlan.files?.[0]?.path || CODEX_SKILLS_DIR);
1762
+ return path.join(baseDir, expanded);
1763
+ }
1764
+
1765
+ function runCodexPostVerify(publicPlan, opts = {}) {
1766
+ const checks = [];
1767
+ let ok = true;
1768
+ for (const check of (publicPlan.postVerify || [])) {
1769
+ if (check.type === 'file_sha256') {
1770
+ const filePath = resolveVerifyPath(check.path, publicPlan);
1771
+ const exists = fs.existsSync(filePath);
1772
+ const actualSha = exists ? currentFileSha(filePath) : '';
1773
+ const passed = Boolean(exists && actualSha === check.sha256);
1774
+ if (!passed) ok = false;
1775
+ checks.push({ ...check, path: filePath, status: passed ? 'pass' : 'fail', actualSha });
1776
+ } else if (check.type === 'expected_file') {
1777
+ const filePath = resolveVerifyPath(check.path, publicPlan);
1778
+ const passed = fs.existsSync(filePath);
1779
+ if (!passed) ok = false;
1780
+ checks.push({ ...check, path: filePath, status: passed ? 'pass' : 'fail' });
1781
+ } else if (check.type === 'command') {
1782
+ if (!opts.verifyCommands) {
1783
+ checks.push({ ...check, status: 'skipped', message: 'command verification is opt-in; re-run with --verify-commands' });
1784
+ continue;
1785
+ }
1786
+ try {
1787
+ const childProcess = require('child_process');
1788
+ childProcess.execSync(String(check.command || ''), { stdio: 'pipe', shell: true, timeout: 30000 });
1789
+ checks.push({ ...check, status: 'pass' });
1790
+ } catch (e) {
1791
+ ok = false;
1792
+ checks.push({ ...check, status: 'fail', message: e.message });
1793
+ }
1794
+ } else {
1795
+ checks.push({ ...check, status: 'skipped', message: 'unknown verification check type' });
1796
+ }
1797
+ }
1798
+ return { ok, checks };
1799
+ }
1800
+
1801
+ function createCodexSessionId(operation = 'session') {
1802
+ const stamp = new Date().toISOString().replace(/[-:.]/g, '').replace('T', '-').replace('Z', '');
1803
+ const random = crypto.randomBytes(4).toString('hex');
1804
+ return `${slugify(operation, 'session')}-${stamp}-${random}`;
1805
+ }
1806
+
1807
+ function writeCodexSession(record) {
1808
+ if (!fs.existsSync(CODEX_SESSIONS_DIR)) {
1809
+ fs.mkdirSync(CODEX_SESSIONS_DIR, { recursive: true, mode: 0o700 });
1810
+ }
1811
+ const sessionId = record.sessionId || createCodexSessionId(record.operation || 'session');
1812
+ const sessionPath = path.join(CODEX_SESSIONS_DIR, `${sessionId}.json`);
1813
+ const payload = {
1814
+ schemaVersion: 1,
1815
+ sessionId,
1816
+ createdAt: new Date().toISOString(),
1817
+ cliVersion: CLI_VERSION,
1818
+ argv: process.argv.slice(2),
1819
+ ...record,
1820
+ sessionId,
1821
+ };
1822
+ fs.writeFileSync(sessionPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
1823
+ return { sessionId, sessionPath };
1824
+ }
1825
+
1826
+ function readCodexSessions() {
1827
+ try {
1828
+ if (!fs.existsSync(CODEX_SESSIONS_DIR)) return [];
1829
+ return fs.readdirSync(CODEX_SESSIONS_DIR)
1830
+ .filter(name => name.endsWith('.json'))
1831
+ .map(name => {
1832
+ const sessionPath = path.join(CODEX_SESSIONS_DIR, name);
1833
+ try {
1834
+ const parsed = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
1835
+ return { ...parsed, sessionPath };
1836
+ } catch {
1837
+ return null;
1838
+ }
1839
+ })
1840
+ .filter(Boolean)
1841
+ .sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
1842
+ } catch {
1843
+ return [];
1844
+ }
1845
+ }
1846
+
1541
1847
  function readCodexManifest() {
1542
1848
  try {
1543
1849
  const parsed = JSON.parse(fs.readFileSync(CODEX_MANIFEST_FILE, 'utf8'));
@@ -1546,7 +1852,15 @@ function readCodexManifest() {
1546
1852
  return { schemaVersion: 1, installs: [] };
1547
1853
  }
1548
1854
 
1549
- function writeCodexManifestRecord(plan, installedFiles) {
1855
+ function writeCodexManifest(manifest) {
1856
+ if (!fs.existsSync(CODEX_TOKREPO_DIR)) {
1857
+ fs.mkdirSync(CODEX_TOKREPO_DIR, { recursive: true, mode: 0o700 });
1858
+ }
1859
+ manifest.updatedAt = new Date().toISOString();
1860
+ fs.writeFileSync(CODEX_MANIFEST_FILE, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
1861
+ }
1862
+
1863
+ function writeCodexManifestRecord(plan, installedFiles, sessionInfo = {}, verification = null) {
1550
1864
  if (!fs.existsSync(CODEX_TOKREPO_DIR)) {
1551
1865
  fs.mkdirSync(CODEX_TOKREPO_DIR, { recursive: true, mode: 0o700 });
1552
1866
  }
@@ -1561,6 +1875,9 @@ function writeCodexManifestRecord(plan, installedFiles) {
1561
1875
  installedAt,
1562
1876
  contentHash: plan.contentHash || '',
1563
1877
  agentMetadata: plan.agentMetadata || {},
1878
+ sessionId: sessionInfo.sessionId,
1879
+ sessionPath: sessionInfo.sessionPath,
1880
+ verification,
1564
1881
  installedFiles: installedFiles.map(file => ({
1565
1882
  path: file.path,
1566
1883
  sourceName: file.sourceName,
@@ -1573,16 +1890,58 @@ function writeCodexManifestRecord(plan, installedFiles) {
1573
1890
  manifest.installs = manifest.installs.filter(item => !(item.uuid === plan.uuid && item.targetTool === 'codex'));
1574
1891
  manifest.installs.push(record);
1575
1892
  manifest.updatedAt = installedAt;
1576
- fs.writeFileSync(CODEX_MANIFEST_FILE, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
1893
+ writeCodexManifest(manifest);
1577
1894
  return record;
1578
1895
  }
1579
1896
 
1580
1897
  function executeCodexInstallPlan(plan, opts = {}) {
1581
- if (opts.dryRun) return { dryRun: true, plan: publicInstallPlan(plan), installedFiles: [] };
1582
- if (plan.installMode === 'stage_only') return executeStageOnlyCodexPlan(plan);
1898
+ const publicPlan = publicInstallPlan(plan);
1899
+ if (opts.dryRun) {
1900
+ const session = writeCodexSession({
1901
+ operation: 'install',
1902
+ status: 'dry_run',
1903
+ targetTool: 'codex',
1904
+ uuid: plan.uuid,
1905
+ title: plan.title,
1906
+ sourceUrl: plan.sourceUrl,
1907
+ policyDecision: publicPlan.policyDecision,
1908
+ plan: publicPlan,
1909
+ result: { dryRun: true, installedFiles: [] },
1910
+ });
1911
+ return { dryRun: true, plan: publicPlan, installedFiles: [], ...session };
1912
+ }
1913
+ if (plan.installMode === 'stage_only') {
1914
+ const result = executeStageOnlyCodexPlan(plan);
1915
+ const verification = runCodexPostVerify(result.plan, opts);
1916
+ const session = writeCodexSession({
1917
+ operation: 'install',
1918
+ status: 'stage_only',
1919
+ targetTool: 'codex',
1920
+ uuid: plan.uuid,
1921
+ title: plan.title,
1922
+ sourceUrl: plan.sourceUrl,
1923
+ policyDecision: result.plan.policyDecision,
1924
+ plan: result.plan,
1925
+ installedFiles: result.installedFiles,
1926
+ verification,
1927
+ result: { staged: true, stageOnly: true, stagePath: result.stagePath },
1928
+ });
1929
+ return { ...result, verification, ...session };
1930
+ }
1583
1931
  if (opts.stage) {
1584
1932
  const stagePath = stageCodexInstallPlan(plan);
1585
- return { dryRun: true, staged: true, stagePath, plan: publicInstallPlan(plan), installedFiles: [] };
1933
+ const session = writeCodexSession({
1934
+ operation: 'install',
1935
+ status: 'staged',
1936
+ targetTool: 'codex',
1937
+ uuid: plan.uuid,
1938
+ title: plan.title,
1939
+ sourceUrl: plan.sourceUrl,
1940
+ policyDecision: publicPlan.policyDecision,
1941
+ plan: publicPlan,
1942
+ result: { staged: true, stagePath },
1943
+ });
1944
+ return { dryRun: true, staged: true, stagePath, plan: publicPlan, installedFiles: [], ...session };
1586
1945
  }
1587
1946
 
1588
1947
  const installedFiles = [];
@@ -1602,8 +1961,22 @@ function executeCodexInstallPlan(plan, opts = {}) {
1602
1961
  });
1603
1962
  }
1604
1963
 
1605
- const manifestRecord = writeCodexManifestRecord(plan, installedFiles);
1606
- return { dryRun: false, plan: publicInstallPlan(plan), installedFiles, manifestRecord };
1964
+ const verification = runCodexPostVerify(publicPlan, opts);
1965
+ const session = writeCodexSession({
1966
+ operation: 'install',
1967
+ status: 'installed',
1968
+ targetTool: 'codex',
1969
+ uuid: plan.uuid,
1970
+ title: plan.title,
1971
+ sourceUrl: plan.sourceUrl,
1972
+ policyDecision: publicPlan.policyDecision,
1973
+ plan: publicPlan,
1974
+ installedFiles,
1975
+ verification,
1976
+ result: { installedFiles },
1977
+ });
1978
+ const manifestRecord = writeCodexManifestRecord(plan, installedFiles, session, verification);
1979
+ return { dryRun: false, plan: publicPlan, installedFiles, manifestRecord, verification, ...session };
1607
1980
  }
1608
1981
 
1609
1982
  async function installCodexAsset(workflow, contents, opts = {}) {
@@ -1631,6 +2004,7 @@ async function cmdInstall() {
1631
2004
  dryRun: Boolean(args.flags.dryRun || args.flags.dry_run),
1632
2005
  stage: Boolean(args.flags.stage),
1633
2006
  approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
2007
+ verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
1634
2008
  json: Boolean(args.flags.json),
1635
2009
  manifest: Boolean(args.flags.manifest),
1636
2010
  };
@@ -1807,7 +2181,8 @@ async function installOneAsset(target, config, apiBase, opts) {
1807
2181
  if (targetTool === 'codex') {
1808
2182
  let result;
1809
2183
  try {
1810
- result = await installCodexAsset(workflow, contents, opts);
2184
+ const serverPlan = opts.serverPlan !== undefined ? opts.serverPlan : await fetchServerCodexInstallPlan(uuid, config, apiBase);
2185
+ result = await installCodexAsset(workflow, contents, { ...opts, serverPlan });
1811
2186
  } catch (e) {
1812
2187
  die(e.message);
1813
2188
  }
@@ -1821,6 +2196,7 @@ async function installOneAsset(target, config, apiBase, opts) {
1821
2196
  } else {
1822
2197
  info(`No Codex skill files were written. Re-run with --approve-mcp or --yes to install.`);
1823
2198
  }
2199
+ if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
1824
2200
  } else if (opts.dryRun) {
1825
2201
  info(`Dry run: ${plan.files.length} file(s) would be installed to ${CODEX_SKILLS_DIR}`);
1826
2202
  for (const file of plan.files) {
@@ -1828,6 +2204,7 @@ async function installOneAsset(target, config, apiBase, opts) {
1828
2204
  log(` ${C.dim}•${C.reset} ~/${rel}`);
1829
2205
  if (file.riskFlags.length) log(` ${C.yellow}${file.riskFlags.join(', ')}${C.reset}`);
1830
2206
  }
2207
+ if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
1831
2208
  } else {
1832
2209
  for (const file of result.installedFiles) {
1833
2210
  const relPath = path.relative(os.homedir(), file.path);
@@ -1836,6 +2213,8 @@ async function installOneAsset(target, config, apiBase, opts) {
1836
2213
  log('');
1837
2214
  success(`${result.installedFiles.length} file(s) installed from ${C.bold}${workflow.title}${C.reset}`);
1838
2215
  log(` ${C.dim}Manifest: ${CODEX_MANIFEST_FILE}${C.reset}`);
2216
+ if (result.sessionPath) log(` ${C.dim}Session: ${result.sessionPath}${C.reset}`);
2217
+ if (result.verification && !result.verification.ok) log(` ${C.yellow}Verification: failed${C.reset}`);
1839
2218
  log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
1840
2219
  }
1841
2220
  }
@@ -1851,6 +2230,9 @@ async function installOneAsset(target, config, apiBase, opts) {
1851
2230
  installedFiles: result.installedFiles || [],
1852
2231
  plan: result.plan,
1853
2232
  manifestPath: CODEX_MANIFEST_FILE,
2233
+ sessionId: result.sessionId,
2234
+ sessionPath: result.sessionPath,
2235
+ verification: result.verification,
1854
2236
  };
1855
2237
  }
1856
2238
 
@@ -2012,8 +2394,16 @@ async function cmdList() {
2012
2394
  data = { ...data, list };
2013
2395
  }
2014
2396
 
2397
+ const originalCount = (data.list || []).length;
2398
+ data = { ...data, list: applyAgentWorkflowFilters(data.list || [], args.flags) };
2399
+ const filters = {
2400
+ target: args.flags.target || undefined,
2401
+ kind: args.flags.kind || args.flags.assetKind || undefined,
2402
+ policy: args.flags.policy || undefined,
2403
+ };
2404
+
2015
2405
  if (args.flags.json) {
2016
- outputJson({ total: data.total || 0, count: (data.list || []).length, list: data.list || [] });
2406
+ outputJson({ total: data.total || 0, fetched: originalCount, count: (data.list || []).length, filters, list: data.list || [] });
2017
2407
  return;
2018
2408
  }
2019
2409
 
@@ -2022,11 +2412,16 @@ async function cmdList() {
2022
2412
  return;
2023
2413
  }
2024
2414
 
2025
- log(` ${C.bold}${data.total}${C.reset} assets:\n`);
2415
+ const filterText = [filters.target ? `target=${filters.target}` : '', filters.kind ? `kind=${filters.kind}` : '', filters.policy ? `policy=${filters.policy}` : ''].filter(Boolean).join(' · ');
2416
+ 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
2417
 
2027
2418
  for (const wf of data.list) {
2028
2419
  const views = wf.view_count || 0;
2029
2420
  log(` ${C.cyan}${wf.uuid.substring(0,8)}${C.reset} ${C.bold}${wf.title}${C.reset}`);
2421
+ if (wf.compatibility?.codex) {
2422
+ const c = wf.compatibility.codex;
2423
+ log(` ${C.dim} codex=${c.status} · policy=${c.policyDecision.decision} · kind=${c.assetKind || 'unknown'}${C.reset}`);
2424
+ }
2030
2425
  log(` ${C.dim} ${views} views · https://tokrepo.com/en/workflows/${wf.uuid}${C.reset}\n`);
2031
2426
  }
2032
2427
  } catch (e) {
@@ -2236,13 +2631,16 @@ async function cmdClone() {
2236
2631
  const assetType = getWorkflowAssetType(workflow);
2237
2632
  const contents = extractInstallableContents(workflow, assetType);
2238
2633
  if (contents.length === 0) throw new Error('No installable content found');
2634
+ const serverPlan = await fetchServerCodexInstallPlan(workflow.uuid, config, apiBase);
2239
2635
  const result = await installCodexAsset(workflow, contents, {
2240
2636
  ...args.flags,
2241
2637
  dryRun,
2242
2638
  stage: Boolean(args.flags.stage),
2243
2639
  approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
2640
+ verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
2244
2641
  json: true,
2245
2642
  throwOnError: true,
2643
+ serverPlan,
2246
2644
  });
2247
2645
  if (!dryRun) installedCount += result.installedFiles.length;
2248
2646
  results.push({
@@ -2255,6 +2653,9 @@ async function cmdClone() {
2255
2653
  files: result.plan.files,
2256
2654
  installedFiles: result.installedFiles || [],
2257
2655
  risks: result.plan.risks,
2656
+ sessionId: result.sessionId,
2657
+ sessionPath: result.sessionPath,
2658
+ verification: result.verification,
2258
2659
  });
2259
2660
  if (!json) {
2260
2661
  const fileCount = (dryRun || args.flags.stage) ? result.plan.files.length : result.installedFiles.length;
@@ -2383,6 +2784,15 @@ async function fetchWorkflowForInstall(uuid, config, apiBase) {
2383
2784
  return { workflow, contents };
2384
2785
  }
2385
2786
 
2787
+ async function fetchServerCodexInstallPlan(uuid, config, apiBase) {
2788
+ try {
2789
+ const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/install-plan?uuid=${encodeURIComponent(uuid)}&target=codex`, null, config?.token, apiBase);
2790
+ return data?.plan || data || null;
2791
+ } catch {
2792
+ return null;
2793
+ }
2794
+ }
2795
+
2386
2796
  async function cmdSyncInstalled() {
2387
2797
  const args = parseArgs(process.argv);
2388
2798
  const targetTool = validateInstallTarget(args.flags.target || 'codex');
@@ -2417,7 +2827,8 @@ async function cmdSyncInstalled() {
2417
2827
 
2418
2828
  try {
2419
2829
  const { workflow, contents } = await fetchWorkflowForInstall(uuid, config, apiBase);
2420
- const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode });
2830
+ const serverPlan = await fetchServerCodexInstallPlan(uuid, config, apiBase);
2831
+ const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode, serverPlan });
2421
2832
  const diff = diffCodexPlanWithLocal(plan, record);
2422
2833
  const shouldWrite = force || diff.needsUpdate;
2423
2834
 
@@ -2455,8 +2866,10 @@ async function cmdSyncInstalled() {
2455
2866
  stage,
2456
2867
  installMode: record.installMode || record.install_mode,
2457
2868
  approveMcp: Boolean(args.flags.approveMcp || args.flags.approve_mcp),
2869
+ verifyCommands: Boolean(args.flags.verify_commands || args.flags.verifyCommands),
2458
2870
  json: true,
2459
2871
  throwOnError: true,
2872
+ serverPlan,
2460
2873
  });
2461
2874
 
2462
2875
  results.push({
@@ -2468,6 +2881,9 @@ async function cmdSyncInstalled() {
2468
2881
  stagePath: installResult.stagePath,
2469
2882
  installedFiles: installResult.installedFiles || [],
2470
2883
  plan: installResult.plan,
2884
+ sessionId: installResult.sessionId,
2885
+ sessionPath: installResult.sessionPath,
2886
+ verification: installResult.verification,
2471
2887
  });
2472
2888
  if (!json) success(stage ? `Staged ${installResult.plan.files.length} file(s)` : `Updated ${(installResult.installedFiles || []).length} file(s)`);
2473
2889
  } catch (e) {
@@ -2526,6 +2942,8 @@ async function cmdInstalled() {
2526
2942
  installMode: record.installMode || record.install_mode,
2527
2943
  installedAt: record.installedAt || record.installed_at,
2528
2944
  contentHash: record.contentHash || record.content_hash || '',
2945
+ sessionId: record.sessionId || record.session_id,
2946
+ sessionPath: record.sessionPath || record.session_path,
2529
2947
  risks: record.risks || [],
2530
2948
  files,
2531
2949
  status: files.some(file => !file.exists) ? 'missing-files' : files.some(file => file.changed) ? 'local-changes' : 'installed',
@@ -2549,6 +2967,281 @@ async function cmdInstalled() {
2549
2967
  }
2550
2968
  }
2551
2969
 
2970
+ function isCodexManagedPath(filePath) {
2971
+ const resolved = path.resolve(expandHomePath(filePath));
2972
+ return ensureInside(CODEX_SKILLS_DIR, resolved) || ensureInside(path.join(CODEX_TOKREPO_DIR, 'staged'), resolved);
2973
+ }
2974
+
2975
+ function removeEmptyCodexDirs(startDir) {
2976
+ const roots = [CODEX_SKILLS_DIR, path.join(CODEX_TOKREPO_DIR, 'staged')].map(root => path.resolve(root));
2977
+ let dir = path.resolve(startDir);
2978
+ const root = roots.find(candidate => dir === candidate || dir.startsWith(candidate + path.sep));
2979
+ if (!root) return;
2980
+ while (dir !== root && dir.startsWith(root + path.sep)) {
2981
+ try {
2982
+ if (!fs.existsSync(dir) || fs.readdirSync(dir).length > 0) break;
2983
+ fs.rmdirSync(dir);
2984
+ } catch {
2985
+ break;
2986
+ }
2987
+ dir = path.dirname(dir);
2988
+ }
2989
+ }
2990
+
2991
+ function findCodexManifestRecord(selector) {
2992
+ const manifest = readCodexManifest();
2993
+ const records = (manifest.installs || []).filter(item => (item.targetTool || item.target_tool) === 'codex');
2994
+ const needle = String(selector || '').trim();
2995
+ if (!needle) return null;
2996
+ const lower = needle.toLowerCase();
2997
+ const exact = records.find(record => String(record.uuid || '').toLowerCase() === lower);
2998
+ if (exact) return exact;
2999
+
3000
+ const prefixMatches = /^[a-f0-9-]{8,}$/i.test(needle)
3001
+ ? records.filter(record => String(record.uuid || '').toLowerCase().startsWith(lower))
3002
+ : [];
3003
+ if (prefixMatches.length === 1) return prefixMatches[0];
3004
+ if (prefixMatches.length > 1) throw new Error(`Multiple installed assets match "${selector}". Use the full UUID.`);
3005
+
3006
+ const slugNeedle = slugify(needle, '');
3007
+ const titleMatches = records.filter(record => {
3008
+ const title = String(record.title || '').toLowerCase();
3009
+ const sourceUrl = String(record.sourceUrl || record.source_url || '').toLowerCase();
3010
+ return title === lower || slugify(record.title || '', '') === slugNeedle || sourceUrl.includes(lower);
3011
+ });
3012
+ if (titleMatches.length === 1) return titleMatches[0];
3013
+ if (titleMatches.length > 1) throw new Error(`Multiple installed assets match "${selector}". Use the UUID.`);
3014
+
3015
+ const fuzzy = records.filter(record => String(record.title || '').toLowerCase().includes(lower));
3016
+ if (fuzzy.length === 1) return fuzzy[0];
3017
+ if (fuzzy.length > 1) throw new Error(`Multiple installed assets match "${selector}". Use the UUID.`);
3018
+ return null;
3019
+ }
3020
+
3021
+ function buildCodexRemovalPlan(record, files, opts = {}) {
3022
+ const actions = (files || []).map(file => {
3023
+ const filePath = path.resolve(expandHomePath(file.path));
3024
+ const exists = fs.existsSync(filePath);
3025
+ const actualSha = exists ? currentFileSha(filePath) : '';
3026
+ const expectedSha = file.sha256 || '';
3027
+ const changed = Boolean(exists && expectedSha && actualSha !== expectedSha);
3028
+ const managed = isCodexManagedPath(filePath);
3029
+ const allowed = managed && (!changed || opts.force);
3030
+ const reason = !managed ? 'outside-managed-roots'
3031
+ : changed && !opts.force ? 'local-changes'
3032
+ : exists ? 'remove'
3033
+ : 'already-missing';
3034
+ return {
3035
+ type: 'remove_file',
3036
+ path: filePath,
3037
+ sourceName: file.sourceName || file.source_name,
3038
+ expectedSha,
3039
+ actualSha,
3040
+ exists,
3041
+ changed,
3042
+ allowed,
3043
+ reason,
3044
+ };
3045
+ });
3046
+ return {
3047
+ schemaVersion: 1,
3048
+ operation: opts.operation || 'uninstall',
3049
+ targetTool: 'codex',
3050
+ uuid: record.uuid,
3051
+ title: record.title,
3052
+ sourceUrl: record.sourceUrl || record.source_url,
3053
+ manifestPath: CODEX_MANIFEST_FILE,
3054
+ force: Boolean(opts.force),
3055
+ dryRun: Boolean(opts.dryRun),
3056
+ requiresConfirmation: actions.some(action => !action.allowed),
3057
+ actions,
3058
+ };
3059
+ }
3060
+
3061
+ function executeCodexRemovalPlan(plan, opts = {}) {
3062
+ const blocked = plan.actions.filter(action => !action.allowed);
3063
+ if (blocked.length > 0) {
3064
+ const first = blocked[0];
3065
+ throw new Error(`Refusing to remove ${first.path}: ${first.reason}. Use --force only if you want to remove local changes.`);
3066
+ }
3067
+
3068
+ const removedFiles = [];
3069
+ const skippedFiles = [];
3070
+ for (const action of plan.actions) {
3071
+ if (!action.exists) {
3072
+ skippedFiles.push({ path: action.path, reason: 'already-missing' });
3073
+ continue;
3074
+ }
3075
+ fs.unlinkSync(action.path);
3076
+ removedFiles.push({ path: action.path, sha256: action.actualSha || action.expectedSha });
3077
+ removeEmptyCodexDirs(path.dirname(action.path));
3078
+ }
3079
+
3080
+ const session = writeCodexSession({
3081
+ operation: plan.operation,
3082
+ status: plan.operation === 'rollback' ? 'rolled_back' : 'uninstalled',
3083
+ targetTool: 'codex',
3084
+ uuid: plan.uuid,
3085
+ title: plan.title,
3086
+ sourceUrl: plan.sourceUrl,
3087
+ plan,
3088
+ result: { removedFiles, skippedFiles },
3089
+ });
3090
+
3091
+ if (opts.removeManifest !== false && plan.uuid) {
3092
+ const manifest = readCodexManifest();
3093
+ manifest.installs = (manifest.installs || []).filter(item => !((item.targetTool || item.target_tool) === 'codex' && item.uuid === plan.uuid));
3094
+ writeCodexManifest(manifest);
3095
+ }
3096
+
3097
+ return { dryRun: false, plan, removedFiles, skippedFiles, ...session };
3098
+ }
3099
+
3100
+ async function cmdUninstall() {
3101
+ const args = parseArgs(process.argv);
3102
+ const target = args.positional[0];
3103
+ if (!target) {
3104
+ showUninstallHelp();
3105
+ process.exit(1);
3106
+ }
3107
+ const targetTool = validateInstallTarget(args.flags.target || 'codex');
3108
+ if (targetTool !== 'codex') error(`uninstall currently supports --target codex only`);
3109
+
3110
+ const json = Boolean(args.flags.json);
3111
+ const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
3112
+ const force = Boolean(args.flags.force);
3113
+ if (!json) log(`\n${C.bold}tokrepo uninstall${C.reset}\n`);
3114
+
3115
+ try {
3116
+ const record = findCodexManifestRecord(target);
3117
+ if (!record) error(`No installed Codex asset found for "${target}". Run: tokrepo installed --target codex`);
3118
+ const files = record.installedFiles || record.installed_files || [];
3119
+ const plan = buildCodexRemovalPlan(record, files, { operation: 'uninstall', dryRun, force });
3120
+ if (dryRun) {
3121
+ const session = writeCodexSession({
3122
+ operation: 'uninstall',
3123
+ status: 'dry_run',
3124
+ targetTool: 'codex',
3125
+ uuid: record.uuid,
3126
+ title: record.title,
3127
+ sourceUrl: record.sourceUrl || record.source_url,
3128
+ plan,
3129
+ result: { dryRun: true },
3130
+ });
3131
+ const response = { dryRun: true, plan, removedFiles: [], ...session };
3132
+ if (json) outputJson(response);
3133
+ else {
3134
+ info(`Dry run: ${plan.actions.length} file(s) would be removed`);
3135
+ for (const action of plan.actions) {
3136
+ const rel = path.relative(os.homedir(), action.path);
3137
+ log(` ${action.allowed ? C.dim : C.yellow}•${C.reset} ~/${rel} ${C.dim}${action.reason}${C.reset}`);
3138
+ }
3139
+ log(` ${C.dim}Session: ${session.sessionPath}${C.reset}`);
3140
+ }
3141
+ return;
3142
+ }
3143
+
3144
+ const result = executeCodexRemovalPlan(plan, { force });
3145
+ if (json) outputJson(result);
3146
+ else {
3147
+ for (const file of result.removedFiles) {
3148
+ success(`Removed: ~/${path.relative(os.homedir(), file.path)}`);
3149
+ }
3150
+ success(`Uninstalled ${record.title || record.uuid}`);
3151
+ log(` ${C.dim}Manifest: ${CODEX_MANIFEST_FILE}${C.reset}`);
3152
+ log(` ${C.dim}Session: ${result.sessionPath}${C.reset}\n`);
3153
+ }
3154
+ } catch (e) {
3155
+ error(`Uninstall failed: ${e.message}`);
3156
+ }
3157
+ }
3158
+
3159
+ function findRollbackSession(selector) {
3160
+ const sessions = readCodexSessions();
3161
+ if (selector === 'last') {
3162
+ return [...sessions].reverse().find(session => (
3163
+ session.operation === 'install'
3164
+ && ['installed', 'staged', 'stage_only'].includes(session.status)
3165
+ && (session.installedFiles?.length || session.result?.stagePath || session.plan?.rollback?.length)
3166
+ ));
3167
+ }
3168
+ const needle = String(selector || '').trim();
3169
+ if (!needle) return null;
3170
+ return sessions.find(session => session.sessionId === needle || String(session.sessionId || '').startsWith(needle));
3171
+ }
3172
+
3173
+ function filesFromRollbackSession(session) {
3174
+ if (!session) return [];
3175
+ if (session.status === 'staged' && session.result?.stagePath) {
3176
+ return [{ path: session.result.stagePath, sha256: currentFileSha(session.result.stagePath), sourceName: 'install-plan.json' }];
3177
+ }
3178
+ if (Array.isArray(session.installedFiles) && session.installedFiles.length > 0) return session.installedFiles;
3179
+ return (session.plan?.rollback || [])
3180
+ .filter(action => action.type === 'remove_file' && action.path)
3181
+ .map(action => ({ path: action.path, sha256: action.sha256 || '', sourceName: path.basename(action.path) }));
3182
+ }
3183
+
3184
+ async function cmdRollback() {
3185
+ const args = parseArgs(process.argv);
3186
+ const selector = args.flags.last ? 'last' : (args.flags.session || args.positional[0]);
3187
+ if (!selector) {
3188
+ showRollbackHelp();
3189
+ process.exit(1);
3190
+ }
3191
+ const targetTool = validateInstallTarget(args.flags.target || 'codex');
3192
+ if (targetTool !== 'codex') error(`rollback currently supports --target codex only`);
3193
+
3194
+ const json = Boolean(args.flags.json);
3195
+ const dryRun = Boolean(args.flags.dryRun || args.flags.dry_run);
3196
+ const force = Boolean(args.flags.force);
3197
+ if (!json) log(`\n${C.bold}tokrepo rollback${C.reset}\n`);
3198
+
3199
+ try {
3200
+ const session = findRollbackSession(selector);
3201
+ if (!session) error(`No rollback session found for "${selector}". Run: tokrepo installed --target codex --json`);
3202
+ const files = filesFromRollbackSession(session);
3203
+ const plan = buildCodexRemovalPlan(session, files, { operation: 'rollback', dryRun, force });
3204
+ plan.rollbackSessionId = session.sessionId;
3205
+ plan.rollbackSessionPath = session.sessionPath;
3206
+
3207
+ if (dryRun) {
3208
+ const audit = writeCodexSession({
3209
+ operation: 'rollback',
3210
+ status: 'dry_run',
3211
+ targetTool: 'codex',
3212
+ uuid: session.uuid,
3213
+ title: session.title,
3214
+ sourceUrl: session.sourceUrl,
3215
+ plan,
3216
+ result: { dryRun: true },
3217
+ });
3218
+ const response = { dryRun: true, plan, removedFiles: [], ...audit };
3219
+ if (json) outputJson(response);
3220
+ else {
3221
+ info(`Dry run: rollback ${session.sessionId} would remove ${plan.actions.length} file(s)`);
3222
+ for (const action of plan.actions) {
3223
+ const rel = path.relative(os.homedir(), action.path);
3224
+ log(` ${action.allowed ? C.dim : C.yellow}•${C.reset} ~/${rel} ${C.dim}${action.reason}${C.reset}`);
3225
+ }
3226
+ log(` ${C.dim}Session: ${audit.sessionPath}${C.reset}`);
3227
+ }
3228
+ return;
3229
+ }
3230
+
3231
+ const result = executeCodexRemovalPlan(plan, { force, removeManifest: Boolean(session.uuid) });
3232
+ if (json) outputJson(result);
3233
+ else {
3234
+ for (const file of result.removedFiles) {
3235
+ success(`Removed: ~/${path.relative(os.homedir(), file.path)}`);
3236
+ }
3237
+ success(`Rolled back ${session.sessionId}`);
3238
+ log(` ${C.dim}Session: ${result.sessionPath}${C.reset}\n`);
3239
+ }
3240
+ } catch (e) {
3241
+ error(`Rollback failed: ${e.message}`);
3242
+ }
3243
+ }
3244
+
2552
3245
  async function cmdOutdated() {
2553
3246
  const args = parseArgs(process.argv);
2554
3247
  const targetTool = validateInstallTarget(args.flags.target || 'codex');
@@ -2574,7 +3267,8 @@ async function cmdOutdated() {
2574
3267
  for (const record of installed) {
2575
3268
  try {
2576
3269
  const { workflow, contents } = await fetchWorkflowForInstall(record.uuid, config, apiBase);
2577
- const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode });
3270
+ const serverPlan = await fetchServerCodexInstallPlan(record.uuid, config, apiBase);
3271
+ const plan = buildCodexInstallPlan(workflow, contents, { installMode: record.installMode || record.install_mode, serverPlan });
2578
3272
  const diff = diffCodexPlanWithLocal(plan, record);
2579
3273
  if (diff.needsUpdate) {
2580
3274
  list.push({
@@ -2743,6 +3437,8 @@ ${C.bold}DISCOVER & INSTALL${C.reset}
2743
3437
  ${C.cyan}installed${C.reset} List installed Codex assets from manifest
2744
3438
  ${C.cyan}outdated${C.reset} Check installed Codex assets for updates
2745
3439
  ${C.cyan}sync-installed${C.reset} Update installed Codex assets from manifest
3440
+ ${C.cyan}uninstall${C.reset} <uuid> Remove a managed Codex install
3441
+ ${C.cyan}rollback${C.reset} --last Roll back the latest Codex install session
2746
3442
 
2747
3443
  ${C.bold}PUBLISH${C.reset}
2748
3444
  ${C.cyan}push${C.reset} [files...] Push files/directory (idempotent upsert)
@@ -2780,7 +3476,7 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
2780
3476
 
2781
3477
  ${C.bold}EXAMPLES${C.reset}
2782
3478
  tokrepo search "mcp server" # Find MCP configs
2783
- tokrepo search video --json # Machine-readable search
3479
+ tokrepo search video --target codex --kind skill --policy allow --json
2784
3480
  tokrepo detail ca000374-f5d8-... --json # Machine-readable detail
2785
3481
  tokrepo plan 91aeb22d-eff0-4310-... # Install plan v2 for agents
2786
3482
  tokrepo install ca000374-f5d8-... # Install by UUID
@@ -2791,6 +3487,8 @@ ${C.bold}EXAMPLES${C.reset}
2791
3487
  tokrepo outdated --target codex --json
2792
3488
  tokrepo update --target codex --all
2793
3489
  tokrepo sync-installed --target codex --dry-run
3490
+ tokrepo uninstall 91aeb22d --target codex --dry-run
3491
+ tokrepo rollback --last --target codex --dry-run
2794
3492
  tokrepo push --private my-rules.md # Save one file privately
2795
3493
  tokrepo push . --kind skill --target codex --install-mode bundle
2796
3494
  tokrepo push --public skill.md # Share one file publicly
@@ -2818,11 +3516,12 @@ function showSearchHelp() {
2818
3516
  ${C.bold}tokrepo search${C.reset}
2819
3517
 
2820
3518
  USAGE
2821
- tokrepo search <query> [--json] [--all] [--page-size N] [--sort-by views|latest|stars|popular]
3519
+ 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
3520
 
2823
3521
  EXAMPLES
2824
3522
  tokrepo search video
2825
3523
  tokrepo search video --json
3524
+ tokrepo search video --target codex --kind skill --policy allow --json
2826
3525
  tokrepo search "mcp server" --json --all
2827
3526
  `);
2828
3527
  }
@@ -2845,7 +3544,7 @@ function showInstallHelp() {
2845
3544
  ${C.bold}tokrepo install${C.reset}
2846
3545
 
2847
3546
  USAGE
2848
- tokrepo install <uuid|url|name|pack/slug> [--target gemini|codex] [--yes] [--dry-run] [--stage] [--approve-mcp] [--json]
3547
+ tokrepo install <uuid|url|name|pack/slug> [--target gemini|codex] [--yes] [--dry-run] [--stage] [--approve-mcp] [--verify-commands] [--json]
2849
3548
 
2850
3549
  TARGETS
2851
3550
  codex Write Codex skills to ~/.codex/skills/<asset-slug>/SKILL.md
@@ -2884,11 +3583,11 @@ function showListHelp() {
2884
3583
  ${C.bold}tokrepo list${C.reset}
2885
3584
 
2886
3585
  USAGE
2887
- tokrepo list [--json] [--all] [--page-size N]
3586
+ tokrepo list [--json] [--all] [--target codex] [--kind skill] [--policy allow] [--page-size N]
2888
3587
 
2889
3588
  EXAMPLES
2890
3589
  tokrepo list
2891
- tokrepo list --json --all
3590
+ tokrepo list --json --all --target codex
2892
3591
  `);
2893
3592
  }
2894
3593
 
@@ -2931,6 +3630,42 @@ EXAMPLES
2931
3630
  `);
2932
3631
  }
2933
3632
 
3633
+ function showUninstallHelp() {
3634
+ log(`
3635
+ ${C.bold}tokrepo uninstall${C.reset}
3636
+
3637
+ USAGE
3638
+ tokrepo uninstall <uuid|uuid-prefix|title> --target codex [--dry-run] [--force] [--json]
3639
+
3640
+ BEHAVIOR
3641
+ Removes only files recorded in ~/.codex/tokrepo/install-manifest.json and only
3642
+ under ~/.codex/skills or ~/.codex/tokrepo/staged. Local changes are blocked
3643
+ unless --force is provided.
3644
+
3645
+ EXAMPLES
3646
+ tokrepo uninstall 91aeb22d --target codex --dry-run --json
3647
+ tokrepo uninstall 91aeb22d-eff0-4310-abc6-811d2394b420 --target codex
3648
+ `);
3649
+ }
3650
+
3651
+ function showRollbackHelp() {
3652
+ log(`
3653
+ ${C.bold}tokrepo rollback${C.reset}
3654
+
3655
+ USAGE
3656
+ tokrepo rollback --last --target codex [--dry-run] [--force] [--json]
3657
+ tokrepo rollback <session-id> --target codex [--dry-run] [--force] [--json]
3658
+
3659
+ BEHAVIOR
3660
+ Replays the rollback section from ~/.codex/tokrepo/sessions/<session-id>.json.
3661
+ Local changes are blocked unless --force is provided.
3662
+
3663
+ EXAMPLES
3664
+ tokrepo rollback --last --target codex --dry-run --json
3665
+ tokrepo rollback install-20260506-120000-abc123 --target codex
3666
+ `);
3667
+ }
3668
+
2934
3669
  function showCommandHelp(command) {
2935
3670
  switch (command) {
2936
3671
  case 'search':
@@ -2952,6 +3687,12 @@ function showCommandHelp(command) {
2952
3687
  case 'installed':
2953
3688
  case 'outdated':
2954
3689
  showSyncInstalledHelp(); break;
3690
+ case 'uninstall':
3691
+ case 'remove':
3692
+ case 'rm':
3693
+ showUninstallHelp(); break;
3694
+ case 'rollback':
3695
+ showRollbackHelp(); break;
2955
3696
  default:
2956
3697
  showHelp(); break;
2957
3698
  }
@@ -2982,6 +3723,8 @@ async function main() {
2982
3723
  case 'delete': await cmdDelete(); break;
2983
3724
  case 'clone': await cmdClone(); break;
2984
3725
  case 'installed': await cmdInstalled(); break;
3726
+ case 'uninstall': case 'remove': case 'rm': await cmdUninstall(); break;
3727
+ case 'rollback': await cmdRollback(); break;
2985
3728
  case 'outdated': await cmdOutdated(); break;
2986
3729
  case 'sync-installed': case 'sync': await cmdSyncInstalled(); break;
2987
3730
  case 'tags': await cmdTags(); break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokrepo",
3
- "version": "3.6.0",
3
+ "version": "3.7.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"