rigjs 4.0.5 → 4.0.7

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/lib/wiki/init.ts CHANGED
@@ -1,8 +1,31 @@
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 { guardPath, refusalMessage } from './pathGuard';
5
- import { saveVaultConfig, VaultConfig } from './config';
6
+ import { saveVaultConfig, loadVaultConfig, VaultConfig } from './config';
7
+ import { vaultConfigPath } from './paths';
8
+
9
+ /**
10
+ * `rig wiki init <scope>`
11
+ *
12
+ * The user runs this from a project root. The CWD is treated as the project
13
+ * (the conceptual "vault"); `<scope>` names a data subdir under it that the
14
+ * wiki should ingest from (e.g. `personal` for `<project>/personal/`).
15
+ *
16
+ * Vault metadata always lives at `<CWD>/rig-wiki/` (fixed name). The scope
17
+ * is recorded in `<CWD>/rig-wiki/.rig/config.yml` as the scan root, so the
18
+ * user's data dir stays untouched.
19
+ *
20
+ * $ cd overmind
21
+ * $ rig wiki init personal
22
+ * ⇒ creates overmind/rig-wiki/ with templates + .rig/config.yml
23
+ * ⇒ config.yml.root = "../personal", config.yml.name = "personal"
24
+ *
25
+ * Idempotent for the same scope. Errors if rig-wiki/ already exists with a
26
+ * different scope (manual `.rig/config.yml` edit required to switch).
27
+ */
28
+ const VAULT_DIRNAME = 'rig-wiki';
6
29
 
7
30
  const PURPOSE_TMPL = `# Purpose
8
31
 
@@ -19,7 +42,8 @@ const SCHEMA_TMPL = `# Schema
19
42
 
20
43
  ## Layers
21
44
  - raw/, purpose.md, schema.md: read-only for LLM
22
- - wiki/, index.md, overview.md, log.md, reviews.md: LLM is sole author
45
+ - index.md, overview.md, log.md, reviews.md, sources/, entities/, concepts/,
46
+ synthesis/, queries/: LLM is sole author
23
47
 
24
48
  ## Page types
25
49
  - sources/<slug>.md : 1-source summary
@@ -37,7 +61,7 @@ const SCHEMA_TMPL = `# Schema
37
61
  - last-updated: <ISO>
38
62
 
39
63
  ## Naming
40
- - kebab-case; no spaces; no dates in wiki/ filenames
64
+ - kebab-case; no spaces; no dates in page filenames
41
65
  - raw/ filenames keep YYYY-MM-DD prefix
42
66
 
43
67
  ## Linking
@@ -56,12 +80,6 @@ const SCHEMA_TMPL = `# Schema
56
80
 
57
81
  const SUBDIRS = ['sources', 'entities', 'concepts', 'synthesis', 'queries'];
58
82
 
59
- // What lives inside the wiki dir but must not enter git / Obsidian Sync:
60
- // - qmd's project-local vector cache (sqlite-vec, non-deterministic, rebuilds
61
- // locally with `rig wiki index` / `rig wiki rebuild`)
62
- // - lint reports (auto-regenerated)
63
- // - daemon proposal diffs (transient, per-machine)
64
- // - editor scratch
65
83
  const GITIGNORE_TMPL = `# rig wiki — local-only artifacts (do not commit)
66
84
  # qmd vector cache (sqlite-vec, machine-specific, rebuildable)
67
85
  .qmd/index.sqlite*
@@ -76,59 +94,99 @@ proposals/
76
94
  *.swp
77
95
  `;
78
96
 
79
- const DEFAULT_VAULT_CONFIG = (vaultBasename: string): VaultConfig => ({
80
- name: vaultBasename,
81
- root: '..',
82
- include: ['**/*.md'],
83
- exclude: [`${vaultBasename}/**`, 'node_modules/**', '.git/**'],
84
- schedule: { scan: '0 */6 * * *', lint: '0 3 * * *', ingest: null },
85
- ingestRules: [{ match: 'raw/**/*.md', mode: 'auto-on-new' }],
86
- });
87
-
88
- export default function wikiInit(givenPath?: string): void {
89
- if (!givenPath || !givenPath.trim()) {
90
- print.error('rig wiki init requires a target subdirectory.');
91
- print.info('usage: rig wiki init <subdir> (e.g. `rig wiki init knowledge` / `rig wiki init harness/llm-wiki`)');
92
- print.info('refusing to default to CWD that would litter the project root with wiki templates.');
97
+ /**
98
+ * Defaults for a freshly-scoped vault. The user can edit
99
+ * `<vault>/.rig/config.yml` afterwards.
100
+ *
101
+ * Hidden directories (segments starting with `.`) and `.gitignore`'d files
102
+ * are skipped automatically by the scanner no need to list them here.
103
+ */
104
+ function defaultVaultConfig(scope: string, rootRel: string): VaultConfig {
105
+ return {
106
+ name: scope,
107
+ root: rootRel,
108
+ include: ['**/*.md'],
109
+ exclude: [],
110
+ schedule: { scan: '0 */6 * * *', lint: '0 3 * * *', ingest: null },
111
+ ingestRules: [{ match: 'raw/**/*.md', mode: 'auto-on-new' }],
112
+ };
113
+ }
114
+
115
+ export default function wikiInit(scope?: string): void {
116
+ if (!scope || !scope.trim()) {
117
+ print.error('rig wiki init requires a scope.');
118
+ print.info('usage: rig wiki init <scope> (e.g. `rig wiki init personal` to ingest from ./personal/)');
119
+ print.info(`<scope> is an existing data subdir of the project. Vault metadata is auto-created at ./${VAULT_DIRNAME}/.`);
120
+ process.exit(1);
121
+ }
122
+
123
+ const cwd = process.cwd();
124
+ const vaultDir = path.join(cwd, VAULT_DIRNAME);
125
+ const scopeAbs = path.resolve(cwd, scope);
126
+
127
+ // The scope must already exist — pointing the wiki at a missing dir would
128
+ // hide what is almost certainly a typo.
129
+ if (!fs.existsSync(scopeAbs) || !fs.statSync(scopeAbs).isDirectory()) {
130
+ print.error(`scope dir not found: ${scope}`);
131
+ print.info(`expected an existing data subdir at ${shortPath(scopeAbs)}`);
93
132
  process.exit(1);
94
133
  }
95
- const root = path.resolve(givenPath);
96
- const guard = guardPath(root, process.cwd());
134
+ // The scope can't be (or contain) the vault dir itself.
135
+ if (scopeAbs === vaultDir || vaultDir.startsWith(scopeAbs + path.sep)) {
136
+ print.error(`scope cannot be or contain the vault dir (${VAULT_DIRNAME}/).`);
137
+ process.exit(1);
138
+ }
139
+
140
+ const guard = guardPath(vaultDir, cwd);
97
141
  if (!guard.ok) {
98
- print.error('refusing to initialize a wiki at a hidden or gitignored path.');
142
+ print.error(`refusing to initialize the vault at a hidden or gitignored path.`);
99
143
  // eslint-disable-next-line no-console
100
- console.error(refusalMessage(root, guard));
144
+ console.error(refusalMessage(vaultDir, guard));
101
145
  process.exit(1);
102
146
  }
103
- fs.mkdirSync(root, { recursive: true });
104
147
 
105
- writeIfMissing(path.join(root, 'purpose.md'), PURPOSE_TMPL);
106
- writeIfMissing(path.join(root, 'schema.md'), SCHEMA_TMPL);
107
- writeIfMissing(path.join(root, 'index.md'), '# Index\n');
108
- writeIfMissing(path.join(root, 'overview.md'), '# Overview\n');
109
- writeIfMissing(path.join(root, 'log.md'), '# Log\n');
110
- writeIfMissing(path.join(root, 'reviews.md'), '# Reviews\n');
111
- writeIfMissing(path.join(root, '.gitignore'), GITIGNORE_TMPL);
148
+ // If the vault already has a config, it must already be scoped to the
149
+ // same data dir — otherwise the user is trying to re-target an existing
150
+ // vault, which we won't do silently. Manual config edit only.
151
+ const cfgFile = vaultConfigPath(vaultDir);
152
+ if (fs.existsSync(cfgFile)) {
153
+ const existing = loadVaultConfig(vaultDir);
154
+ const existingRootAbs = existing?.root
155
+ ? path.resolve(vaultDir, existing.root)
156
+ : path.dirname(vaultDir);
157
+ if (existingRootAbs !== scopeAbs) {
158
+ print.error(`vault already initialized at ${shortPath(vaultDir)} for scope "${existing?.name ?? '?'}" (root: ${existing?.root ?? '..'}).`);
159
+ print.info(`to switch scopes, edit ${shortPath(cfgFile)} (name + root) by hand.`);
160
+ process.exit(1);
161
+ }
162
+ }
163
+
164
+ fs.mkdirSync(vaultDir, { recursive: true });
165
+ writeIfMissing(path.join(vaultDir, 'purpose.md'), PURPOSE_TMPL);
166
+ writeIfMissing(path.join(vaultDir, 'schema.md'), SCHEMA_TMPL);
167
+ writeIfMissing(path.join(vaultDir, 'index.md'), '# Index\n');
168
+ writeIfMissing(path.join(vaultDir, 'overview.md'), '# Overview\n');
169
+ writeIfMissing(path.join(vaultDir, 'log.md'), '# Log\n');
170
+ writeIfMissing(path.join(vaultDir, 'reviews.md'), '# Reviews\n');
171
+ writeIfMissing(path.join(vaultDir, '.gitignore'), GITIGNORE_TMPL);
112
172
 
113
- fs.mkdirSync(path.join(root, 'raw'), { recursive: true });
114
- writeIfMissing(path.join(root, 'raw', '.gitkeep'), '');
173
+ fs.mkdirSync(path.join(vaultDir, 'raw'), { recursive: true });
174
+ writeIfMissing(path.join(vaultDir, 'raw', '.gitkeep'), '');
115
175
 
116
176
  for (const sub of SUBDIRS) {
117
- const d = path.join(root, 'wiki', sub);
177
+ const d = path.join(vaultDir, sub);
118
178
  fs.mkdirSync(d, { recursive: true });
119
179
  writeIfMissing(path.join(d, '.gitkeep'), '');
120
180
  }
121
181
 
122
- // Seed `<vault>/.rig/config.yml` with sensible defaults. Idempotent: if the
123
- // user has already authored one, leave it alone.
124
- const vaultCfgFile = path.join(root, '.rig', 'config.yml');
125
- if (!fs.existsSync(vaultCfgFile)) {
126
- saveVaultConfig(root, DEFAULT_VAULT_CONFIG(path.basename(root)));
182
+ if (!fs.existsSync(cfgFile)) {
183
+ const rootRel = path.relative(vaultDir, scopeAbs);
184
+ saveVaultConfig(vaultDir, defaultVaultConfig(scope, rootRel));
127
185
  }
128
186
 
129
- print.succeed(`wiki initialized at ${root}`);
130
- print.info(`next: edit purpose.md + schema.md (and .rig/config.yml if scope differs from defaults), then \`rig wiki register ${shortPath(root)}\``);
131
- print.info('on a new device, after cloning, run `rig wiki rebuild` to refresh local caches.');
187
+ print.succeed(`vault initialized at ${shortPath(vaultDir)} (scope: ${scope})`);
188
+ print.info(`next: edit ${shortPath(path.join(vaultDir, 'purpose.md'))} to describe what this wiki is for.`);
189
+ print.info(`then run \`rig wiki scan\` from anywhere inside ${shortPath(cwd)} to see what will be ingested.`);
132
190
  }
133
191
 
134
192
  function writeIfMissing(file: string, content: string) {
@@ -136,7 +194,8 @@ function writeIfMissing(file: string, content: string) {
136
194
  fs.writeFileSync(file, content, 'utf8');
137
195
  }
138
196
 
139
- function shortPath(p: string) {
140
- const home = process.env.HOME || '';
141
- return home && p.startsWith(home) ? '~' + p.slice(home.length) : p;
197
+ function shortPath(p: string): string {
198
+ const home = os.homedir();
199
+ if (p.startsWith(home + path.sep)) return '~' + p.slice(home.length);
200
+ return p;
142
201
  }
@@ -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.7",
4
+ "versionCode": 26052414,
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",