tokrepo 2.0.0 → 3.1.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 +453 -6
  2. package/package.json +2 -2
package/bin/tokrepo.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const crypto = require('crypto');
5
6
  const https = require('https');
6
7
  const http = require('http');
7
8
  const readline = require('readline');
@@ -21,8 +22,11 @@ const C = {
21
22
 
22
23
  const CONFIG_DIR = path.join(require('os').homedir(), '.tokrepo');
23
24
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
25
+ const SYNC_STATE_FILE = path.join(CONFIG_DIR, 'sync-state.json');
24
26
  const PROJECT_CONFIG = '.tokrepo.json';
25
27
  const DEFAULT_API = 'https://api.tokrepo.com';
28
+ const CLI_VERSION = '3.1.0';
29
+ const VERSION_CHECK_FILE = path.join(require('os').homedir(), '.tokrepo', '.version-check');
26
30
 
27
31
  // ─── Helpers ───
28
32
 
@@ -42,9 +46,73 @@ function readConfig() {
42
46
 
43
47
  function writeConfig(config) {
44
48
  if (!fs.existsSync(CONFIG_DIR)) {
45
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
49
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
46
50
  }
47
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
51
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
52
+ }
53
+
54
+ // Check npm registry for newer version (non-blocking, max once per day)
55
+ async function checkForUpdate() {
56
+ try {
57
+ // Only check once per 24 hours
58
+ if (fs.existsSync(VERSION_CHECK_FILE)) {
59
+ const stat = fs.statSync(VERSION_CHECK_FILE);
60
+ const hoursSinceCheck = (Date.now() - stat.mtimeMs) / 3600000;
61
+ if (hoursSinceCheck < 24) return;
62
+ }
63
+ // Touch the file to mark check time
64
+ if (!fs.existsSync(path.dirname(VERSION_CHECK_FILE))) {
65
+ fs.mkdirSync(path.dirname(VERSION_CHECK_FILE), { recursive: true, mode: 0o700 });
66
+ }
67
+ fs.writeFileSync(VERSION_CHECK_FILE, '', { mode: 0o600 });
68
+
69
+ const data = await new Promise((resolve, reject) => {
70
+ const req = https.get('https://registry.npmjs.org/tokrepo/latest', {
71
+ headers: { 'Accept': 'application/json' },
72
+ timeout: 3000,
73
+ }, (res) => {
74
+ let body = '';
75
+ res.on('data', (chunk) => { body += chunk; });
76
+ res.on('end', () => {
77
+ try { resolve(JSON.parse(body)); } catch { reject(); }
78
+ });
79
+ });
80
+ req.on('error', reject);
81
+ req.on('timeout', () => { req.destroy(); reject(); });
82
+ });
83
+
84
+ const latest = data.version;
85
+ if (latest && latest !== CLI_VERSION) {
86
+ const cmp = compareVersions(latest, CLI_VERSION);
87
+ if (cmp > 0) {
88
+ log('');
89
+ log(`${C.yellow}!${C.reset} Update available: ${C.dim}${CLI_VERSION}${C.reset} → ${C.green}${latest}${C.reset}`);
90
+ log(` Run: ${C.cyan}npm install -g tokrepo${C.reset}`);
91
+ log('');
92
+ }
93
+ }
94
+ } catch {
95
+ // Silent fail — update check is best-effort
96
+ }
97
+ }
98
+
99
+ function compareVersions(a, b) {
100
+ const pa = a.split('.').map(Number);
101
+ const pb = b.split('.').map(Number);
102
+ for (let i = 0; i < 3; i++) {
103
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
104
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
105
+ }
106
+ return 0;
107
+ }
108
+
109
+ // Sync state: maps "dirPath:title" → { uuid, hash, lastSync }
110
+ function readSyncState() {
111
+ try { return JSON.parse(fs.readFileSync(SYNC_STATE_FILE, 'utf8')); } catch { return {}; }
112
+ }
113
+ function writeSyncState(state) {
114
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
115
+ fs.writeFileSync(SYNC_STATE_FILE, JSON.stringify(state, null, 2), { mode: 0o600 });
48
116
  }
49
117
 
50
118
  function readProjectConfig(baseDir = process.cwd()) {
@@ -74,6 +142,10 @@ function apiRequest(method, urlPath, body, token, apiBase) {
74
142
  return new Promise((resolve, reject) => {
75
143
  const base = apiBase || DEFAULT_API;
76
144
  const url = new URL(urlPath, base);
145
+ // Force HTTPS to prevent token transmission over plain HTTP
146
+ if (url.protocol === 'http:' && !url.hostname.match(/^(localhost|127\.0\.0\.1)$/)) {
147
+ url.protocol = 'https:';
148
+ }
77
149
  const isHttps = url.protocol === 'https:';
78
150
  const mod = isHttps ? https : http;
79
151
 
@@ -144,7 +216,8 @@ function detectFileType(filename) {
144
216
 
145
217
  // Guess tag from file type
146
218
  function guessTag(fileType) {
147
- const map = { skill: 'Skills', prompt: 'Prompts', script: 'Scripts', config: 'Configs' };
219
+ // Use lowercase singular names to match backend tag slugs
220
+ const map = { skill: 'skill', prompt: 'prompt', script: 'script', config: 'config', mcp: 'mcp' };
148
221
  return map[fileType] || null;
149
222
  }
150
223
 
@@ -253,6 +326,11 @@ function collectFiles(paths, baseDir) {
253
326
 
254
327
  for (const p of paths) {
255
328
  const resolved = path.resolve(baseDir, p);
329
+ // Prevent path traversal — resolved must be within baseDir
330
+ if (!resolved.startsWith(path.resolve(baseDir) + path.sep) && resolved !== path.resolve(baseDir)) {
331
+ warn(`Skipped (outside project): ${p}`);
332
+ continue;
333
+ }
256
334
  if (!fs.existsSync(resolved)) {
257
335
  warn(`Not found: ${p}`);
258
336
  continue;
@@ -674,6 +752,9 @@ Examples:
674
752
  }
675
753
  case 'mcp':
676
754
  case 'mcp configs': {
755
+ // Security warning for MCP configs
756
+ warn('MCP server config detected. Review the configuration carefully before adding to your project.');
757
+ warn('MCP servers can execute arbitrary code. Only install from trusted sources.');
677
758
  // Save as mcp config, hint about manual merge
678
759
  if (!fileName.endsWith('.json')) fileName = fileName.replace(/\.md$/, '.json');
679
760
  break;
@@ -704,6 +785,8 @@ Examples:
704
785
  }
705
786
  }
706
787
 
788
+ // Sanitize fileName to prevent path traversal from API response
789
+ fileName = path.basename(fileName);
707
790
  const destPath = path.join(destDir, fileName);
708
791
 
709
792
  // Don't overwrite without warning
@@ -848,6 +931,351 @@ async function cmdTags() {
848
931
  }
849
932
  }
850
933
 
934
+ // ─── Sync: scan directory, diff with remote, upsert changes ───
935
+
936
+ function computeHash(files) {
937
+ const h = crypto.createHash('sha256');
938
+ for (const f of files) {
939
+ h.update(f.name);
940
+ h.update('\0');
941
+ h.update(f.content);
942
+ h.update('\0');
943
+ }
944
+ return h.digest('hex');
945
+ }
946
+
947
+ function scanDirectory(dirPath) {
948
+ const assets = [];
949
+ const SKIP_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'dist', 'build']);
950
+ const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db', 'package-lock.json', 'yarn.lock']);
951
+
952
+ // Each subdirectory = one asset; loose files = one asset per file
953
+ let entries;
954
+ try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); } catch { return assets; }
955
+
956
+ const looseFiles = [];
957
+
958
+ for (const entry of entries) {
959
+ if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name) || SKIP_FILES.has(entry.name)) continue;
960
+
961
+ const fullPath = path.join(dirPath, entry.name);
962
+
963
+ if (entry.isDirectory()) {
964
+ // Subdirectory = one asset (e.g., ~/.claude/skills/my-skill/)
965
+ const files = collectAssetFiles(fullPath);
966
+ if (files.length === 0) continue;
967
+
968
+ const title = guessAssetTitle(files, entry.name);
969
+ const hash = computeHash(files);
970
+ const detectedTags = new Set();
971
+ for (const f of files) {
972
+ const ft = detectFileType(f.name);
973
+ const tag = guessTag(ft);
974
+ if (tag) detectedTags.add(tag);
975
+ }
976
+
977
+ assets.push({ title, files, hash, tags: Array.from(detectedTags), sourcePath: fullPath });
978
+ } else if (entry.isFile()) {
979
+ // Loose file
980
+ const ext = path.extname(entry.name).toLowerCase();
981
+ const validExts = ['.md', '.sh', '.py', '.js', '.mjs', '.ts', '.json', '.yaml', '.yml', '.toml', '.prompt'];
982
+ if (validExts.includes(ext) || entry.name === '.cursorrules' || entry.name === '.windsurfrules') {
983
+ looseFiles.push({ path: fullPath, name: entry.name });
984
+ }
985
+ }
986
+ }
987
+
988
+ // Each loose file = one asset
989
+ for (const f of looseFiles) {
990
+ let content;
991
+ try { content = fs.readFileSync(f.path, 'utf8'); } catch { continue; }
992
+ if (!content.trim()) continue;
993
+
994
+ const files = [{ name: f.name, content, type: detectFileType(f.name) }];
995
+ const title = guessAssetTitle(files, path.basename(f.name, path.extname(f.name)));
996
+ const hash = computeHash(files);
997
+ const ft = detectFileType(f.name);
998
+ const tag = guessTag(ft);
999
+
1000
+ assets.push({ title, files, hash, tags: tag ? [tag] : [], sourcePath: f.path });
1001
+ }
1002
+
1003
+ return assets;
1004
+ }
1005
+
1006
+ function collectAssetFiles(dirPath) {
1007
+ const files = [];
1008
+ const SKIP = new Set(['.DS_Store', 'node_modules', '.git', '__pycache__']);
1009
+
1010
+ function walk(dir, relBase) {
1011
+ let entries;
1012
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
1013
+ for (const entry of entries) {
1014
+ if (entry.name.startsWith('.') || SKIP.has(entry.name)) continue;
1015
+ const fullPath = path.join(dir, entry.name);
1016
+ const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
1017
+ if (entry.isDirectory()) {
1018
+ walk(fullPath, relPath);
1019
+ } else if (entry.isFile()) {
1020
+ const ext = path.extname(entry.name).toLowerCase();
1021
+ const validExts = ['.md', '.sh', '.py', '.js', '.mjs', '.ts', '.json', '.yaml', '.yml', '.toml', '.prompt', '.rb', '.go', '.rs'];
1022
+ if (validExts.includes(ext) || entry.name === '.cursorrules') {
1023
+ let content;
1024
+ try { content = fs.readFileSync(fullPath, 'utf8'); } catch { continue; }
1025
+ if (!content.trim()) continue;
1026
+ files.push({ name: relPath, content, type: detectFileType(relPath) });
1027
+ }
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ walk(dirPath, '');
1033
+ return files;
1034
+ }
1035
+
1036
+ function guessAssetTitle(files, fallbackName) {
1037
+ // Try to find a heading in the first .md file
1038
+ for (const f of files) {
1039
+ if (f.name.toLowerCase().endsWith('.md')) {
1040
+ const match = f.content.match(/^#\s+(.+)$/m);
1041
+ if (match) return match[1].trim();
1042
+ }
1043
+ }
1044
+ // Clean up fallback name
1045
+ return fallbackName
1046
+ .replace(/[-_]/g, ' ')
1047
+ .replace(/\b\w/g, c => c.toUpperCase());
1048
+ }
1049
+
1050
+ async function cmdSync() {
1051
+ const args = parseArgs(process.argv);
1052
+ const config = readConfig();
1053
+ if (!config || !config.token) error(`Not logged in. Run: ${C.cyan}tokrepo login${C.reset}`);
1054
+
1055
+ const targetDir = args.positional[0]
1056
+ ? path.resolve(args.positional[0])
1057
+ : path.join(require('os').homedir(), '.claude', 'skills');
1058
+
1059
+ if (!fs.existsSync(targetDir)) {
1060
+ error(`Directory not found: ${targetDir}`);
1061
+ }
1062
+
1063
+ const visibility = args.flags.public ? 1 : 0; // default private for sync
1064
+
1065
+ log(`\n${C.bold}tokrepo sync${C.reset}\n`);
1066
+ info(`Scanning ${targetDir}...`);
1067
+
1068
+ const assets = scanDirectory(targetDir);
1069
+ if (assets.length === 0) {
1070
+ info('No assets found in directory.');
1071
+ return;
1072
+ }
1073
+
1074
+ log(` Found ${C.bold}${assets.length}${C.reset} assets\n`);
1075
+
1076
+ // Load local sync state for fast local-only diff
1077
+ const syncState = readSyncState();
1078
+
1079
+ // Classify each asset: check local state first, then remote diff for unknowns
1080
+ const needsRemoteCheck = [];
1081
+ const localStatus = {};
1082
+
1083
+ for (const asset of assets) {
1084
+ const key = asset.sourcePath;
1085
+ const cached = syncState[key];
1086
+ if (cached && cached.hash === asset.hash) {
1087
+ // Local hash matches last sync — unchanged
1088
+ localStatus[asset.title] = { status: 'unchanged', uuid: cached.uuid };
1089
+ } else if (cached && cached.hash !== asset.hash) {
1090
+ // Local hash differs from last sync — updated
1091
+ localStatus[asset.title] = { status: 'updated', uuid: cached.uuid };
1092
+ } else {
1093
+ // Not in local state — check remote
1094
+ needsRemoteCheck.push(asset);
1095
+ }
1096
+ }
1097
+
1098
+ // Remote diff for assets not in local state
1099
+ if (needsRemoteCheck.length > 0) {
1100
+ info('Comparing with remote...');
1101
+ const diffPayload = needsRemoteCheck.map(a => ({ title: a.title, content_hash: a.hash }));
1102
+ try {
1103
+ const data = await apiRequest('POST', '/api/v1/tokenboard/push/diff', { assets: diffPayload }, config.token, config.api);
1104
+ for (const r of data.results) {
1105
+ localStatus[r.title] = { status: r.status, uuid: r.remote_uuid || '' };
1106
+ }
1107
+ } catch (e) {
1108
+ warn(`Diff API: ${e.message} — treating unknowns as new`);
1109
+ for (const a of needsRemoteCheck) {
1110
+ localStatus[a.title] = { status: 'new', uuid: '' };
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ // Show status
1116
+ let newCount = 0, updatedCount = 0, unchangedCount = 0;
1117
+
1118
+ for (const asset of assets) {
1119
+ const st = localStatus[asset.title] || { status: 'new' };
1120
+ if (st.status === 'new') {
1121
+ log(` ${C.green}+ new${C.reset} ${asset.title} ${C.dim}(${asset.files.length} files)${C.reset}`);
1122
+ newCount++;
1123
+ } else if (st.status === 'updated') {
1124
+ log(` ${C.yellow}~ updated${C.reset} ${asset.title}`);
1125
+ updatedCount++;
1126
+ } else {
1127
+ log(` ${C.dim}= unchanged ${asset.title}${C.reset}`);
1128
+ unchangedCount++;
1129
+ }
1130
+ }
1131
+
1132
+ log('');
1133
+ log(` ${C.green}${newCount} new${C.reset} ${C.yellow}${updatedCount} updated${C.reset} ${C.dim}${unchangedCount} unchanged${C.reset}`);
1134
+
1135
+ if (newCount === 0 && updatedCount === 0) {
1136
+ log('');
1137
+ success('Everything is up to date!');
1138
+ return;
1139
+ }
1140
+
1141
+ log('');
1142
+
1143
+ // Confirm unless -y
1144
+ if (!args.flags.yes) {
1145
+ const confirm = await ask(`Push ${newCount + updatedCount} assets? (y/N):`);
1146
+ if (confirm.toLowerCase() !== 'y') { log('Aborted.'); return; }
1147
+ }
1148
+
1149
+ // Upsert each changed asset + save state
1150
+ let successCount = 0;
1151
+ let failCount = 0;
1152
+
1153
+ for (const asset of assets) {
1154
+ const st = localStatus[asset.title] || { status: 'new' };
1155
+ if (st.status === 'unchanged') continue;
1156
+
1157
+ const totalChars = asset.files.reduce((sum, f) => sum + f.content.length, 0);
1158
+
1159
+ try {
1160
+ const data = await apiRequest('POST', '/api/v1/tokenboard/push/upsert', {
1161
+ title: asset.title,
1162
+ files: asset.files,
1163
+ tags: asset.tags,
1164
+ token_cost: String(Math.round(totalChars / 4)),
1165
+ visibility,
1166
+ }, config.token, config.api);
1167
+
1168
+ const action = data.action === 'created' ? C.green + '+ created' : C.yellow + '~ updated';
1169
+ log(` ${action}${C.reset} ${asset.title} ${C.dim}${data.url}${C.reset}`);
1170
+
1171
+ // Save to local sync state
1172
+ syncState[asset.sourcePath] = {
1173
+ uuid: data.uuid,
1174
+ hash: asset.hash,
1175
+ title: asset.title,
1176
+ url: data.url,
1177
+ lastSync: new Date().toISOString(),
1178
+ };
1179
+ successCount++;
1180
+ } catch (e) {
1181
+ log(` ${C.red}✗ failed${C.reset} ${asset.title}: ${e.message}`);
1182
+ failCount++;
1183
+ }
1184
+ }
1185
+
1186
+ // Persist sync state
1187
+ writeSyncState(syncState);
1188
+
1189
+ log('');
1190
+ if (failCount === 0) {
1191
+ success(`Synced ${successCount} assets!`);
1192
+ } else {
1193
+ warn(`${successCount} synced, ${failCount} failed`);
1194
+ }
1195
+ log('');
1196
+ }
1197
+
1198
+ async function cmdStatus() {
1199
+ const args = parseArgs(process.argv);
1200
+ const config = readConfig();
1201
+ if (!config || !config.token) error(`Not logged in. Run: ${C.cyan}tokrepo login${C.reset}`);
1202
+
1203
+ const targetDir = args.positional[0]
1204
+ ? path.resolve(args.positional[0])
1205
+ : path.join(require('os').homedir(), '.claude', 'skills');
1206
+
1207
+ if (!fs.existsSync(targetDir)) {
1208
+ error(`Directory not found: ${targetDir}`);
1209
+ }
1210
+
1211
+ log(`\n${C.bold}tokrepo status${C.reset}\n`);
1212
+ info(`Scanning ${targetDir}...`);
1213
+
1214
+ const assets = scanDirectory(targetDir);
1215
+ if (assets.length === 0) {
1216
+ info('No assets found in directory.');
1217
+ return;
1218
+ }
1219
+
1220
+ // Check local sync state first, remote for unknowns
1221
+ const syncState = readSyncState();
1222
+ const localStatus = {};
1223
+ const needsRemoteCheck = [];
1224
+
1225
+ for (const asset of assets) {
1226
+ const cached = syncState[asset.sourcePath];
1227
+ if (cached && cached.hash === asset.hash) {
1228
+ localStatus[asset.title] = { status: 'unchanged', uuid: cached.uuid };
1229
+ } else if (cached && cached.hash !== asset.hash) {
1230
+ localStatus[asset.title] = { status: 'updated', uuid: cached.uuid };
1231
+ } else {
1232
+ needsRemoteCheck.push(asset);
1233
+ }
1234
+ }
1235
+
1236
+ if (needsRemoteCheck.length > 0) {
1237
+ info('Comparing with remote...');
1238
+ const diffPayload = needsRemoteCheck.map(a => ({ title: a.title, content_hash: a.hash }));
1239
+ try {
1240
+ const data = await apiRequest('POST', '/api/v1/tokenboard/push/diff', { assets: diffPayload }, config.token, config.api);
1241
+ for (const r of data.results) {
1242
+ localStatus[r.title] = { status: r.status, uuid: r.remote_uuid || '' };
1243
+ }
1244
+ } catch (e) {
1245
+ error(`Diff API error: ${e.message}`);
1246
+ }
1247
+ }
1248
+
1249
+ log('');
1250
+ let newCount = 0, updatedCount = 0, unchangedCount = 0;
1251
+
1252
+ for (const asset of assets) {
1253
+ const st = localStatus[asset.title] || { status: 'new', uuid: '' };
1254
+ const status = st.status;
1255
+ const uuid = st.uuid || '';
1256
+ const uuidShort = uuid ? ` ${C.dim}(${uuid.substring(0, 8)})${C.reset}` : '';
1257
+
1258
+ if (status === 'new') {
1259
+ log(` ${C.green}+ new${C.reset} ${asset.title} ${C.dim}(${asset.files.length} files)${C.reset}`);
1260
+ newCount++;
1261
+ } else if (status === 'updated') {
1262
+ log(` ${C.yellow}~ modified${C.reset} ${asset.title}${uuidShort}`);
1263
+ updatedCount++;
1264
+ } else {
1265
+ log(` ${C.dim} unchanged ${asset.title}${uuidShort}${C.reset}`);
1266
+ unchangedCount++;
1267
+ }
1268
+ }
1269
+
1270
+ log('');
1271
+ log(` ${C.bold}${assets.length}${C.reset} local assets: ${C.green}${newCount} new${C.reset} ${C.yellow}${updatedCount} modified${C.reset} ${C.dim}${unchangedCount} synced${C.reset}`);
1272
+
1273
+ if (newCount > 0 || updatedCount > 0) {
1274
+ log(`\n Run ${C.cyan}tokrepo sync ${args.positional[0] || ''}${C.reset} to push changes`);
1275
+ }
1276
+ log('');
1277
+ }
1278
+
851
1279
  function showHelp() {
852
1280
  log(`
853
1281
  ${C.bold}tokrepo${C.reset} — AI assets for humans and agents. Like GitHub, for AI experience.
@@ -865,10 +1293,12 @@ ${C.bold}DISCOVER & INSTALL${C.reset}
865
1293
  ${C.cyan}install${C.reset} <name|uuid> Smart install (auto-detects type & placement)
866
1294
  ${C.cyan}pull${C.reset} <url|uuid> Download raw asset files
867
1295
 
868
- ${C.bold}PUBLISH${C.reset}
869
- ${C.cyan}push${C.reset} [files...] Push files/directory to TokRepo
1296
+ ${C.bold}PUBLISH & SYNC${C.reset}
1297
+ ${C.cyan}push${C.reset} [files...] Push files/directory (creates new asset)
1298
+ ${C.cyan}sync${C.reset} [dir] Sync directory to TokRepo (smart upsert)
1299
+ ${C.cyan}status${C.reset} [dir] Show local vs remote diff
870
1300
  ${C.cyan}init${C.reset} Create .tokrepo.json project config
871
- ${C.cyan}update${C.reset} <uuid> [f] Update existing asset
1301
+ ${C.cyan}update${C.reset} <uuid> [f] Update existing asset by UUID
872
1302
  ${C.cyan}delete${C.reset} <uuid> Delete an asset
873
1303
 
874
1304
  ${C.bold}ACCOUNT${C.reset}
@@ -892,6 +1322,16 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
892
1322
  MCP → current dir (.json)
893
1323
  Prompts → current dir (.md)
894
1324
 
1325
+ ${C.bold}SYNC (the killer feature)${C.reset}
1326
+ ${C.cyan}tokrepo sync ~/.claude/skills/${C.reset} # Sync all skills (default: private)
1327
+ ${C.cyan}tokrepo sync ~/.claude/skills/ --public${C.reset} # Sync as public assets
1328
+ ${C.cyan}tokrepo sync . -y${C.reset} # Sync current dir, skip confirm
1329
+ ${C.cyan}tokrepo status${C.reset} # Show what would change
1330
+
1331
+ Sync scans a directory, detects new/modified assets, and pushes only
1332
+ what changed. Each subdirectory becomes one asset. Loose files become
1333
+ individual assets. Like ${C.bold}git push${C.reset} for your AI assets.
1334
+
895
1335
  ${C.bold}EXAMPLES${C.reset}
896
1336
  tokrepo search "mcp server" # Find MCP configs
897
1337
  tokrepo install ca000374-f5d8-... # Install by UUID
@@ -919,6 +1359,8 @@ async function main() {
919
1359
  case 'login': await cmdLogin(); break;
920
1360
  case 'init': await cmdInit(); break;
921
1361
  case 'push': await cmdPush(); break;
1362
+ case 'sync': await cmdSync(); break;
1363
+ case 'status': case 'st': await cmdStatus(); break;
922
1364
  case 'pull': await cmdPull(); break;
923
1365
  case 'search': case 'find': await cmdSearch(); break;
924
1366
  case 'install': case 'i': await cmdInstall(); break;
@@ -927,11 +1369,16 @@ async function main() {
927
1369
  case 'delete': await cmdDelete(); break;
928
1370
  case 'tags': await cmdTags(); break;
929
1371
  case 'whoami': await cmdWhoami(); break;
1372
+ case '--version': case '-v': case 'version':
1373
+ log(`tokrepo ${CLI_VERSION}`); break;
930
1374
  case 'help': case '--help': case '-h': case undefined:
931
1375
  showHelp(); break;
932
1376
  default:
933
1377
  error(`Unknown command: ${command}. Run: tokrepo help`);
934
1378
  }
1379
+
1380
+ // Non-blocking update check after command completes
1381
+ checkForUpdate();
935
1382
  }
936
1383
 
937
1384
  main().catch((e) => { error(e.message); });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tokrepo",
3
- "version": "2.0.0",
4
- "description": "AI assets for humans and agents — search, install, push. Like GitHub, for AI experience.",
3
+ "version": "3.1.0",
4
+ "description": "AI assets for humans and agents — sync, search, install, push. Like git push for your AI skills.",
5
5
  "bin": {
6
6
  "tokrepo": "bin/tokrepo.js"
7
7
  },