tokrepo 3.3.0 → 3.3.2

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 +242 -42
  2. package/package.json +1 -1
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');
@@ -23,7 +24,7 @@ const CONFIG_DIR = path.join(require('os').homedir(), '.tokrepo');
23
24
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
24
25
  const PROJECT_CONFIG = '.tokrepo.json';
25
26
  const DEFAULT_API = 'https://api.tokrepo.com';
26
- const CLI_VERSION = '3.3.0';
27
+ const CLI_VERSION = '3.3.2';
27
28
  const VERSION_CHECK_FILE = path.join(require('os').homedir(), '.tokrepo', '.version-check');
28
29
 
29
30
  // ─── Helpers ───
@@ -291,6 +292,10 @@ function parseArgs(argv) {
291
292
  } else if (arg.startsWith('--tag=')) {
292
293
  if (!args.flags.tags) args.flags.tags = [];
293
294
  args.flags.tags.push(arg.split('=').slice(1).join('='));
295
+ } else if (arg === '--target' && i + 1 < argv.length) {
296
+ args.flags.target = argv[++i];
297
+ } else if (arg.startsWith('--target=')) {
298
+ args.flags.target = arg.split('=').slice(1).join('=');
294
299
  } else if (arg === '-y' || arg === '--yes') {
295
300
  args.flags.yes = true;
296
301
  } else if (!arg.startsWith('-')) {
@@ -414,6 +419,7 @@ async function saveAndVerifyToken(token) {
414
419
 
415
420
  function browserAuthFlow() {
416
421
  return new Promise((resolve) => {
422
+ const state = crypto.randomBytes(16).toString('hex');
417
423
  const server = http.createServer((req, res) => {
418
424
  // CORS headers for browser fetch
419
425
  res.setHeader('Access-Control-Allow-Origin', '*');
@@ -432,6 +438,11 @@ function browserAuthFlow() {
432
438
  req.on('end', () => {
433
439
  try {
434
440
  const data = JSON.parse(body);
441
+ if (!data.token || data.state !== state) {
442
+ res.writeHead(403);
443
+ res.end('Invalid authorization state');
444
+ return;
445
+ }
435
446
  res.writeHead(200, { 'Content-Type': 'application/json' });
436
447
  res.end(JSON.stringify({ ok: true }));
437
448
  server.close();
@@ -452,7 +463,7 @@ function browserAuthFlow() {
452
463
  // Listen on random port
453
464
  server.listen(0, '127.0.0.1', () => {
454
465
  const port = server.address().port;
455
- const authUrl = `https://tokrepo.com/en/cli-auth?port=${port}`;
466
+ const authUrl = `https://tokrepo.com/en/cli-auth?port=${port}&state=${state}`;
456
467
 
457
468
  info(`Listening on http://127.0.0.1:${port}`);
458
469
  log(` ${C.dim}If browser doesn't open, visit:${C.reset}`);
@@ -517,7 +528,7 @@ async function cmdPush() {
517
528
  // Flags override config
518
529
  title = args.flags.title || title || guessTitle(filesToPush, baseDir);
519
530
  description = args.flags.desc || description || '';
520
- visibility = args.flags.public ? 1 : (args.flags.private ? 0 : (projectConfig?.visibility ?? 1));
531
+ visibility = args.flags.public ? 1 : (args.flags.private ? 0 : (projectConfig?.visibility ?? 0));
521
532
  tags = args.flags.tags || tags || [];
522
533
 
523
534
  // Read files and detect types
@@ -553,8 +564,8 @@ async function cmdPush() {
553
564
  // Show summary
554
565
  log(`\n${C.bold}tokrepo push${C.reset}\n`);
555
566
  log(` ${C.bold}Title:${C.reset} ${title}`);
556
- log(` ${C.bold}Visibility:${C.reset} ${visibility === 1 ? `${C.green}public${C.reset}` : `${C.yellow}private${C.reset}`}`);
557
- log(` ${C.bold}Files:${C.reset} ${pushFiles.length}`);
567
+ log(` ${C.bold}Visibility:${C.reset} ${visibility === 1 ? `${C.green}public${C.reset} (visible to everyone)` : `${C.yellow}private${C.reset} (only you can see)`}`);
568
+ log(` ${C.bold}Files:${C.reset} ${pushFiles.length} (only these files will be uploaded)`);
558
569
  if (detectedTags.size > 0) {
559
570
  log(` ${C.bold}Tags:${C.reset} ${Array.from(detectedTags).join(', ')}`);
560
571
  }
@@ -614,7 +625,7 @@ async function cmdInit() {
614
625
  title: title || dirName,
615
626
  description: description || '',
616
627
  files: ['*.md', '*.sh', '*.py', '*.js', '*.mjs', '*.ts', '*.json', '*.yaml'],
617
- visibility: 1,
628
+ visibility: 0,
618
629
  tags: [],
619
630
  };
620
631
 
@@ -663,6 +674,11 @@ async function cmdPull() {
663
674
  }
664
675
  }
665
676
 
677
+ // Normalize query: replace hyphens/underscores/dots with spaces for better matching
678
+ function normalizeQuery(q) {
679
+ return q.replace(/[-_.]/g, ' ').replace(/\s+/g, ' ').trim();
680
+ }
681
+
666
682
  // Resolve various input formats to a UUID:
667
683
  // - UUID directly: "ca000374-f5d8-..."
668
684
  // - Full URL: "https://tokrepo.com/en/workflows/ca000374-f5d8-..."
@@ -680,9 +696,10 @@ async function resolveAssetId(input, config, apiBase) {
680
696
  const atMatch = input.match(/^@([^/]+)\/(.+)$/);
681
697
  if (atMatch) {
682
698
  const [, username, assetName] = atMatch;
683
- info(`Searching for "${assetName}" by @${username}...`);
699
+ const normalizedName = normalizeQuery(assetName);
700
+ info(`Searching for "${normalizedName}" by @${username}...`);
684
701
  // Search by keyword, then filter by author nickname
685
- const encoded = encodeURIComponent(assetName);
702
+ const encoded = encodeURIComponent(normalizedName);
686
703
  try {
687
704
  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
705
  const items = data.list || data.items || [];
@@ -700,9 +717,10 @@ async function resolveAssetId(input, config, apiBase) {
700
717
  error(`Asset not found: ${input}`);
701
718
  }
702
719
 
703
- // Plain name: search by keyword
704
- info(`Searching for "${input}"...`);
705
- const encoded = encodeURIComponent(input);
720
+ // Plain name: search by keyword (normalize separators)
721
+ const normalizedInput = normalizeQuery(input);
722
+ info(`Searching for "${normalizedInput}"...`);
723
+ const encoded = encodeURIComponent(normalizedInput);
706
724
  try {
707
725
  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
726
  const items = data.list || data.items || [];
@@ -714,10 +732,12 @@ async function resolveAssetId(input, config, apiBase) {
714
732
  // ─── Search ───
715
733
 
716
734
  async function cmdSearch() {
717
- const query = process.argv.slice(3).join(' ');
718
- if (!query) error('Usage: tokrepo search <keyword>');
735
+ const rawQuery = process.argv.slice(3).join(' ');
736
+ if (!rawQuery) error('Usage: tokrepo search <keyword>');
719
737
 
720
- log(`\n${C.bold}tokrepo search${C.reset} "${query}"\n`);
738
+ const query = normalizeQuery(rawQuery);
739
+ const displayQuery = query !== rawQuery ? `"${rawQuery}" → "${query}"` : `"${query}"`;
740
+ log(`\n${C.bold}tokrepo search${C.reset} ${displayQuery}\n`);
721
741
 
722
742
  const config = readConfig();
723
743
  const apiBase = config?.api || DEFAULT_API;
@@ -728,7 +748,14 @@ async function cmdSearch() {
728
748
 
729
749
  if (!data.list || data.list.length === 0) {
730
750
  info('No assets found.');
731
- log(`\n ${C.dim}Try different keywords or browse: https://tokrepo.com/en/featured${C.reset}\n`);
751
+ // Suggest broader search terms
752
+ const words = query.split(' ');
753
+ if (words.length > 1) {
754
+ log(`\n ${C.dim}Try fewer keywords:${C.reset}`);
755
+ log(` ${C.cyan}tokrepo search ${words[0]}${C.reset}`);
756
+ log(` ${C.cyan}tokrepo search ${words.slice(0, 2).join(' ')}${C.reset}`);
757
+ }
758
+ log(`\n ${C.dim}Browse all: https://tokrepo.com/en/featured${C.reset}\n`);
732
759
  return;
733
760
  }
734
761
 
@@ -739,8 +766,13 @@ async function cmdSearch() {
739
766
  const tags = (wf.tags || []).map(t => t.name).join(', ');
740
767
  const views = wf.view_count || 0;
741
768
  const votes = wf.vote_count || 0;
769
+ // Truncate long descriptions for readability
770
+ const desc = (wf.description || '').length > 80
771
+ ? wf.description.substring(0, 77) + '...'
772
+ : (wf.description || '');
742
773
 
743
774
  log(` ${C.dim}${String(i + 1).padStart(2)}.${C.reset} ${C.bold}${wf.title}${C.reset}`);
775
+ if (desc) log(` ${desc}`);
744
776
  if (tags) log(` ${C.cyan}${tags}${C.reset} ${C.dim}★${votes} 👁${views}${C.reset}`);
745
777
  log(` ${C.dim}tokrepo install ${wf.uuid}${C.reset}`);
746
778
  log('');
@@ -752,50 +784,189 @@ async function cmdSearch() {
752
784
 
753
785
  // ─── Install (smart pull with correct placement) ───
754
786
 
787
+ function normalizeInstallTarget(target) {
788
+ if (!target) return '';
789
+ const normalized = String(target).trim().toLowerCase();
790
+ const aliases = {
791
+ gemini: 'gemini',
792
+ 'gemini-cli': 'gemini',
793
+ };
794
+ return aliases[normalized] || normalized;
795
+ }
796
+
797
+ function validateInstallTarget(target) {
798
+ if (!target) return '';
799
+ const normalized = normalizeInstallTarget(target);
800
+ if (normalized !== 'gemini') {
801
+ error(`Unsupported install target: ${target}. Supported targets: gemini`);
802
+ }
803
+ return normalized;
804
+ }
805
+
806
+ function pickWritablePath(destPath, overwrite) {
807
+ if (!fs.existsSync(destPath)) return destPath;
808
+ if (overwrite) {
809
+ warn(`File exists: ${path.relative(process.cwd(), destPath)} (overwriting)`);
810
+ return destPath;
811
+ }
812
+
813
+ const dir = path.dirname(destPath);
814
+ const ext = path.extname(destPath);
815
+ const base = path.basename(destPath, ext);
816
+ let index = 2;
817
+ let candidate = path.join(dir, `${base}.${index}${ext}`);
818
+ while (fs.existsSync(candidate)) {
819
+ index++;
820
+ candidate = path.join(dir, `${base}.${index}${ext}`);
821
+ }
822
+ warn(`File exists: ${path.relative(process.cwd(), destPath)}; writing ${path.relative(process.cwd(), candidate)} instead. Use --yes to overwrite.`);
823
+ return candidate;
824
+ }
825
+
826
+ function formatGeminiContent(workflow, contents) {
827
+ const parts = [
828
+ `# ${workflow.title || 'TokRepo Asset'}`,
829
+ workflow.description ? workflow.description : '',
830
+ '<!-- Installed from TokRepo. Gemini CLI reads GEMINI.md as project instructions. -->',
831
+ ].filter(Boolean);
832
+
833
+ for (const item of contents) {
834
+ const title = item.name ? `## ${item.name}` : '## Instructions';
835
+ parts.push(`${title}\n\n${String(item.content || '').trim()}`);
836
+ }
837
+
838
+ return `${parts.join('\n\n').trim()}\n`;
839
+ }
840
+
755
841
  async function cmdInstall() {
756
- const target = process.argv[3];
842
+ const args = parseArgs(process.argv);
843
+ const target = args.positional[0];
757
844
  if (!target) {
758
- error(`Usage: tokrepo install <name-or-uuid>
845
+ error(`Usage: tokrepo install <target> [--target gemini] [--yes]
759
846
 
760
847
  Examples:
761
- tokrepo install awesome-cursor-rules
762
- tokrepo install ca000374-f5d8-4d75-a30c-460fda0b6b0e
763
- tokrepo install https://tokrepo.com/en/workflows/ca000374-...`);
848
+ tokrepo install awesome-cursor-rules # by name
849
+ tokrepo install ca000374-f5d8-4d75-a30c-460fda0b6b0e # by uuid
850
+ tokrepo install https://tokrepo.com/en/workflows/ca000374-...
851
+ tokrepo install pack/seo-geo # install whole theme pack
852
+ tokrepo install c4b18aeb --target gemini # write .gemini/GEMINI.md`);
764
853
  }
765
854
 
766
855
  log(`\n${C.bold}tokrepo install${C.reset}\n`);
767
856
 
768
857
  const config = readConfig();
769
858
  const apiBase = config?.api || DEFAULT_API;
859
+ const installOpts = {
860
+ targetTool: validateInstallTarget(args.flags.target),
861
+ yes: Boolean(args.flags.yes),
862
+ };
863
+
864
+ // pack/<slug> dispatch — install entire theme pack
865
+ if (target.startsWith('pack/')) {
866
+ const slug = target.slice('pack/'.length).trim();
867
+ if (!slug) {
868
+ error('Pack slug is required, e.g. tokrepo install pack/seo-geo');
869
+ }
870
+ await installPack(slug, config, apiBase, installOpts);
871
+ return;
872
+ }
873
+
874
+ await installOneAsset(target, config, apiBase, installOpts);
875
+ }
876
+
877
+ // Install all assets in a theme pack — sequentially, continue past per-item errors
878
+ async function installPack(slug, config, apiBase, opts) {
879
+ info(`Fetching pack ${C.bold}${slug}${C.reset}...`);
880
+ let pack;
881
+ try {
882
+ const data = await apiRequest('GET', `/api/v1/tokenboard/homepage/packs/${encodeURIComponent(slug)}`, null, config?.token, apiBase);
883
+ pack = data.pack;
884
+ } catch (e) {
885
+ error(`Pack not found: ${slug} (${e.message})`);
886
+ }
887
+
888
+ log(`\n ${C.bold}${pack.icon} ${pack.title}${C.reset}`);
889
+ if (pack.description) log(` ${C.dim}${pack.description.substring(0, 140)}${C.reset}`);
890
+ log(` ${C.dim}${pack.items.length} asset(s) in this pack${C.reset}\n`);
891
+
892
+ let ok = 0, fail = 0;
893
+ for (let i = 0; i < pack.items.length; i++) {
894
+ const it = pack.items[i];
895
+ log(`${C.dim}[${i + 1}/${pack.items.length}]${C.reset}`);
896
+ try {
897
+ await installOneAsset(it.uuid, config, apiBase, { ...(opts || {}), silent: false, throwOnError: true });
898
+ ok++;
899
+ } catch (e) {
900
+ warn(`Skipped "${it.title}": ${e.message}`);
901
+ fail++;
902
+ }
903
+ }
904
+
905
+ log('');
906
+ if (fail === 0) {
907
+ success(`${ok} asset(s) installed from pack ${C.bold}${pack.title}${C.reset}`);
908
+ } else {
909
+ log(` ${C.dim}${ok} ok, ${fail} failed${C.reset}`);
910
+ }
911
+ log(` ${C.dim}Pack page: https://tokrepo.com/packs/${slug}${C.reset}\n`);
912
+ }
913
+
914
+ // Single asset install — extracted so `pack/` flow can reuse.
915
+ // opts.throwOnError: pack flow wants to throw and continue; single-cli flow uses error() (which exits)
916
+ async function installOneAsset(target, config, apiBase, opts) {
917
+ opts = opts || {};
918
+ const die = (msg) => { if (opts.throwOnError) throw new Error(msg); error(msg); };
770
919
 
771
920
  // Resolve target to UUID
772
921
  let uuid = target;
773
922
 
774
923
  // URL format
775
- const urlMatch = target.match(/workflows\/([a-f0-9-]+)/);
924
+ const urlMatch = target.match(/workflows\/([^/?#]+)/);
776
925
  if (urlMatch) {
926
+ // URL may carry either UUID or slug-uuid8 — pass through to detail resolver below
777
927
  uuid = urlMatch[1];
778
928
  }
779
- // UUID format check
780
- else if (!/^[a-f0-9-]{36}$/.test(target)) {
781
- // Search by name
782
- info(`Searching for "${target}"...`);
929
+
930
+ // 已经是完整 UUID — 直接用
931
+ if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(uuid)) {
932
+ // ok
933
+ }
934
+ // SEO slug 形态:结尾是 -<8 hex>,先尝试 /detail?slug= 直查,避免走 search 超时
935
+ else if (/-[a-f0-9]{8}$/i.test(uuid)) {
936
+ try {
937
+ const data = await apiRequest('GET', `/api/v1/tokenboard/workflows/detail?slug=${encodeURIComponent(uuid)}`, null, config?.token, apiBase);
938
+ if (data && data.workflow && data.workflow.uuid) {
939
+ uuid = data.workflow.uuid;
940
+ }
941
+ } catch (_) {
942
+ // 404 → 回落到 search
943
+ }
944
+ }
945
+
946
+ if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(uuid)) {
947
+ // Search by name (normalize separators for better matching)
948
+ const normalizedTarget = normalizeQuery(uuid);
949
+ info(`Searching for "${normalizedTarget}"...`);
783
950
  try {
784
- const encoded = encodeURIComponent(target);
951
+ const encoded = encodeURIComponent(normalizedTarget);
785
952
  const searchData = await apiRequest('GET', `/api/v1/tokenboard/workflows/list?keyword=${encoded}&page=1&page_size=5&sort_by=views`, null, config?.token, apiBase);
786
953
 
787
954
  if (!searchData.list || searchData.list.length === 0) {
788
- error(`No asset found matching "${target}". Try: tokrepo search ${target}`);
955
+ die(`No asset found matching "${target}". Try: tokrepo search ${target}`);
789
956
  }
790
957
 
791
- // If exact title match, use it directly
792
- const exact = searchData.list.find(w => w.title.toLowerCase().includes(target.toLowerCase()));
958
+ // If title contains all query words, prefer it
959
+ const queryWords = normalizedTarget.toLowerCase().split(' ');
960
+ const exact = searchData.list.find(w => {
961
+ const title = w.title.toLowerCase();
962
+ return queryWords.every(word => title.includes(word));
963
+ });
793
964
  const chosen = exact || searchData.list[0];
794
965
 
795
966
  uuid = chosen.uuid;
796
967
  info(`Found: ${C.bold}${chosen.title}${C.reset}`);
797
968
  } catch (e) {
798
- error(`Search failed: ${e.message}`);
969
+ die(`Search failed: ${e.message}`);
799
970
  }
800
971
  }
801
972
 
@@ -808,7 +979,7 @@ Examples:
808
979
  workflow = data.workflow;
809
980
  files = data.workflow.files || [];
810
981
  } catch (e) {
811
- error(`Fetch failed: ${e.message}`);
982
+ die(`Fetch failed: ${e.message}`);
812
983
  }
813
984
 
814
985
  log(`\n ${C.bold}${workflow.title}${C.reset}`);
@@ -842,10 +1013,34 @@ Examples:
842
1013
  }
843
1014
 
844
1015
  if (contents.length === 0) {
845
- error('No installable content found in this asset.');
1016
+ die('No installable content found in this asset.');
846
1017
  }
847
1018
 
848
1019
  log('');
1020
+ const targetTool = normalizeInstallTarget(opts.targetTool);
1021
+
1022
+ if (targetTool === 'gemini') {
1023
+ const destDir = path.join(process.cwd(), '.gemini');
1024
+ if (!fs.existsSync(destDir)) {
1025
+ fs.mkdirSync(destDir, { recursive: true });
1026
+ }
1027
+ const destPath = pickWritablePath(path.join(destDir, 'GEMINI.md'), Boolean(opts.yes));
1028
+ const resolvedDir = path.resolve(destDir);
1029
+ const resolvedDest = path.resolve(destPath);
1030
+ if (!resolvedDest.startsWith(resolvedDir + path.sep) && resolvedDest !== resolvedDir) {
1031
+ die('Install path escaped .gemini directory.');
1032
+ }
1033
+ fs.writeFileSync(destPath, formatGeminiContent(workflow, contents));
1034
+ const relPath = path.relative(process.cwd(), destPath);
1035
+ success(`Installed: ${relPath}`);
1036
+ if (path.basename(destPath) !== 'GEMINI.md') {
1037
+ warn('Gemini CLI automatically reads GEMINI.md. Merge this file if you want it loaded by default.');
1038
+ }
1039
+ log('');
1040
+ success(`1 file installed from ${C.bold}${workflow.title}${C.reset}`);
1041
+ log(` ${C.dim}Source: https://tokrepo.com/en/workflows/${uuid}${C.reset}\n`);
1042
+ return;
1043
+ }
849
1044
 
850
1045
  // Smart install based on asset type
851
1046
  let installed = 0;
@@ -902,7 +1097,7 @@ Examples:
902
1097
  }
903
1098
  }
904
1099
 
905
- const destPath = path.join(destDir, fileName);
1100
+ let destPath = path.join(destDir, fileName);
906
1101
 
907
1102
  // Path traversal guard: ensure resolved path stays inside destDir
908
1103
  if (!path.resolve(destPath).startsWith(path.resolve(destDir) + path.sep) && path.resolve(destPath) !== path.resolve(destDir)) {
@@ -910,10 +1105,7 @@ Examples:
910
1105
  continue;
911
1106
  }
912
1107
 
913
- // Don't overwrite without warning
914
- if (fs.existsSync(destPath)) {
915
- warn(`File exists: ${path.relative(process.cwd(), destPath)} (overwriting)`);
916
- }
1108
+ destPath = pickWritablePath(destPath, Boolean(opts.yes));
917
1109
 
918
1110
  fs.writeFileSync(destPath, item.content);
919
1111
 
@@ -1241,10 +1433,14 @@ function showHelp() {
1241
1433
  log(`
1242
1434
  ${C.bold}tokrepo${C.reset} — AI assets for humans and agents. Like GitHub, for AI experience.
1243
1435
 
1436
+ ${C.dim}You control what gets pushed. Each push uploads only the files you specify.
1437
+ Nothing is shared without your explicit action. Private by default.${C.reset}
1438
+
1244
1439
  ${C.bold}QUICK START${C.reset}
1245
1440
  ${C.cyan}tokrepo search cursor rules${C.reset} # find assets
1246
1441
  ${C.cyan}tokrepo install awesome-cursor-rules${C.reset} # install to your project
1247
- ${C.cyan}tokrepo push --public .${C.reset} # share your own assets
1442
+ ${C.cyan}tokrepo push --private my-skill.md${C.reset} # save privately (only you can see)
1443
+ ${C.cyan}tokrepo push --public my-skill.md${C.reset} # share publicly
1248
1444
 
1249
1445
  ${C.bold}USAGE${C.reset}
1250
1446
  tokrepo <command> [args] [options]
@@ -1270,14 +1466,15 @@ ${C.bold}ACCOUNT${C.reset}
1270
1466
  ${C.cyan}help${C.reset} Show this help
1271
1467
 
1272
1468
  ${C.bold}PUSH OPTIONS${C.reset}
1273
- ${C.cyan}--public${C.reset} Make asset publicly visible (default)
1274
- ${C.cyan}--private${C.reset} Make asset private
1469
+ ${C.cyan}--private${C.reset} Keep asset private only you can see it (recommended for personal assets)
1470
+ ${C.cyan}--public${C.reset} Share asset publicly with the community
1275
1471
  ${C.cyan}--title${C.reset} "..." Set title (auto-detected from README or dir name)
1276
1472
  ${C.cyan}--desc${C.reset} "..." Set description
1277
1473
  ${C.cyan}--tag${C.reset} Skills Add tag (repeatable)
1278
1474
 
1279
1475
  ${C.bold}INSTALL BEHAVIOR${C.reset}
1280
1476
  Skills → .claude/skills/ (if .claude/ exists)
1477
+ Gemini → .gemini/GEMINI.md (with --target gemini)
1281
1478
  Scripts → current dir (chmod +x)
1282
1479
  Configs → project root
1283
1480
  MCP → current dir (.json)
@@ -1286,8 +1483,11 @@ ${C.bold}INSTALL BEHAVIOR${C.reset}
1286
1483
  ${C.bold}EXAMPLES${C.reset}
1287
1484
  tokrepo search "mcp server" # Find MCP configs
1288
1485
  tokrepo install ca000374-f5d8-... # Install by UUID
1289
- tokrepo push --public . # Push current directory
1290
- tokrepo push --public --title "My MCP" . # Push with custom title
1486
+ tokrepo install c4b18aeb --target gemini # Install for Gemini CLI
1487
+ tokrepo push --private my-rules.md # Save one file privately
1488
+ tokrepo push --public skill.md # Share one file publicly
1489
+ tokrepo push --private . # Push current dir as private
1490
+ tokrepo push --public --title "My MCP" . # Push dir publicly with title
1291
1491
 
1292
1492
  ${C.bold}FILE TYPE AUTO-DETECTION${C.reset}
1293
1493
  .sh .py .js .ts .mjs .go .rs → script
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokrepo",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
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"