rigjs 4.0.5 → 4.0.6

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.
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import os from 'os';
3
4
  import print from '../print';
4
5
  import { paths } from './paths';
5
6
 
@@ -10,8 +11,6 @@ const BUNDLED_SKILLS = [
10
11
 
11
12
  /** Find rig's package root by walking up from built/ or lib/. */
12
13
  function findRigRoot(): string | undefined {
13
- // Walk up from `built/index.js` (prod) or `lib/wiki/installSkill.ts` (dev)
14
- // looking for the rigjs package root.
15
14
  let dir = __dirname;
16
15
  for (let i = 0; i < 10; i++) {
17
16
  const pkg = path.join(dir, 'package.json');
@@ -28,8 +27,19 @@ function findRigRoot(): string | undefined {
28
27
  return undefined;
29
28
  }
30
29
 
31
- interface InstallOpts { force?: boolean; }
30
+ interface InstallOpts { force?: boolean; project?: boolean; }
32
31
 
32
+ /**
33
+ * Install bundled skills (rig-wiki, rig-crew) as symlinks.
34
+ *
35
+ * Default (global): link into `~/.claude/skills/<name>/SKILL.md`. Affects
36
+ * every project that uses Claude Code on this machine.
37
+ *
38
+ * `--project` (project-level override for overmind-style monorepos):
39
+ * link into BOTH `<cwd>/.claude/skills/<name>/SKILL.md` (Claude Code) AND
40
+ * `<cwd>/.agents/skills/<name>/SKILL.md` (Codex). When the user is inside
41
+ * the project, the project-local skill files override the global ones.
42
+ */
33
43
  export default function wikiInstallSkill(opts: InstallOpts): void {
34
44
  const root = findRigRoot();
35
45
  if (!root) {
@@ -37,45 +47,77 @@ export default function wikiInstallSkill(opts: InstallOpts): void {
37
47
  process.exit(1);
38
48
  }
39
49
 
40
- if (!fs.existsSync(paths.claudeSkillsDir)) {
41
- print.error(`Claude Code skills dir not found: ${paths.claudeSkillsDir}`);
42
- print.info('install Claude Code first, then re-run `rig wiki install-skill`.');
43
- process.exit(1);
44
- }
50
+ const targetRoots = resolveTargetRoots(opts);
51
+ for (const tr of targetRoots) {
52
+ if (!fs.existsSync(tr.parent)) {
53
+ // Project-level install creates the dirs on demand (parent is <cwd>).
54
+ // Global install requires the dir to already exist — that's how we
55
+ // detect "Claude Code is installed on this machine".
56
+ if (tr.kind === 'global') {
57
+ print.error(`${tr.label} dir not found: ${tr.parent}`);
58
+ print.info(`install ${tr.label} first, then re-run \`rig wiki install-skill\`.`);
59
+ process.exit(1);
60
+ }
61
+ }
45
62
 
46
- for (const skill of BUNDLED_SKILLS) {
47
- const src = path.join(root, skill.file);
48
- if (!fs.existsSync(src)) {
49
- print.warn(`skipping ${skill.name}: ${skill.file} not found inside rigjs install`);
50
- continue;
63
+ for (const skill of BUNDLED_SKILLS) {
64
+ const src = path.join(root, skill.file);
65
+ if (!fs.existsSync(src)) {
66
+ print.warn(`skipping ${skill.name}: ${skill.file} not found inside rigjs install`);
67
+ continue;
68
+ }
69
+ linkSkill(skill.name, src, tr.parent, opts);
51
70
  }
52
- linkSkill(skill.name, src, opts);
53
71
  }
54
72
 
55
- print.info('restart Claude Code to pick up new or updated skills.');
73
+ if (opts.project) {
74
+ print.info('project-local skills will override global ones when this project is the cwd.');
75
+ } else {
76
+ print.info('restart Claude Code to pick up new or updated skills.');
77
+ }
78
+ }
79
+
80
+ interface TargetRoot {
81
+ /** `<parent>/<skill-name>/SKILL.md` is the resulting symlink. */
82
+ parent: string;
83
+ kind: 'global' | 'project';
84
+ label: string;
56
85
  }
57
86
 
58
- function linkSkill(name: string, src: string, opts: InstallOpts): void {
59
- const targetDir = path.join(paths.claudeSkillsDir, name);
87
+ function resolveTargetRoots(opts: InstallOpts): TargetRoot[] {
88
+ if (opts.project) {
89
+ const cwd = process.cwd();
90
+ return [
91
+ { parent: path.join(cwd, '.claude', 'skills'), kind: 'project', label: 'Claude Code project skills' },
92
+ { parent: path.join(cwd, '.agents', 'skills'), kind: 'project', label: 'Codex project skills' },
93
+ ];
94
+ }
95
+ return [
96
+ { parent: paths.claudeSkillsDir, kind: 'global', label: 'Claude Code skills' },
97
+ ];
98
+ }
99
+
100
+ function linkSkill(name: string, src: string, parentDir: string, opts: InstallOpts): void {
101
+ const targetDir = path.join(parentDir, name);
60
102
  const target = path.join(targetDir, 'SKILL.md');
61
103
  fs.mkdirSync(targetDir, { recursive: true });
62
104
 
63
105
  if (fs.existsSync(target) || isBrokenSymlink(target)) {
64
106
  const existing = safeReadlink(target);
65
107
  if (existing === src) {
66
- print.info(`already linked: ${target}`);
108
+ print.info(`already linked: ${shortPath(target)}`);
67
109
  return;
68
110
  }
69
111
  if (!opts.force) {
70
112
  const what = existing ? `symlink -> ${existing}` : 'a regular file';
71
- print.error(`${target} exists as ${what}. Pass --force to replace.`);
113
+ print.error(`${shortPath(target)} exists as ${what}. Pass --force to replace.`);
72
114
  process.exit(1);
73
115
  }
74
116
  fs.rmSync(target, { force: true });
75
117
  }
76
118
 
77
119
  fs.symlinkSync(src, target);
78
- print.succeed(`linked ${target} -> ${src}`);
120
+ print.succeed(`linked ${shortPath(target)} -> ${shortPath(src)}`);
79
121
  }
80
122
 
81
123
  function safeReadlink(p: string): string | null {
@@ -90,3 +132,9 @@ function isBrokenSymlink(p: string): boolean {
90
132
  try { return Boolean(fs.readlinkSync(p)); } catch { return false; }
91
133
  }
92
134
  }
135
+
136
+ function shortPath(p: string): string {
137
+ const home = os.homedir();
138
+ if (p.startsWith(home + path.sep)) return '~' + p.slice(home.length);
139
+ return p;
140
+ }
package/lib/wiki/lint.ts CHANGED
@@ -16,13 +16,13 @@ import fs from 'fs';
16
16
  import path from 'path';
17
17
  import crypto from 'crypto';
18
18
  import print from '../print';
19
- import { loadWikiConfig, resolveWiki, WikiEntry } from './config';
19
+ import { requireVault, WikiEntry } from './config';
20
20
  import { recordLastRun } from './db';
21
21
 
22
- interface LintOpts { wiki?: string; all?: boolean; json?: boolean; }
22
+ interface LintOpts { json?: boolean; }
23
23
 
24
24
  interface PageMeta {
25
- rel: string; // wiki-relative path, e.g. "wiki/sources/foo.md"
25
+ rel: string; // vault-relative path, e.g. "sources/foo.md"
26
26
  slug: string; // filename without ext
27
27
  sub: string; // "sources" | "entities" | "concepts" | "synthesis" | "queries"
28
28
  frontmatter: Record<string, unknown> | null;
@@ -44,34 +44,24 @@ const SOURCE_EXTRA_KEYS = ['source-sha', 'source-path'] as const;
44
44
  const WIKI_SUBDIRS = ['sources', 'entities', 'concepts', 'synthesis', 'queries'] as const;
45
45
 
46
46
  export default async function wikiLint(opts: LintOpts): Promise<void> {
47
- const cfg = loadWikiConfig();
48
- const targets: WikiEntry[] = opts.all
49
- ? cfg.wikis
50
- : [resolveWiki(cfg, opts.wiki)].filter(Boolean) as WikiEntry[];
51
- if (targets.length === 0) {
52
- print.error('no wiki resolved. Pass --wiki <name>, --all, or run from inside a registered project.');
53
- process.exit(1);
54
- }
55
-
56
- let severeFound = false;
57
- const reports: { wiki: string; findings: Findings }[] = [];
58
- for (const t of targets) {
59
- const findings = lintOne(t);
60
- reports.push({ wiki: t.name, findings });
61
- const sev =
62
- findings.missingFrontmatter.length +
63
- findings.missingRequiredKey.length +
64
- findings.brokenWikilinks.length +
65
- findings.missingRawSource.length;
66
- if (sev > 0) severeFound = true;
67
- if (!opts.json) printSummary(t.name, findings);
68
- writeReport(t, findings);
69
- recordLastRun(t.name, 'lint', sev > 0 ? 11 : 0);
70
- }
47
+ const target = requireVault();
48
+ const findings = lintOne(target);
49
+ const sev =
50
+ findings.missingFrontmatter.length +
51
+ findings.missingRequiredKey.length +
52
+ findings.brokenWikilinks.length +
53
+ findings.missingRawSource.length;
54
+ const severeFound = sev > 0;
55
+ if (!opts.json) printSummary(target.name, findings);
56
+ writeReport(target, findings);
57
+ recordLastRun(target.name, 'lint', severeFound ? 11 : 0);
71
58
 
72
59
  if (opts.json) {
73
60
  // eslint-disable-next-line no-console
74
- console.log(JSON.stringify({ ok: !severeFound, code: severeFound ? 11 : 0, data: reports }, null, 2));
61
+ console.log(JSON.stringify({
62
+ ok: !severeFound, code: severeFound ? 11 : 0,
63
+ data: [{ wiki: target.name, findings }],
64
+ }, null, 2));
75
65
  }
76
66
  if (severeFound) process.exit(11);
77
67
  }
@@ -89,7 +79,7 @@ function lintOne(wiki: WikiEntry): Findings {
89
79
 
90
80
  const pages: PageMeta[] = [];
91
81
  for (const sub of WIKI_SUBDIRS) {
92
- const dir = path.join(wiki.path, 'wiki', sub);
82
+ const dir = path.join(wiki.path, sub);
93
83
  if (!fs.existsSync(dir)) continue;
94
84
  for (const name of fs.readdirSync(dir)) {
95
85
  if (!name.endsWith('.md') || name === '.gitkeep') continue;
package/lib/wiki/paths.ts CHANGED
@@ -6,7 +6,6 @@ export const RIG_HOME = process.env.RIG_HOME || path.join(os.homedir(), '.rig');
6
6
  export const paths = {
7
7
  home: RIG_HOME,
8
8
  config: path.join(RIG_HOME, 'config.yml'),
9
- registry: path.join(RIG_HOME, 'wikis.yml'),
10
9
  stateDb: path.join(RIG_HOME, 'state.db'),
11
10
  locks: path.join(RIG_HOME, 'locks'),
12
11
  logs: path.join(RIG_HOME, 'logs'),
package/lib/wiki/query.ts CHANGED
@@ -10,12 +10,11 @@
10
10
 
11
11
  import path from 'path';
12
12
  import print from '../print';
13
- import { loadWikiConfig, resolveWiki, loadRigConfig, WikiEntry } from './config';
13
+ import { requireVault, loadRigConfig, WikiEntry } from './config';
14
14
  import { qmdQuery, QmdHit } from './qmd';
15
15
  import { adapters } from './agent/registry';
16
16
 
17
17
  interface QueryOpts {
18
- wiki?: string;
19
18
  json?: boolean;
20
19
  limit?: number;
21
20
  synth?: boolean;
@@ -28,12 +27,7 @@ export default async function wikiQuery(q: string, opts: QueryOpts): Promise<voi
28
27
  print.error('empty query.');
29
28
  process.exit(1);
30
29
  }
31
- const cfg = loadWikiConfig();
32
- const target = resolveWiki(cfg, opts.wiki);
33
- if (!target) {
34
- print.error('no wiki resolved. Pass --wiki <name> or run from inside a registered project.');
35
- process.exit(1);
36
- }
30
+ const target = requireVault();
37
31
 
38
32
  const limit = Math.max(1, Math.min(50, opts.limit || 10));
39
33
  const hits = await qmdQuery(q, target.name, { limit, rerank: opts.rerank !== false });
@@ -78,13 +72,15 @@ function printHits(wiki: WikiEntry, q: string, hits: QmdHit[]): void {
78
72
  console.log('');
79
73
  }
80
74
 
81
- // "/abs/.../wiki/sources/foo.md" → "foo". Outside wiki/<sub>/ null so the
82
- // caller falls back to printing the literal path.
75
+ // "/abs/<vault>/sources/foo.md" → "foo". For paths outside the page-tree
76
+ // subdirs returns null so the caller falls back to the literal path.
77
+ const PAGE_SUBDIRS = ['sources', 'entities', 'concepts', 'synthesis', 'queries'];
83
78
  function toWikilink(wiki: WikiEntry, filePath: string): string | null {
84
79
  try {
85
80
  const abs = path.isAbsolute(filePath) ? filePath : path.resolve(wiki.path, filePath);
86
- const wikiRoot = path.join(wiki.path, 'wiki') + path.sep;
87
- if (!abs.startsWith(wikiRoot)) return null;
81
+ const rel = path.relative(wiki.path, abs);
82
+ const first = rel.split(path.sep)[0];
83
+ if (!PAGE_SUBDIRS.includes(first)) return null;
88
84
  return path.basename(abs, path.extname(abs));
89
85
  } catch { return null; }
90
86
  }
@@ -13,44 +13,33 @@
13
13
  // - full re-embed (Qwen3-Embedding by default)
14
14
 
15
15
  import print from '../print';
16
- import { loadWikiConfig, resolveWiki, WikiEntry } from './config';
16
+ import { requireVault } from './config';
17
17
  import { getDb, recordLastRun } from './db';
18
18
  import { qmdEmbed, qmdResetStore } from './qmd';
19
19
 
20
- interface RebuildOpts { wiki?: string; all?: boolean; skipEmbed?: boolean; }
20
+ interface RebuildOpts { skipEmbed?: boolean; }
21
21
 
22
22
  export default async function wikiRebuild(opts: RebuildOpts): Promise<void> {
23
- const cfg = loadWikiConfig();
24
- const targets: WikiEntry[] = opts.all
25
- ? cfg.wikis
26
- : [resolveWiki(cfg, opts.wiki)].filter(Boolean) as WikiEntry[];
27
- if (targets.length === 0) {
28
- print.error('no wiki resolved. Pass --wiki <name>, --all, or run from inside a registered project.');
29
- process.exit(1);
30
- }
31
-
23
+ const t = requireVault();
32
24
  const db = getDb();
33
- for (const t of targets) {
34
- print.start(`rebuild: ${t.name}`);
35
- const del = db.prepare('DELETE FROM source_sha WHERE wiki = ?').run(t.name);
36
- print.info(` cleared ${del.changes} source_sha rows for ${t.name}`);
37
25
 
38
- qmdResetStore(t.name);
26
+ print.start(`rebuild: ${t.name}`);
27
+ const del = db.prepare('DELETE FROM source_sha WHERE wiki = ?').run(t.name);
28
+ print.info(` cleared ${del.changes} source_sha rows for ${t.name}`);
39
29
 
40
- if (!opts.skipEmbed) {
41
- const res = await qmdEmbed(t.name, t.path, { force: true });
42
- if (res.ok) print.info(` qmd embed: ${t.name} done`);
43
- else {
44
- print.error(` qmd embed: ${t.name} failed: ${res.stderr.trim()}`);
45
- recordLastRun(t.name, 'rebuild', 1);
46
- process.exitCode = 1;
47
- continue;
48
- }
49
- }
30
+ qmdResetStore(t.name);
50
31
 
51
- recordLastRun(t.name, 'rebuild', 0);
52
- print.succeed(`rebuilt: ${t.name}`);
32
+ if (!opts.skipEmbed) {
33
+ const res = await qmdEmbed(t.name, t.path, { force: true });
34
+ if (res.ok) print.info(` qmd embed: ${t.name} done`);
35
+ else {
36
+ print.error(` qmd embed: ${t.name} failed: ${res.stderr.trim()}`);
37
+ recordLastRun(t.name, 'rebuild', 1);
38
+ process.exit(1);
39
+ }
53
40
  }
54
41
 
42
+ recordLastRun(t.name, 'rebuild', 0);
43
+ print.succeed(`rebuilt: ${t.name}`);
55
44
  print.info('next: run `rig wiki scan` to baseline the new sha index.');
56
45
  }
package/lib/wiki/scan.ts CHANGED
Binary file
@@ -1,30 +1,50 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import os from 'os';
3
4
  import print from '../print';
4
5
  import { paths } from './paths';
5
6
 
6
- export default function wikiUninstallSkill(): void {
7
- const targetDir = path.join(paths.claudeSkillsDir, 'rig-wiki');
8
- const target = path.join(targetDir, 'SKILL.md');
7
+ const BUNDLED_SKILLS = ['rig-wiki', 'rig-crew'];
9
8
 
10
- let removed = false;
11
- if (fs.existsSync(target) || isBrokenSymlink(target)) {
12
- fs.rmSync(target, { force: true });
13
- print.succeed(`removed ${target}`);
14
- removed = true;
15
- }
9
+ interface UninstallOpts { project?: boolean; }
10
+
11
+ /**
12
+ * Remove bundled-skill symlinks. Mirrors `installSkill` flags:
13
+ * default — `~/.claude/skills/<name>/SKILL.md`
14
+ * `--project` — both `<cwd>/.claude/skills/<name>/SKILL.md`
15
+ * and `<cwd>/.agents/skills/<name>/SKILL.md`
16
+ */
17
+ export default function wikiUninstallSkill(opts: UninstallOpts): void {
18
+ const parents = opts.project
19
+ ? [
20
+ path.join(process.cwd(), '.claude', 'skills'),
21
+ path.join(process.cwd(), '.agents', 'skills'),
22
+ ]
23
+ : [paths.claudeSkillsDir];
16
24
 
17
- // Clean the rig-wiki/ dir if we left it empty.
18
- if (fs.existsSync(targetDir)) {
19
- try {
20
- if (fs.readdirSync(targetDir).length === 0) {
21
- fs.rmdirSync(targetDir);
22
- print.info(`removed empty dir ${targetDir}`);
25
+ let removed = 0;
26
+ for (const parent of parents) {
27
+ for (const name of BUNDLED_SKILLS) {
28
+ const targetDir = path.join(parent, name);
29
+ const target = path.join(targetDir, 'SKILL.md');
30
+
31
+ if (fs.existsSync(target) || isBrokenSymlink(target)) {
32
+ fs.rmSync(target, { force: true });
33
+ print.succeed(`removed ${shortPath(target)}`);
34
+ removed++;
23
35
  }
24
- } catch { /* non-fatal */ }
36
+
37
+ if (fs.existsSync(targetDir)) {
38
+ try {
39
+ if (fs.readdirSync(targetDir).length === 0) {
40
+ fs.rmdirSync(targetDir);
41
+ }
42
+ } catch { /* non-fatal */ }
43
+ }
44
+ }
25
45
  }
26
46
 
27
- if (!removed) print.info(`nothing to remove at ${target}`);
47
+ if (removed === 0) print.info('nothing to remove.');
28
48
  }
29
49
 
30
50
  function isBrokenSymlink(p: string): boolean {
@@ -35,3 +55,9 @@ function isBrokenSymlink(p: string): boolean {
35
55
  try { return Boolean(fs.readlinkSync(p)); } catch { return false; }
36
56
  }
37
57
  }
58
+
59
+ function shortPath(p: string): string {
60
+ const home = os.homedir();
61
+ if (p.startsWith(home + path.sep)) return '~' + p.slice(home.length);
62
+ return p;
63
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rigjs",
3
- "version": "4.0.5",
4
- "versionCode": 26052412,
3
+ "version": "4.0.6",
4
+ "versionCode": 26052413,
5
5
  "description": "A multi-repos dev tool based on yarn and git.Rigjs is intended to be the simplest way to develop,share and deliver codes between different developers or different projects.",
6
6
  "keywords": [
7
7
  "modular",
@@ -90,4 +90,63 @@ try {
90
90
  } finally {
91
91
  rmSync(tmpDir, { recursive: true, force: true });
92
92
  }
93
+
94
+ if (status === 0) {
95
+ await syncNpmmirror(pkg.name);
96
+ }
93
97
  process.exit(status);
98
+
99
+ /**
100
+ * Tell registry.npmmirror.com to pull the newly published version from
101
+ * upstream npmjs. Fire the POST kickoff, then poll the sync log a few
102
+ * times — we don't block release on completion, just surface progress.
103
+ *
104
+ * Failures (network, 5xx, timeout) are warnings, not hard errors: the
105
+ * publish itself succeeded; the mirror will catch up on its own.
106
+ */
107
+ async function syncNpmmirror(pkgName) {
108
+ const slug = encodeURIComponent(pkgName);
109
+ const kickoffUrl = `https://registry.npmmirror.com/-/package/${slug}/syncs?sync_upstream=true`;
110
+ process.stdout.write(`\nsyncing ${pkgName} to npmmirror.com...\n`);
111
+
112
+ let logId;
113
+ try {
114
+ const r = await fetch(kickoffUrl, { method: 'PUT' });
115
+ if (!r.ok) {
116
+ process.stderr.write(` kickoff failed (HTTP ${r.status}) — mirror will sync on its own schedule\n`);
117
+ return;
118
+ }
119
+ const body = await r.json();
120
+ logId = body.logId || body.id;
121
+ if (!logId) {
122
+ process.stdout.write(` kicked off (no logId returned, treating as fire-and-forget)\n`);
123
+ return;
124
+ }
125
+ process.stdout.write(` kicked off (logId=${logId})\n`);
126
+ } catch (e) {
127
+ process.stderr.write(` kickoff error: ${e.message} — mirror will sync on its own schedule\n`);
128
+ return;
129
+ }
130
+
131
+ // Poll the sync log: 3s interval, give up after 60s.
132
+ const statusUrl = `https://registry.npmmirror.com/-/package/${slug}/syncs/${logId}`;
133
+ const start = Date.now();
134
+ const TIMEOUT_MS = 60_000;
135
+ while (Date.now() - start < TIMEOUT_MS) {
136
+ await new Promise(r => setTimeout(r, 3000));
137
+ try {
138
+ const r = await fetch(statusUrl);
139
+ if (!r.ok) continue;
140
+ const s = await r.json();
141
+ if (s.syncDone || s.state === 'success') {
142
+ process.stdout.write(` npmmirror sync complete\n`);
143
+ return;
144
+ }
145
+ if (s.state === 'fail') {
146
+ process.stderr.write(` npmmirror sync failed: ${s.error || '(no error message)'}\n`);
147
+ return;
148
+ }
149
+ } catch { /* transient — keep polling */ }
150
+ }
151
+ process.stdout.write(` still in progress after ${TIMEOUT_MS / 1000}s — npmmirror will finish in the background\n`);
152
+ }
package/skills.md CHANGED
@@ -15,22 +15,49 @@ Keep the root `README.md` short: it should link here and to the canonical skill
15
15
 
16
16
  ## Install
17
17
 
18
- Normal install:
18
+ ### Global install (default — affects every project on the machine)
19
19
 
20
20
  ```bash
21
- npm i -g rigjs
21
+ yarn global add rigjs
22
22
  ```
23
23
 
24
- The `postinstall` script links bundled skills into `~/.claude/skills/` when Claude Code is installed.
25
-
26
- Security-conscious install:
24
+ The `postinstall` script links bundled skills into `~/.claude/skills/` (Claude Code's user-level skill directory). If you prefer to skip the postinstall:
27
25
 
28
26
  ```bash
29
- npm i -g rigjs --ignore-scripts
27
+ yarn global add rigjs --ignore-scripts
30
28
  rig wiki install-skill
31
29
  ```
32
30
 
33
- `rig wiki install-skill` installs both bundled skills. The command name remains under `wiki` for backward compatibility.
31
+ ### Project-level install (per-project override, Claude Code + Codex)
32
+
33
+ For "monorepo of work projects" setups — e.g. overmind — you can install the skills **into the project itself** so they live alongside the code and override the global ones whenever the user is inside that project:
34
+
35
+ ```bash
36
+ cd <project>
37
+ rig wiki install-skill --project
38
+ ```
39
+
40
+ This creates symlinks at:
41
+
42
+ - `<project>/.claude/skills/rig-wiki/SKILL.md` → `<rigjs-install>/RIG_WIKI_SKILL.md`
43
+ - `<project>/.claude/skills/rig-crew/SKILL.md` → `<rigjs-install>/RIG_CREW_SKILL.md`
44
+ - `<project>/.agents/skills/rig-wiki/SKILL.md` → (same target, for Codex)
45
+ - `<project>/.agents/skills/rig-crew/SKILL.md` → (same target, for Codex)
46
+
47
+ Both Claude Code (`.claude/skills/`) and Codex (`.agents/skills/`) read from project-local skill dirs when invoked inside the project, so a single `--project` install covers both agents.
48
+
49
+ Project-local skills take precedence over `~/.claude/skills/` while the user is in that project. To remove:
50
+
51
+ ```bash
52
+ cd <project>
53
+ rig wiki uninstall-skill --project
54
+ ```
55
+
56
+ ### Why project-level over global
57
+
58
+ - Pins the skill version to the rigjs install in `node_modules` (or wherever the global rig lives), so the skill the agent sees matches the CLI it's about to call.
59
+ - Lets the project decide which agent gets which skill — committing `.claude/skills/rig-wiki/` to the repo makes the agent behaviour reproducible across machines.
60
+ - Works in CI / sandboxes where there's no home-dir `~/.claude/skills/` to install into.
34
61
 
35
62
  ## Maintenance
36
63
 
package/lib/wiki/list.ts DELETED
@@ -1,69 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import print from '../print';
4
- import { loadWikiConfig, loadRigConfig } from './config';
5
- import { getLastRun } from './db';
6
- import { detectQmd } from './qmd';
7
- import { adapters } from './agent/registry';
8
-
9
- export default async function wikiList(): Promise<void> {
10
- const cfg = loadWikiConfig();
11
- const rig = loadRigConfig();
12
- if (cfg.wikis.length === 0) {
13
- print.info('no wikis registered. Use `rig wiki register [<path>]` to add one.');
14
- } else {
15
- const rows = cfg.wikis.map(w => ({
16
- name: w.name,
17
- path: shortPath(w.path),
18
- pages: countPages(w.path),
19
- lastScan: fmtTs(getLastRun(w.name, 'scan')?.ts),
20
- lastIngest: fmtTs(getLastRun(w.name, 'ingest')?.ts),
21
- lastLint: fmtTs(getLastRun(w.name, 'lint')?.ts),
22
- }));
23
- const header = ['NAME', 'PATH', 'PAGES', 'LAST SCAN', 'LAST INGEST', 'LAST LINT'];
24
- printTable(header, rows.map(r => [r.name, r.path, String(r.pages), r.lastScan, r.lastIngest, r.lastLint]));
25
- }
26
-
27
- const qmd = detectQmd();
28
- const defaultAgent = rig.wiki?.defaultAgent || 'claude';
29
- const agentDetect = await adapters.find(a => a.name === defaultAgent)?.detect();
30
- // eslint-disable-next-line no-console
31
- console.log(`\nagent: ${defaultAgent}${agentDetect?.installed ? ` (${agentDetect.version || 'installed'})` : ' (NOT installed)'}` +
32
- ` qmd: ${qmd.installed ? qmd.version || 'installed' : 'not installed (fallback mode)'}`);
33
- }
34
-
35
- function countPages(wikiDir: string): number {
36
- const wiki = path.join(wikiDir, 'wiki');
37
- if (!fs.existsSync(wiki)) return 0;
38
- let n = 0;
39
- for (const sub of ['sources', 'entities', 'concepts', 'synthesis', 'queries']) {
40
- const d = path.join(wiki, sub);
41
- if (!fs.existsSync(d)) continue;
42
- for (const f of fs.readdirSync(d)) if (f.endsWith('.md') && f !== '.gitkeep') n++;
43
- }
44
- return n;
45
- }
46
-
47
- function shortPath(p: string): string {
48
- const home = process.env.HOME || '';
49
- return home && p.startsWith(home) ? '~' + p.slice(home.length) : p;
50
- }
51
-
52
- function fmtTs(ts?: number): string {
53
- if (!ts) return '—';
54
- const d = new Date(ts);
55
- return d.toISOString().replace('T', ' ').slice(0, 16);
56
- }
57
-
58
- function printTable(header: string[], rows: string[][]): void {
59
- const widths = header.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] || '').length)));
60
- const fmt = (cells: string[]) => cells.map((c, i) => (c || '').padEnd(widths[i])).join(' ');
61
- // eslint-disable-next-line no-console
62
- console.log(fmt(header));
63
- // eslint-disable-next-line no-console
64
- console.log(widths.map(w => '-'.repeat(w)).join(' '));
65
- for (const r of rows) {
66
- // eslint-disable-next-line no-console
67
- console.log(fmt(r));
68
- }
69
- }