tokrepo 3.2.0 → 3.3.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.
- package/bin/tokrepo.js +319 -390
- package/package.json +3 -3
package/bin/tokrepo.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const crypto = require('crypto');
|
|
6
5
|
const https = require('https');
|
|
7
6
|
const http = require('http');
|
|
8
7
|
const readline = require('readline');
|
|
@@ -22,10 +21,9 @@ const C = {
|
|
|
22
21
|
|
|
23
22
|
const CONFIG_DIR = path.join(require('os').homedir(), '.tokrepo');
|
|
24
23
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
25
|
-
const SYNC_STATE_FILE = path.join(CONFIG_DIR, 'sync-state.json');
|
|
26
24
|
const PROJECT_CONFIG = '.tokrepo.json';
|
|
27
25
|
const DEFAULT_API = 'https://api.tokrepo.com';
|
|
28
|
-
const CLI_VERSION = '3.
|
|
26
|
+
const CLI_VERSION = '3.3.0';
|
|
29
27
|
const VERSION_CHECK_FILE = path.join(require('os').homedir(), '.tokrepo', '.version-check');
|
|
30
28
|
|
|
31
29
|
// ─── Helpers ───
|
|
@@ -37,6 +35,11 @@ function warn(msg) { log(`${C.yellow}!${C.reset} ${msg}`); }
|
|
|
37
35
|
function info(msg) { log(`${C.cyan}→${C.reset} ${msg}`); }
|
|
38
36
|
|
|
39
37
|
function readConfig() {
|
|
38
|
+
// P0: TOKREPO_TOKEN env var takes priority (enables Agent automation)
|
|
39
|
+
const envToken = process.env.TOKREPO_TOKEN;
|
|
40
|
+
if (envToken) {
|
|
41
|
+
return { token: envToken, api: process.env.TOKREPO_API || DEFAULT_API };
|
|
42
|
+
}
|
|
40
43
|
try {
|
|
41
44
|
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
42
45
|
} catch {
|
|
@@ -106,15 +109,6 @@ function compareVersions(a, b) {
|
|
|
106
109
|
return 0;
|
|
107
110
|
}
|
|
108
111
|
|
|
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 });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
112
|
function readProjectConfig(baseDir = process.cwd()) {
|
|
119
113
|
const configPath = path.join(baseDir, PROJECT_CONFIG);
|
|
120
114
|
try {
|
|
@@ -151,7 +145,7 @@ function apiRequest(method, urlPath, body, token, apiBase) {
|
|
|
151
145
|
|
|
152
146
|
const headers = {
|
|
153
147
|
'Content-Type': 'application/json',
|
|
154
|
-
'User-Agent':
|
|
148
|
+
'User-Agent': `tokrepo-cli/${CLI_VERSION}`,
|
|
155
149
|
};
|
|
156
150
|
if (token) {
|
|
157
151
|
headers['Authorization'] = `Bearer ${token}`;
|
|
@@ -216,8 +210,7 @@ function detectFileType(filename) {
|
|
|
216
210
|
|
|
217
211
|
// Guess tag from file type
|
|
218
212
|
function guessTag(fileType) {
|
|
219
|
-
|
|
220
|
-
const map = { skill: 'skill', prompt: 'prompt', script: 'script', config: 'config', mcp: 'mcp' };
|
|
213
|
+
const map = { skill: 'Skills', prompt: 'Prompts', script: 'Scripts', config: 'Configs' };
|
|
221
214
|
return map[fileType] || null;
|
|
222
215
|
}
|
|
223
216
|
|
|
@@ -326,11 +319,6 @@ function collectFiles(paths, baseDir) {
|
|
|
326
319
|
|
|
327
320
|
for (const p of paths) {
|
|
328
321
|
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
|
-
}
|
|
334
322
|
if (!fs.existsSync(resolved)) {
|
|
335
323
|
warn(`Not found: ${p}`);
|
|
336
324
|
continue;
|
|
@@ -388,15 +376,33 @@ function guessTitle(files, baseDir) {
|
|
|
388
376
|
|
|
389
377
|
async function cmdLogin() {
|
|
390
378
|
log(`\n${C.bold}tokrepo login${C.reset}\n`);
|
|
391
|
-
|
|
379
|
+
|
|
380
|
+
// Check for --token flag for manual token entry
|
|
381
|
+
const args = process.argv.slice(2);
|
|
382
|
+
const useToken = args.includes('--token') || args.includes('-t');
|
|
383
|
+
|
|
384
|
+
if (useToken) {
|
|
385
|
+
// Manual token flow
|
|
386
|
+
info('Paste your API token (from https://tokrepo.com/en/my/settings)');
|
|
387
|
+
log('');
|
|
388
|
+
const token = await ask('API Token:');
|
|
389
|
+
if (!token) error('Token is required');
|
|
390
|
+
return await saveAndVerifyToken(token);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Browser OAuth flow (default)
|
|
394
|
+
info('Opening browser for authentication...');
|
|
395
|
+
log(` ${C.dim}(Use ${C.cyan}tokrepo login --token${C.dim} to paste a token manually)${C.reset}`);
|
|
392
396
|
log('');
|
|
393
397
|
|
|
394
|
-
const token = await
|
|
395
|
-
if (!token) error('
|
|
398
|
+
const token = await browserAuthFlow();
|
|
399
|
+
if (!token) error('Authentication failed or was cancelled');
|
|
400
|
+
return await saveAndVerifyToken(token);
|
|
401
|
+
}
|
|
396
402
|
|
|
403
|
+
async function saveAndVerifyToken(token) {
|
|
397
404
|
writeConfig({ token, api: DEFAULT_API });
|
|
398
405
|
success(`Config saved to ${CONFIG_FILE}`);
|
|
399
|
-
|
|
400
406
|
try {
|
|
401
407
|
const config = readConfig();
|
|
402
408
|
const data = await fetchCurrentUser(config);
|
|
@@ -406,6 +412,71 @@ async function cmdLogin() {
|
|
|
406
412
|
}
|
|
407
413
|
}
|
|
408
414
|
|
|
415
|
+
function browserAuthFlow() {
|
|
416
|
+
return new Promise((resolve) => {
|
|
417
|
+
const server = http.createServer((req, res) => {
|
|
418
|
+
// CORS headers for browser fetch
|
|
419
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
420
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
421
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
422
|
+
|
|
423
|
+
if (req.method === 'OPTIONS') {
|
|
424
|
+
res.writeHead(204);
|
|
425
|
+
res.end();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (req.method === 'POST' && req.url === '/callback') {
|
|
430
|
+
let body = '';
|
|
431
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
432
|
+
req.on('end', () => {
|
|
433
|
+
try {
|
|
434
|
+
const data = JSON.parse(body);
|
|
435
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
436
|
+
res.end(JSON.stringify({ ok: true }));
|
|
437
|
+
server.close();
|
|
438
|
+
clearTimeout(timeout);
|
|
439
|
+
resolve(data.token);
|
|
440
|
+
} catch {
|
|
441
|
+
res.writeHead(400);
|
|
442
|
+
res.end('Invalid request');
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
res.writeHead(404);
|
|
449
|
+
res.end('Not found');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Listen on random port
|
|
453
|
+
server.listen(0, '127.0.0.1', () => {
|
|
454
|
+
const port = server.address().port;
|
|
455
|
+
const authUrl = `https://tokrepo.com/en/cli-auth?port=${port}`;
|
|
456
|
+
|
|
457
|
+
info(`Listening on http://127.0.0.1:${port}`);
|
|
458
|
+
log(` ${C.dim}If browser doesn't open, visit:${C.reset}`);
|
|
459
|
+
log(` ${C.cyan}${authUrl}${C.reset}`);
|
|
460
|
+
log('');
|
|
461
|
+
info('Waiting for authorization...');
|
|
462
|
+
|
|
463
|
+
// Open browser
|
|
464
|
+
const opener = process.platform === 'darwin' ? 'open'
|
|
465
|
+
: process.platform === 'win32' ? 'start'
|
|
466
|
+
: 'xdg-open';
|
|
467
|
+
require('child_process').exec(`${opener} "${authUrl}"`);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Timeout after 5 minutes
|
|
471
|
+
const timeout = setTimeout(() => {
|
|
472
|
+
server.close();
|
|
473
|
+
log('');
|
|
474
|
+
warn('Authorization timed out (5 minutes). Try again or use --token flag.');
|
|
475
|
+
resolve(null);
|
|
476
|
+
}, 5 * 60 * 1000);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
409
480
|
async function cmdPush() {
|
|
410
481
|
const args = parseArgs(process.argv);
|
|
411
482
|
|
|
@@ -501,7 +572,7 @@ async function cmdPush() {
|
|
|
501
572
|
info('Pushing...');
|
|
502
573
|
|
|
503
574
|
try {
|
|
504
|
-
const data = await apiRequest('POST', '/api/v1/tokenboard/push/
|
|
575
|
+
const data = await apiRequest('POST', '/api/v1/tokenboard/push/upsert', {
|
|
505
576
|
title,
|
|
506
577
|
description,
|
|
507
578
|
files: pushFiles,
|
|
@@ -511,7 +582,10 @@ async function cmdPush() {
|
|
|
511
582
|
}, config.token, config.api);
|
|
512
583
|
|
|
513
584
|
log('');
|
|
514
|
-
|
|
585
|
+
const actionLabel = data.action === 'created' ? 'Created'
|
|
586
|
+
: data.action === 'updated' ? 'Updated'
|
|
587
|
+
: 'Unchanged (no diff)';
|
|
588
|
+
success(`${actionLabel}!`);
|
|
515
589
|
log(`\n ${C.bold}URL:${C.reset} ${C.cyan}${data.url}${C.reset}`);
|
|
516
590
|
log(` ${C.bold}UUID:${C.reset} ${data.uuid}\n`);
|
|
517
591
|
} catch (e) {
|
|
@@ -555,17 +629,15 @@ async function cmdInit() {
|
|
|
555
629
|
|
|
556
630
|
async function cmdPull() {
|
|
557
631
|
const urlOrUuid = process.argv[3];
|
|
558
|
-
if (!urlOrUuid) error('Usage: tokrepo pull <url
|
|
632
|
+
if (!urlOrUuid) error('Usage: tokrepo pull <url|uuid|@user/name>');
|
|
559
633
|
|
|
560
634
|
log(`\n${C.bold}tokrepo pull${C.reset}\n`);
|
|
561
635
|
|
|
562
|
-
let uuid = urlOrUuid;
|
|
563
|
-
const urlMatch = urlOrUuid.match(/workflows\/([a-f0-9-]+)/);
|
|
564
|
-
if (urlMatch) uuid = urlMatch[1];
|
|
565
|
-
|
|
566
636
|
const config = readConfig();
|
|
567
637
|
const apiBase = config?.api || DEFAULT_API;
|
|
568
638
|
|
|
639
|
+
let uuid = await resolveAssetId(urlOrUuid, config, apiBase);
|
|
640
|
+
|
|
569
641
|
info(`Fetching ${uuid}...`);
|
|
570
642
|
|
|
571
643
|
try {
|
|
@@ -591,6 +663,54 @@ async function cmdPull() {
|
|
|
591
663
|
}
|
|
592
664
|
}
|
|
593
665
|
|
|
666
|
+
// Resolve various input formats to a UUID:
|
|
667
|
+
// - UUID directly: "ca000374-f5d8-..."
|
|
668
|
+
// - Full URL: "https://tokrepo.com/en/workflows/ca000374-f5d8-..."
|
|
669
|
+
// - @username/asset-name: search by author + keyword
|
|
670
|
+
// - Plain name: search by keyword
|
|
671
|
+
async function resolveAssetId(input, config, apiBase) {
|
|
672
|
+
// Already a UUID
|
|
673
|
+
if (/^[a-f0-9-]{36}$/.test(input)) return input;
|
|
674
|
+
|
|
675
|
+
// URL containing UUID
|
|
676
|
+
const urlMatch = input.match(/workflows\/([a-f0-9-]{36})/);
|
|
677
|
+
if (urlMatch) return urlMatch[1];
|
|
678
|
+
|
|
679
|
+
// @username/asset-name format
|
|
680
|
+
const atMatch = input.match(/^@([^/]+)\/(.+)$/);
|
|
681
|
+
if (atMatch) {
|
|
682
|
+
const [, username, assetName] = atMatch;
|
|
683
|
+
info(`Searching for "${assetName}" by @${username}...`);
|
|
684
|
+
// Search by keyword, then filter by author nickname
|
|
685
|
+
const encoded = encodeURIComponent(assetName);
|
|
686
|
+
try {
|
|
687
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?keyword=${encoded}&page=1&page_size=20&sort_by=views`, null, config?.token, apiBase);
|
|
688
|
+
const items = data.list || data.items || [];
|
|
689
|
+
const match = items.find(w => {
|
|
690
|
+
const authorName = (w.author?.nickname || w.nickname || '').toLowerCase();
|
|
691
|
+
return authorName === username.toLowerCase();
|
|
692
|
+
});
|
|
693
|
+
if (match) return match.uuid;
|
|
694
|
+
// Fallback: return first result
|
|
695
|
+
if (items.length > 0) {
|
|
696
|
+
warn(`No exact match for @${username}, using best match: "${items[0].title}"`);
|
|
697
|
+
return items[0].uuid;
|
|
698
|
+
}
|
|
699
|
+
} catch { /* fall through */ }
|
|
700
|
+
error(`Asset not found: ${input}`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Plain name: search by keyword
|
|
704
|
+
info(`Searching for "${input}"...`);
|
|
705
|
+
const encoded = encodeURIComponent(input);
|
|
706
|
+
try {
|
|
707
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?keyword=${encoded}&page=1&page_size=5&sort_by=views`, null, config?.token, apiBase);
|
|
708
|
+
const items = data.list || data.items || [];
|
|
709
|
+
if (items.length > 0) return items[0].uuid;
|
|
710
|
+
} catch { /* fall through */ }
|
|
711
|
+
error(`Asset not found: ${input}`);
|
|
712
|
+
}
|
|
713
|
+
|
|
594
714
|
// ─── Search ───
|
|
595
715
|
|
|
596
716
|
async function cmdSearch() {
|
|
@@ -752,9 +872,6 @@ Examples:
|
|
|
752
872
|
}
|
|
753
873
|
case 'mcp':
|
|
754
874
|
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.');
|
|
758
875
|
// Save as mcp config, hint about manual merge
|
|
759
876
|
if (!fileName.endsWith('.json')) fileName = fileName.replace(/\.md$/, '.json');
|
|
760
877
|
break;
|
|
@@ -785,10 +902,14 @@ Examples:
|
|
|
785
902
|
}
|
|
786
903
|
}
|
|
787
904
|
|
|
788
|
-
// Sanitize fileName to prevent path traversal from API response
|
|
789
|
-
fileName = path.basename(fileName);
|
|
790
905
|
const destPath = path.join(destDir, fileName);
|
|
791
906
|
|
|
907
|
+
// Path traversal guard: ensure resolved path stays inside destDir
|
|
908
|
+
if (!path.resolve(destPath).startsWith(path.resolve(destDir) + path.sep) && path.resolve(destPath) !== path.resolve(destDir)) {
|
|
909
|
+
warn(`Skipping "${fileName}" — path traversal detected`);
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
|
|
792
913
|
// Don't overwrite without warning
|
|
793
914
|
if (fs.existsSync(destPath)) {
|
|
794
915
|
warn(`File exists: ${path.relative(process.cwd(), destPath)} (overwriting)`);
|
|
@@ -913,393 +1034,207 @@ async function cmdDelete() {
|
|
|
913
1034
|
}
|
|
914
1035
|
}
|
|
915
1036
|
|
|
916
|
-
async function
|
|
917
|
-
|
|
1037
|
+
async function cmdClone() {
|
|
1038
|
+
const target = process.argv[3];
|
|
1039
|
+
if (!target) error('Usage: tokrepo clone @username');
|
|
1040
|
+
|
|
1041
|
+
log(`\n${C.bold}tokrepo clone${C.reset}\n`);
|
|
918
1042
|
|
|
919
1043
|
const config = readConfig();
|
|
920
1044
|
const apiBase = config?.api || DEFAULT_API;
|
|
921
1045
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
for (const tag of data.tags) {
|
|
926
|
-
log(` ${C.cyan}${tag.name}${C.reset}${tag.count ? ` ${C.dim}(${tag.count} assets)${C.reset}` : ''}`);
|
|
927
|
-
}
|
|
928
|
-
log('');
|
|
929
|
-
} catch (e) {
|
|
930
|
-
error(`Failed: ${e.message}`);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
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
|
-
}
|
|
1046
|
+
// Extract username from @username format
|
|
1047
|
+
let username = target;
|
|
1048
|
+
if (username.startsWith('@')) username = username.slice(1);
|
|
946
1049
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
'
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', '__pycache__', '.venv', 'dist', 'build']);
|
|
959
|
-
const SKIP_FILES = new Set(['.DS_Store', 'Thumbs.db', 'package-lock.json', 'yarn.lock']);
|
|
960
|
-
|
|
961
|
-
// If the directory itself is a project, treat the whole thing as one asset
|
|
962
|
-
if (isProjectDirectory(dirPath)) {
|
|
963
|
-
const files = collectAssetFiles(dirPath);
|
|
964
|
-
if (files.length > 0) {
|
|
965
|
-
const title = guessAssetTitle(files, path.basename(dirPath));
|
|
966
|
-
const hash = computeHash(files);
|
|
967
|
-
const detectedTags = new Set();
|
|
968
|
-
for (const f of files) {
|
|
969
|
-
const ft = detectFileType(f.name);
|
|
970
|
-
const tag = guessTag(ft);
|
|
971
|
-
if (tag) detectedTags.add(tag);
|
|
1050
|
+
// Step 1: Find user's UUID by searching for their workflows
|
|
1051
|
+
info(`Finding user @${username}...`);
|
|
1052
|
+
let authorUuid = '';
|
|
1053
|
+
try {
|
|
1054
|
+
const searchData = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?keyword=${encodeURIComponent(username)}&page=1&page_size=5&sort_by=latest`, null, config?.token, apiBase);
|
|
1055
|
+
const items = searchData.list || searchData.items || [];
|
|
1056
|
+
for (const item of items) {
|
|
1057
|
+
const authorName = (item.author?.nickname || item.nickname || '').toLowerCase();
|
|
1058
|
+
if (authorName === username.toLowerCase()) {
|
|
1059
|
+
authorUuid = item.author?.uuid || item.author_uuid || '';
|
|
1060
|
+
break;
|
|
972
1061
|
}
|
|
973
|
-
assets.push({ title, files, hash, tags: Array.from(detectedTags), sourcePath: dirPath });
|
|
974
1062
|
}
|
|
975
|
-
|
|
1063
|
+
} catch { /* fall through */ }
|
|
1064
|
+
|
|
1065
|
+
if (!authorUuid) {
|
|
1066
|
+
// Try fetching user's own workflows if logged in and cloning self
|
|
1067
|
+
if (config?.token) {
|
|
1068
|
+
try {
|
|
1069
|
+
const me = await apiRequest('GET', '/api/v1/tokenboard/auth/me', null, config.token, apiBase);
|
|
1070
|
+
if (me.nickname?.toLowerCase() === username.toLowerCase()) {
|
|
1071
|
+
authorUuid = me.uuid;
|
|
1072
|
+
}
|
|
1073
|
+
} catch { /* fall through */ }
|
|
1074
|
+
}
|
|
1075
|
+
if (!authorUuid) error(`User @${username} not found. Make sure they have published assets.`);
|
|
976
1076
|
}
|
|
977
1077
|
|
|
978
|
-
//
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
if (files.length === 0) continue;
|
|
993
|
-
|
|
994
|
-
const title = entry.name;
|
|
995
|
-
const hash = computeHash(files);
|
|
996
|
-
const detectedTags = new Set();
|
|
997
|
-
for (const f of files) {
|
|
998
|
-
const ft = detectFileType(f.name);
|
|
999
|
-
const tag = guessTag(ft);
|
|
1000
|
-
if (tag) detectedTags.add(tag);
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
assets.push({ title, files, hash, tags: Array.from(detectedTags), sourcePath: fullPath });
|
|
1004
|
-
} else if (entry.isFile()) {
|
|
1005
|
-
// Loose file
|
|
1006
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
1007
|
-
const validExts = ['.md', '.sh', '.py', '.js', '.mjs', '.ts', '.json', '.yaml', '.yml', '.toml', '.prompt'];
|
|
1008
|
-
if (validExts.includes(ext) || entry.name === '.cursorrules' || entry.name === '.windsurfrules') {
|
|
1009
|
-
looseFiles.push({ path: fullPath, name: entry.name });
|
|
1010
|
-
}
|
|
1078
|
+
// Step 2: List all public workflows by this author
|
|
1079
|
+
info(`Fetching all assets by @${username}...`);
|
|
1080
|
+
let allItems = [];
|
|
1081
|
+
let page = 1;
|
|
1082
|
+
while (true) {
|
|
1083
|
+
try {
|
|
1084
|
+
const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?author_uuid=${authorUuid}&page=${page}&page_size=50&sort_by=latest`, null, config?.token, apiBase);
|
|
1085
|
+
const items = data.list || data.items || [];
|
|
1086
|
+
if (items.length === 0) break;
|
|
1087
|
+
allItems = allItems.concat(items);
|
|
1088
|
+
if (items.length < 50) break;
|
|
1089
|
+
page++;
|
|
1090
|
+
} catch (e) {
|
|
1091
|
+
error(`Failed to list assets: ${e.message}`);
|
|
1011
1092
|
}
|
|
1012
1093
|
}
|
|
1013
1094
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
if (!content.trim()) continue;
|
|
1095
|
+
if (allItems.length === 0) {
|
|
1096
|
+
info(`@${username} has no public assets.`);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1019
1099
|
|
|
1020
|
-
|
|
1021
|
-
const title = guessAssetTitle(files, path.basename(f.name, path.extname(f.name)));
|
|
1022
|
-
const hash = computeHash(files);
|
|
1023
|
-
const ft = detectFileType(f.name);
|
|
1024
|
-
const tag = guessTag(ft);
|
|
1100
|
+
log(` Found ${C.bold}${allItems.length}${C.reset} assets\n`);
|
|
1025
1101
|
|
|
1026
|
-
|
|
1102
|
+
// Step 3: Create directory and pull each asset
|
|
1103
|
+
const outDir = path.join(process.cwd(), username);
|
|
1104
|
+
if (!fs.existsSync(outDir)) {
|
|
1105
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
1027
1106
|
}
|
|
1028
1107
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const SKIP = new Set(['.DS_Store', 'node_modules', '.git', '__pycache__']);
|
|
1108
|
+
let downloaded = 0;
|
|
1109
|
+
for (const item of allItems) {
|
|
1110
|
+
const title = item.title || item.uuid;
|
|
1111
|
+
const safeDirName = title.replace(/[/\\?%*:|"<>]/g, '-').substring(0, 80);
|
|
1112
|
+
const assetDir = path.join(outDir, safeDirName);
|
|
1035
1113
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
if (
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
let content;
|
|
1050
|
-
try { content = fs.readFileSync(fullPath, 'utf8'); } catch { continue; }
|
|
1051
|
-
if (!content.trim()) continue;
|
|
1052
|
-
files.push({ name: relPath, content, type: detectFileType(relPath) });
|
|
1114
|
+
try {
|
|
1115
|
+
const detail = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?uuid=${item.uuid}`, null, config?.token, apiBase);
|
|
1116
|
+
const workflow = detail.workflow;
|
|
1117
|
+
|
|
1118
|
+
if (workflow.steps && workflow.steps.length > 0) {
|
|
1119
|
+
if (!fs.existsSync(assetDir)) fs.mkdirSync(assetDir, { recursive: true });
|
|
1120
|
+
for (const step of workflow.steps) {
|
|
1121
|
+
const content = step.prompt_template || step.promptTemplate;
|
|
1122
|
+
if (content) {
|
|
1123
|
+
const fileName = `${step.title || 'step-' + step.step_order}`;
|
|
1124
|
+
const safeName = fileName.replace(/[/\\?%*:|"<>]/g, '-');
|
|
1125
|
+
fs.writeFileSync(path.join(assetDir, safeName), content);
|
|
1126
|
+
}
|
|
1053
1127
|
}
|
|
1128
|
+
downloaded++;
|
|
1129
|
+
log(` ${C.green}✓${C.reset} ${safeDirName} ${C.dim}(${workflow.steps.length} files)${C.reset}`);
|
|
1054
1130
|
}
|
|
1131
|
+
} catch (e) {
|
|
1132
|
+
log(` ${C.yellow}!${C.reset} ${safeDirName} ${C.dim}(skipped: ${e.message})${C.reset}`);
|
|
1055
1133
|
}
|
|
1056
1134
|
}
|
|
1057
1135
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1136
|
+
log('');
|
|
1137
|
+
success(`Cloned ${downloaded}/${allItems.length} assets to ./${username}/`);
|
|
1060
1138
|
}
|
|
1061
1139
|
|
|
1062
|
-
function
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1140
|
+
async function cmdTags() {
|
|
1141
|
+
log(`\n${C.bold}tokrepo tags${C.reset}\n`);
|
|
1142
|
+
|
|
1143
|
+
const config = readConfig();
|
|
1144
|
+
const apiBase = config?.api || DEFAULT_API;
|
|
1145
|
+
|
|
1146
|
+
try {
|
|
1147
|
+
const data = await apiRequest('GET', '/api/v1/tokenboard/tags/list', null, null, apiBase);
|
|
1148
|
+
log(` Available tags:\n`);
|
|
1149
|
+
for (const tag of data.tags) {
|
|
1150
|
+
log(` ${C.cyan}${tag.name}${C.reset}${tag.count ? ` ${C.dim}(${tag.count} assets)${C.reset}` : ''}`);
|
|
1068
1151
|
}
|
|
1152
|
+
log('');
|
|
1153
|
+
} catch (e) {
|
|
1154
|
+
error(`Failed: ${e.message}`);
|
|
1069
1155
|
}
|
|
1070
|
-
// Clean up fallback name
|
|
1071
|
-
return fallbackName
|
|
1072
|
-
.replace(/[-_]/g, ' ')
|
|
1073
|
-
.replace(/\b\w/g, c => c.toUpperCase());
|
|
1074
1156
|
}
|
|
1075
1157
|
|
|
1076
|
-
async function
|
|
1077
|
-
|
|
1158
|
+
async function cmdStatus() {
|
|
1159
|
+
log(`\n${C.bold}tokrepo status${C.reset}\n`);
|
|
1160
|
+
|
|
1078
1161
|
const config = readConfig();
|
|
1079
1162
|
if (!config || !config.token) error(`Not logged in. Run: ${C.cyan}tokrepo login${C.reset}`);
|
|
1080
1163
|
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
: path.join(require('os').homedir(), '.claude', 'skills');
|
|
1164
|
+
const projectConfig = readProjectConfig();
|
|
1165
|
+
const baseDir = process.cwd();
|
|
1084
1166
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1167
|
+
// Collect local files
|
|
1168
|
+
let filesToCheck;
|
|
1169
|
+
if (projectConfig) {
|
|
1170
|
+
const patterns = projectConfig.files || ['*.md'];
|
|
1171
|
+
filesToCheck = findFiles(patterns, baseDir);
|
|
1172
|
+
} else {
|
|
1173
|
+
filesToCheck = collectFiles(['.'], baseDir);
|
|
1087
1174
|
}
|
|
1088
1175
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
log(`\n${C.bold}tokrepo sync${C.reset}\n`);
|
|
1092
|
-
info(`Scanning ${targetDir}...`);
|
|
1093
|
-
|
|
1094
|
-
const assets = scanDirectory(targetDir);
|
|
1095
|
-
if (assets.length === 0) {
|
|
1096
|
-
info('No assets found in directory.');
|
|
1176
|
+
if (filesToCheck.length === 0) {
|
|
1177
|
+
info('No pushable files found in current directory.');
|
|
1097
1178
|
return;
|
|
1098
1179
|
}
|
|
1099
1180
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
// Classify each asset: check local state first, then remote diff for unknowns
|
|
1106
|
-
const needsRemoteCheck = [];
|
|
1107
|
-
const localStatus = {};
|
|
1108
|
-
|
|
1109
|
-
for (const asset of assets) {
|
|
1110
|
-
const key = asset.sourcePath;
|
|
1111
|
-
const cached = syncState[key];
|
|
1112
|
-
if (cached && cached.hash === asset.hash) {
|
|
1113
|
-
// Local hash matches last sync — unchanged
|
|
1114
|
-
localStatus[asset.title] = { status: 'unchanged', uuid: cached.uuid };
|
|
1115
|
-
} else if (cached && cached.hash !== asset.hash) {
|
|
1116
|
-
// Local hash differs from last sync — updated
|
|
1117
|
-
localStatus[asset.title] = { status: 'updated', uuid: cached.uuid };
|
|
1118
|
-
} else {
|
|
1119
|
-
// Not in local state — check remote
|
|
1120
|
-
needsRemoteCheck.push(asset);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
// Remote diff for assets not in local state
|
|
1125
|
-
if (needsRemoteCheck.length > 0) {
|
|
1126
|
-
info('Comparing with remote...');
|
|
1127
|
-
const diffPayload = needsRemoteCheck.map(a => ({ title: a.title, content_hash: a.hash }));
|
|
1128
|
-
try {
|
|
1129
|
-
const data = await apiRequest('POST', '/api/v1/tokenboard/push/diff', { assets: diffPayload }, config.token, config.api);
|
|
1130
|
-
for (const r of data.results) {
|
|
1131
|
-
localStatus[r.title] = { status: r.status, uuid: r.remote_uuid || '' };
|
|
1132
|
-
}
|
|
1133
|
-
} catch (e) {
|
|
1134
|
-
warn(`Diff API: ${e.message} — treating unknowns as new`);
|
|
1135
|
-
for (const a of needsRemoteCheck) {
|
|
1136
|
-
localStatus[a.title] = { status: 'new', uuid: '' };
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1181
|
+
// Build assets with content hashes for diff
|
|
1182
|
+
const crypto = require('crypto');
|
|
1183
|
+
const assets = [];
|
|
1184
|
+
const titleBase = projectConfig?.title || guessTitle(filesToCheck, baseDir);
|
|
1140
1185
|
|
|
1141
|
-
//
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
for (const
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
} else if (st.status === 'updated') {
|
|
1150
|
-
log(` ${C.yellow}~ updated${C.reset} ${asset.title}`);
|
|
1151
|
-
updatedCount++;
|
|
1152
|
-
} else {
|
|
1153
|
-
log(` ${C.dim}= unchanged ${asset.title}${C.reset}`);
|
|
1154
|
-
unchangedCount++;
|
|
1155
|
-
}
|
|
1186
|
+
// Each file becomes an asset for diff comparison
|
|
1187
|
+
// But the primary asset is the whole push unit
|
|
1188
|
+
const pushFiles = [];
|
|
1189
|
+
for (const f of filesToCheck) {
|
|
1190
|
+
let content;
|
|
1191
|
+
try { content = fs.readFileSync(f.path, 'utf8'); } catch { continue; }
|
|
1192
|
+
if (!content.trim()) continue;
|
|
1193
|
+
pushFiles.push({ name: f.relPath, content });
|
|
1156
1194
|
}
|
|
1157
1195
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
if (newCount === 0 && updatedCount === 0) {
|
|
1162
|
-
log('');
|
|
1163
|
-
success('Everything is up to date!');
|
|
1196
|
+
if (pushFiles.length === 0) {
|
|
1197
|
+
info('No readable text files found.');
|
|
1164
1198
|
return;
|
|
1165
1199
|
}
|
|
1166
1200
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
// Upsert each changed asset + save state
|
|
1176
|
-
let successCount = 0;
|
|
1177
|
-
let failCount = 0;
|
|
1178
|
-
|
|
1179
|
-
for (const asset of assets) {
|
|
1180
|
-
const st = localStatus[asset.title] || { status: 'new' };
|
|
1181
|
-
if (st.status === 'unchanged') continue;
|
|
1182
|
-
|
|
1183
|
-
const totalChars = asset.files.reduce((sum, f) => sum + f.content.length, 0);
|
|
1184
|
-
|
|
1185
|
-
try {
|
|
1186
|
-
const data = await apiRequest('POST', '/api/v1/tokenboard/push/upsert', {
|
|
1187
|
-
title: asset.title,
|
|
1188
|
-
files: asset.files,
|
|
1189
|
-
tags: asset.tags,
|
|
1190
|
-
token_cost: String(Math.round(totalChars / 4)),
|
|
1191
|
-
visibility,
|
|
1192
|
-
}, config.token, config.api);
|
|
1193
|
-
|
|
1194
|
-
const action = data.action === 'created' ? C.green + '+ created' : C.yellow + '~ updated';
|
|
1195
|
-
log(` ${action}${C.reset} ${asset.title} ${C.dim}${data.url}${C.reset}`);
|
|
1196
|
-
|
|
1197
|
-
// Save to local sync state
|
|
1198
|
-
syncState[asset.sourcePath] = {
|
|
1199
|
-
uuid: data.uuid,
|
|
1200
|
-
hash: asset.hash,
|
|
1201
|
-
title: asset.title,
|
|
1202
|
-
url: data.url,
|
|
1203
|
-
lastSync: new Date().toISOString(),
|
|
1204
|
-
};
|
|
1205
|
-
successCount++;
|
|
1206
|
-
} catch (e) {
|
|
1207
|
-
log(` ${C.red}✗ failed${C.reset} ${asset.title}: ${e.message}`);
|
|
1208
|
-
failCount++;
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// Persist sync state
|
|
1213
|
-
writeSyncState(syncState);
|
|
1214
|
-
|
|
1215
|
-
log('');
|
|
1216
|
-
if (failCount === 0) {
|
|
1217
|
-
success(`Synced ${successCount} assets!`);
|
|
1218
|
-
} else {
|
|
1219
|
-
warn(`${successCount} synced, ${failCount} failed`);
|
|
1220
|
-
}
|
|
1221
|
-
log('');
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
async function cmdStatus() {
|
|
1225
|
-
const args = parseArgs(process.argv);
|
|
1226
|
-
const config = readConfig();
|
|
1227
|
-
if (!config || !config.token) error(`Not logged in. Run: ${C.cyan}tokrepo login${C.reset}`);
|
|
1228
|
-
|
|
1229
|
-
const targetDir = args.positional[0]
|
|
1230
|
-
? path.resolve(args.positional[0])
|
|
1231
|
-
: path.join(require('os').homedir(), '.claude', 'skills');
|
|
1232
|
-
|
|
1233
|
-
if (!fs.existsSync(targetDir)) {
|
|
1234
|
-
error(`Directory not found: ${targetDir}`);
|
|
1201
|
+
// Compute content hash matching backend's computeContentHash format
|
|
1202
|
+
const h = crypto.createHash('sha256');
|
|
1203
|
+
for (const f of pushFiles) {
|
|
1204
|
+
h.update(f.name);
|
|
1205
|
+
h.update('\0');
|
|
1206
|
+
h.update(f.content);
|
|
1207
|
+
h.update('\0');
|
|
1235
1208
|
}
|
|
1209
|
+
const localHash = h.digest('hex');
|
|
1236
1210
|
|
|
1237
|
-
|
|
1238
|
-
info(`Scanning ${targetDir}...`);
|
|
1211
|
+
assets.push({ title: titleBase, content_hash: localHash });
|
|
1239
1212
|
|
|
1240
|
-
|
|
1241
|
-
if (assets.length === 0) {
|
|
1242
|
-
info('No assets found in directory.');
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1213
|
+
info('Comparing local files with remote...');
|
|
1245
1214
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
localStatus[asset.title] = { status: 'unchanged', uuid: cached.uuid };
|
|
1255
|
-
} else if (cached && cached.hash !== asset.hash) {
|
|
1256
|
-
localStatus[asset.title] = { status: 'updated', uuid: cached.uuid };
|
|
1257
|
-
} else {
|
|
1258
|
-
needsRemoteCheck.push(asset);
|
|
1215
|
+
try {
|
|
1216
|
+
const data = await apiRequest('POST', '/api/v1/tokenboard/push/diff', { assets }, config.token, config.api);
|
|
1217
|
+
log('');
|
|
1218
|
+
for (const r of data.results) {
|
|
1219
|
+
const icon = r.status === 'new' ? `${C.green}+ new${C.reset}`
|
|
1220
|
+
: r.status === 'updated' ? `${C.yellow}~ modified${C.reset}`
|
|
1221
|
+
: `${C.dim}= unchanged${C.reset}`;
|
|
1222
|
+
log(` ${icon} ${r.title}${r.remote_uuid ? ` ${C.dim}(${r.remote_uuid.substring(0, 8)}...)${C.reset}` : ''}`);
|
|
1259
1223
|
}
|
|
1260
|
-
|
|
1224
|
+
log('');
|
|
1261
1225
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const
|
|
1265
|
-
try {
|
|
1266
|
-
const data = await apiRequest('POST', '/api/v1/tokenboard/push/diff', { assets: diffPayload }, config.token, config.api);
|
|
1267
|
-
for (const r of data.results) {
|
|
1268
|
-
localStatus[r.title] = { status: r.status, uuid: r.remote_uuid || '' };
|
|
1269
|
-
}
|
|
1270
|
-
} catch (e) {
|
|
1271
|
-
error(`Diff API error: ${e.message}`);
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1226
|
+
const newCount = data.results.filter(r => r.status === 'new').length;
|
|
1227
|
+
const updatedCount = data.results.filter(r => r.status === 'updated').length;
|
|
1228
|
+
const unchangedCount = data.results.filter(r => r.status === 'unchanged').length;
|
|
1274
1229
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
for (const asset of assets) {
|
|
1279
|
-
const st = localStatus[asset.title] || { status: 'new', uuid: '' };
|
|
1280
|
-
const status = st.status;
|
|
1281
|
-
const uuid = st.uuid || '';
|
|
1282
|
-
const uuidShort = uuid ? ` ${C.dim}(${uuid.substring(0, 8)})${C.reset}` : '';
|
|
1283
|
-
|
|
1284
|
-
if (status === 'new') {
|
|
1285
|
-
log(` ${C.green}+ new${C.reset} ${asset.title} ${C.dim}(${asset.files.length} files)${C.reset}`);
|
|
1286
|
-
newCount++;
|
|
1287
|
-
} else if (status === 'updated') {
|
|
1288
|
-
log(` ${C.yellow}~ modified${C.reset} ${asset.title}${uuidShort}`);
|
|
1289
|
-
updatedCount++;
|
|
1230
|
+
if (newCount || updatedCount) {
|
|
1231
|
+
info(`${newCount ? newCount + ' new' : ''}${newCount && updatedCount ? ', ' : ''}${updatedCount ? updatedCount + ' modified' : ''}. Run ${C.cyan}tokrepo push${C.reset} to sync.`);
|
|
1290
1232
|
} else {
|
|
1291
|
-
|
|
1292
|
-
unchangedCount++;
|
|
1233
|
+
success('Everything up to date.');
|
|
1293
1234
|
}
|
|
1235
|
+
} catch (e) {
|
|
1236
|
+
error(`Status check failed: ${e.message}`);
|
|
1294
1237
|
}
|
|
1295
|
-
|
|
1296
|
-
log('');
|
|
1297
|
-
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}`);
|
|
1298
|
-
|
|
1299
|
-
if (newCount > 0 || updatedCount > 0) {
|
|
1300
|
-
log(`\n Run ${C.cyan}tokrepo sync ${args.positional[0] || ''}${C.reset} to push changes`);
|
|
1301
|
-
}
|
|
1302
|
-
log('');
|
|
1303
1238
|
}
|
|
1304
1239
|
|
|
1305
1240
|
function showHelp() {
|
|
@@ -1317,18 +1252,18 @@ ${C.bold}USAGE${C.reset}
|
|
|
1317
1252
|
${C.bold}DISCOVER & INSTALL${C.reset}
|
|
1318
1253
|
${C.cyan}search${C.reset} <query> Search assets by keyword
|
|
1319
1254
|
${C.cyan}install${C.reset} <name|uuid> Smart install (auto-detects type & placement)
|
|
1320
|
-
${C.cyan}pull${C.reset} <url|uuid>
|
|
1255
|
+
${C.cyan}pull${C.reset} <url|uuid|@u/n> Download raw asset files
|
|
1256
|
+
${C.cyan}clone${C.reset} @username Clone all assets from a user
|
|
1321
1257
|
|
|
1322
|
-
${C.bold}PUBLISH
|
|
1323
|
-
${C.cyan}push${C.reset} [files...] Push files/directory (
|
|
1324
|
-
${C.cyan}
|
|
1325
|
-
${C.cyan}status${C.reset} [dir] Show local vs remote diff
|
|
1258
|
+
${C.bold}PUBLISH${C.reset}
|
|
1259
|
+
${C.cyan}push${C.reset} [files...] Push files/directory (idempotent upsert)
|
|
1260
|
+
${C.cyan}status${C.reset} Compare local vs remote (like git status)
|
|
1326
1261
|
${C.cyan}init${C.reset} Create .tokrepo.json project config
|
|
1327
|
-
${C.cyan}update${C.reset} <uuid> [f] Update existing asset
|
|
1262
|
+
${C.cyan}update${C.reset} <uuid> [f] Update existing asset
|
|
1328
1263
|
${C.cyan}delete${C.reset} <uuid> Delete an asset
|
|
1329
1264
|
|
|
1330
1265
|
${C.bold}ACCOUNT${C.reset}
|
|
1331
|
-
${C.cyan}login${C.reset} Save API token
|
|
1266
|
+
${C.cyan}login${C.reset} Save API token (or set TOKREPO_TOKEN env var)
|
|
1332
1267
|
${C.cyan}list${C.reset} List your published assets
|
|
1333
1268
|
${C.cyan}tags${C.reset} List available tags
|
|
1334
1269
|
${C.cyan}whoami${C.reset} Show current user
|
|
@@ -1348,16 +1283,6 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
|
|
|
1348
1283
|
MCP → current dir (.json)
|
|
1349
1284
|
Prompts → current dir (.md)
|
|
1350
1285
|
|
|
1351
|
-
${C.bold}SYNC (the killer feature)${C.reset}
|
|
1352
|
-
${C.cyan}tokrepo sync ~/.claude/skills/${C.reset} # Sync all skills (default: private)
|
|
1353
|
-
${C.cyan}tokrepo sync ~/.claude/skills/ --public${C.reset} # Sync as public assets
|
|
1354
|
-
${C.cyan}tokrepo sync . -y${C.reset} # Sync current dir, skip confirm
|
|
1355
|
-
${C.cyan}tokrepo status${C.reset} # Show what would change
|
|
1356
|
-
|
|
1357
|
-
Sync scans a directory, detects new/modified assets, and pushes only
|
|
1358
|
-
what changed. Each subdirectory becomes one asset. Loose files become
|
|
1359
|
-
individual assets. Like ${C.bold}git push${C.reset} for your AI assets.
|
|
1360
|
-
|
|
1361
1286
|
${C.bold}EXAMPLES${C.reset}
|
|
1362
1287
|
tokrepo search "mcp server" # Find MCP configs
|
|
1363
1288
|
tokrepo install ca000374-f5d8-... # Install by UUID
|
|
@@ -1371,8 +1296,12 @@ ${C.bold}FILE TYPE AUTO-DETECTION${C.reset}
|
|
|
1371
1296
|
.prompt .prompt.md → prompt
|
|
1372
1297
|
.md (other) → other
|
|
1373
1298
|
|
|
1299
|
+
${C.bold}AGENT / CI SETUP${C.reset}
|
|
1300
|
+
export TOKREPO_TOKEN=tk_xxx # skip login, agents use this
|
|
1301
|
+
export TOKREPO_API=https://... # optional custom API endpoint
|
|
1302
|
+
|
|
1374
1303
|
${C.bold}GET YOUR TOKEN${C.reset}
|
|
1375
|
-
https://tokrepo.com/en/
|
|
1304
|
+
https://tokrepo.com/en/my/settings
|
|
1376
1305
|
`);
|
|
1377
1306
|
}
|
|
1378
1307
|
|
|
@@ -1385,15 +1314,15 @@ async function main() {
|
|
|
1385
1314
|
case 'login': await cmdLogin(); break;
|
|
1386
1315
|
case 'init': await cmdInit(); break;
|
|
1387
1316
|
case 'push': await cmdPush(); break;
|
|
1388
|
-
case 'sync': await cmdSync(); break;
|
|
1389
|
-
case 'status': case 'st': await cmdStatus(); break;
|
|
1390
1317
|
case 'pull': await cmdPull(); break;
|
|
1391
1318
|
case 'search': case 'find': await cmdSearch(); break;
|
|
1392
1319
|
case 'install': case 'i': await cmdInstall(); break;
|
|
1393
1320
|
case 'list': await cmdList(); break;
|
|
1394
1321
|
case 'update': await cmdUpdate(); break;
|
|
1395
1322
|
case 'delete': await cmdDelete(); break;
|
|
1323
|
+
case 'clone': await cmdClone(); break;
|
|
1396
1324
|
case 'tags': await cmdTags(); break;
|
|
1325
|
+
case 'status': case 'diff': await cmdStatus(); break;
|
|
1397
1326
|
case 'whoami': await cmdWhoami(); break;
|
|
1398
1327
|
case '--version': case '-v': case 'version':
|
|
1399
1328
|
log(`tokrepo ${CLI_VERSION}`); break;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokrepo",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "AI assets for humans and agents —
|
|
3
|
+
"version": "3.3.0",
|
|
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"
|
|
7
7
|
},
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/
|
|
14
|
+
"url": "git+https://github.com/tokrepo/cli.git"
|
|
15
15
|
},
|
|
16
16
|
"homepage": "https://tokrepo.com",
|
|
17
17
|
"keywords": [
|