rigjs 3.0.33 → 4.0.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 (99) hide show
  1. package/.claude/skills/rig-wiki/SKILL.md +104 -0
  2. package/.claude-plugin/plugin.json +14 -0
  3. package/README.md +18 -1
  4. package/README_CN.md +17 -1
  5. package/RIG_CREW_SKILL.md +274 -0
  6. package/RIG_WIKI_SKILL.md +104 -0
  7. package/bin/rig.js +0 -0
  8. package/built/index.js +376 -299
  9. package/doc/architecture/README.md +139 -0
  10. package/doc/architecture/agents.md +180 -0
  11. package/doc/architecture/fc.md +17 -0
  12. package/doc/architecture/wiki.md +278 -0
  13. package/lib/crew/ask.ts +24 -0
  14. package/lib/crew/board.ts +123 -0
  15. package/lib/crew/config.ts +109 -0
  16. package/lib/crew/doctor.ts +40 -0
  17. package/lib/crew/inbox.ts +29 -0
  18. package/lib/crew/index.ts +108 -0
  19. package/lib/crew/init.ts +113 -0
  20. package/lib/crew/paths.ts +13 -0
  21. package/lib/crew/project.ts +84 -0
  22. package/lib/crew/role.ts +121 -0
  23. package/lib/crew/roleCommand.ts +150 -0
  24. package/lib/crew/state.ts +19 -0
  25. package/lib/crew/status.ts +27 -0
  26. package/lib/crew/stub.ts +9 -0
  27. package/lib/crew/sync.ts +15 -0
  28. package/lib/crew/task.ts +92 -0
  29. package/lib/crew/vault.ts +266 -0
  30. package/lib/installLocal.ts +189 -0
  31. package/lib/rig/index.ts +26 -3
  32. package/lib/wiki/README.md +79 -0
  33. package/lib/wiki/agent/claude.ts +65 -0
  34. package/lib/wiki/agent/codex.ts +22 -0
  35. package/lib/wiki/agent/index.ts +11 -0
  36. package/lib/wiki/agent/list.ts +27 -0
  37. package/lib/wiki/agent/pi.ts +21 -0
  38. package/lib/wiki/agent/registry.ts +16 -0
  39. package/lib/wiki/agent/types.ts +37 -0
  40. package/lib/wiki/agent/use.ts +21 -0
  41. package/lib/wiki/config.ts +99 -0
  42. package/lib/wiki/daemon/index.ts +25 -0
  43. package/lib/wiki/daemon/install.ts +69 -0
  44. package/lib/wiki/daemon/logs.ts +16 -0
  45. package/lib/wiki/daemon/runner.ts +42 -0
  46. package/lib/wiki/daemon/start.ts +20 -0
  47. package/lib/wiki/daemon/status.ts +23 -0
  48. package/lib/wiki/daemon/stop.ts +16 -0
  49. package/lib/wiki/daemon/uninstall.ts +17 -0
  50. package/lib/wiki/db.ts +71 -0
  51. package/lib/wiki/fetch.ts +206 -0
  52. package/lib/wiki/index.ts +106 -0
  53. package/lib/wiki/indexCmd.ts +23 -0
  54. package/lib/wiki/ingest.ts +271 -0
  55. package/lib/wiki/init.ts +125 -0
  56. package/lib/wiki/installSkill.ts +92 -0
  57. package/lib/wiki/lint.ts +252 -0
  58. package/lib/wiki/list.ts +69 -0
  59. package/lib/wiki/pathGuard.ts +87 -0
  60. package/lib/wiki/paths.ts +29 -0
  61. package/lib/wiki/platform.ts +8 -0
  62. package/lib/wiki/qmd.ts +205 -0
  63. package/lib/wiki/query.ts +144 -0
  64. package/lib/wiki/rebuild.ts +56 -0
  65. package/lib/wiki/register.ts +94 -0
  66. package/lib/wiki/scan.ts +0 -0
  67. package/lib/wiki/uninstallSkill.ts +37 -0
  68. package/lib/wiki/unregister.ts +16 -0
  69. package/package.json +36 -6
  70. package/scripts/postinstall.mjs +108 -0
  71. package/scripts/publish.mjs +93 -0
  72. package/scripts/sync-skill.mjs +33 -0
  73. package/scripts/version-code.mjs +86 -0
  74. package/skills.md +54 -0
  75. package/.github/workflows/npm-publish.yml +0 -22
  76. package/demo/.env.oem1 +0 -4
  77. package/demo/.env.oem2 +0 -4
  78. package/demo/babel.config.js +0 -5
  79. package/demo/env.rig.json5 +0 -8
  80. package/demo/jsconfig.json +0 -19
  81. package/demo/package.json +0 -59
  82. package/demo/package.rig.json5 +0 -78
  83. package/demo/public/favicon.ico +0 -0
  84. package/demo/public/index.html +0 -17
  85. package/demo/rig_dev/.gitkeep +0 -0
  86. package/demo/rig_helper.d.ts +0 -4
  87. package/demo/rig_helper.js +0 -10
  88. package/demo/rigs/.gitkeep +0 -0
  89. package/demo/src/App.vue +0 -34
  90. package/demo/src/assets/logo.png +0 -0
  91. package/demo/src/components/HelloWorld.vue +0 -58
  92. package/demo/src/main.js +0 -8
  93. package/demo/vue.config.js +0 -8
  94. package/demo/yarn.lock +0 -6312
  95. package/develop.png +0 -0
  96. package/jest/test.rig.json5 +0 -14
  97. package/jest.config.ts +0 -16
  98. package/production.png +0 -0
  99. package/tsconfig.json +0 -53
@@ -0,0 +1,205 @@
1
+ // qmd integration — in-process Node SDK (`@tobilu/qmd`).
2
+ //
3
+ // rig wiki is vector-only: indexing always embeds, queries are pure
4
+ // `searchVector` followed by a Qwen3-Reranker pass. No BM25, no query
5
+ // expansion, no language-specific tokenizer headaches.
6
+ //
7
+ // Two models, both mirrored on the personal CDN for zero-config global
8
+ // acceleration (China + worldwide via Aliyun PoPs):
9
+ // - Embed: Qwen3-Embedding-0.6B (~610MB, sets QMD_EMBED_MODEL)
10
+ // - Rerank: Qwen3-Reranker-0.6B (~610MB, sets QMD_RERANK_MODEL)
11
+ //
12
+ // Per-wiki SQLite DB lives at `~/.rig/cache/qmd/<wiki>.sqlite`. Model GGUFs
13
+ // cache in `~/.cache/qmd/models/` (hardcoded inside qmd).
14
+ //
15
+ // Concurrency note: qmd's `setConfigSource` is module-global; serialize
16
+ // store lifetimes by opening + closing inside each call.
17
+
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import { paths } from './paths';
21
+
22
+ // CDN-backed defaults. node-llama-cpp's resolveModelFile accepts https://.
23
+ // qmd's `isQwen3EmbeddingModel` matches "Qwen.*Embed" in the URI, so the
24
+ // Qwen3 query-instruction template (Instruct:/Query:) is auto-applied.
25
+ const DEFAULT_EMBED_MODEL_URL = 'https://assets.terncloud.com/rig/models/Qwen3-Embedding-0.6B-Q8_0.gguf';
26
+ const DEFAULT_RERANK_MODEL_URL = 'https://assets.terncloud.com/rig/models/qwen3-reranker-0.6b-q8_0.gguf';
27
+
28
+ // Apply rig defaults unless the user pinned different ones. Runs at module
29
+ // load so every qmd call inherits without the caller worrying.
30
+ if (!process.env.QMD_EMBED_MODEL) process.env.QMD_EMBED_MODEL = DEFAULT_EMBED_MODEL_URL;
31
+ if (!process.env.QMD_RERANK_MODEL) process.env.QMD_RERANK_MODEL = DEFAULT_RERANK_MODEL_URL;
32
+
33
+ export interface QmdInfo {
34
+ installed: true;
35
+ version: string;
36
+ bundled: true;
37
+ embedModel: string;
38
+ rerankModel: string;
39
+ }
40
+
41
+ let infoCache: QmdInfo | null = null;
42
+
43
+ export function detectQmd(): QmdInfo {
44
+ if (infoCache) return infoCache;
45
+ infoCache = {
46
+ installed: true,
47
+ version: qmdVersion(),
48
+ bundled: true,
49
+ embedModel: process.env.QMD_EMBED_MODEL || DEFAULT_EMBED_MODEL_URL,
50
+ rerankModel: process.env.QMD_RERANK_MODEL || DEFAULT_RERANK_MODEL_URL,
51
+ };
52
+ return infoCache;
53
+ }
54
+
55
+ // qmd's `exports` field blocks `require.resolve('@tobilu/qmd')`; walk up
56
+ // from __dirname looking for node_modules/@tobilu/qmd/package.json instead.
57
+ function qmdVersion(): string {
58
+ try {
59
+ let dir = __dirname;
60
+ for (let i = 0; i < 8; i++) {
61
+ const p = path.join(dir, 'node_modules', '@tobilu', 'qmd', 'package.json');
62
+ if (fs.existsSync(p)) {
63
+ const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
64
+ if (pkg.name === '@tobilu/qmd' && typeof pkg.version === 'string') return pkg.version;
65
+ }
66
+ const parent = path.dirname(dir);
67
+ if (parent === dir) break;
68
+ dir = parent;
69
+ }
70
+ } catch { /* fall through */ }
71
+ return 'unknown';
72
+ }
73
+
74
+ function dbPathFor(wikiName: string): string {
75
+ const dir = path.join(paths.cache, 'qmd');
76
+ fs.mkdirSync(dir, { recursive: true });
77
+ return path.join(dir, `${wikiName}.sqlite`);
78
+ }
79
+
80
+ async function loadQmd(): Promise<{ createStore: any }> {
81
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
+ const m: any = await import('@tobilu/qmd');
83
+ return { createStore: m.createStore };
84
+ }
85
+
86
+ /**
87
+ * Embed a wiki dir. Incremental by default; pass `{ force: true }` to
88
+ * re-embed everything (e.g. after switching the embed model).
89
+ */
90
+ export async function qmdEmbed(
91
+ wikiName: string,
92
+ dir: string,
93
+ opts: { force?: boolean } = {}
94
+ ): Promise<{ ok: boolean; stderr: string }> {
95
+ try {
96
+ const { createStore } = await loadQmd();
97
+ const store = await createStore({
98
+ dbPath: dbPathFor(wikiName),
99
+ config: {
100
+ collections: { [wikiName]: { path: dir, pattern: '**/*.md' } },
101
+ },
102
+ });
103
+ try {
104
+ await store.update({});
105
+ await store.embed({ chunkStrategy: 'auto', force: !!opts.force });
106
+ } finally {
107
+ await store.close();
108
+ }
109
+ return { ok: true, stderr: '' };
110
+ } catch (e) {
111
+ return { ok: false, stderr: errMsg(e) };
112
+ }
113
+ }
114
+
115
+ export interface QmdHit {
116
+ file: string;
117
+ displayPath?: string;
118
+ title?: string;
119
+ body?: string;
120
+ score: number;
121
+ rerankScore?: number;
122
+ }
123
+
124
+ /**
125
+ * Pure vector search + Qwen3 reranker.
126
+ *
127
+ * Pipeline:
128
+ * 1. searchVector(top 40, dedup'd by docid via qmd internals)
129
+ * 2. Store.rerank() against the candidate set's chunk bodies
130
+ * 3. Sort by rerank score, return top-k
131
+ *
132
+ * Set `{ rerank: false }` to skip step 2 (faster, no reranker model load).
133
+ */
134
+ export async function qmdQuery(
135
+ q: string,
136
+ wikiName: string,
137
+ opts: { limit?: number; candidateLimit?: number; rerank?: boolean } = {}
138
+ ): Promise<QmdHit[] | null> {
139
+ const limit = opts.limit ?? 10;
140
+ const candidateLimit = Math.max(limit, opts.candidateLimit ?? 40);
141
+ const doRerank = opts.rerank !== false; // default ON
142
+
143
+ try {
144
+ const { createStore } = await loadQmd();
145
+ const store = await createStore({ dbPath: dbPathFor(wikiName) });
146
+ try {
147
+ const raw = await store.searchVector(q, { limit: candidateLimit, collection: wikiName });
148
+ const candidates: QmdHit[] = Array.isArray(raw) ? raw.map(normalizeHit) : [];
149
+ if (!doRerank || candidates.length === 0) return candidates.slice(0, limit);
150
+
151
+ const docs = candidates
152
+ .filter(c => c.file && (c.body || ''))
153
+ .map(c => ({ file: c.file, text: c.body || '' }));
154
+ if (docs.length === 0) return candidates.slice(0, limit);
155
+
156
+ // qmd 2.5.x exposes `rerank` only on store.internal (not on the public
157
+ // QMDStore). store.search(opts) does include reranking but also runs
158
+ // BM25 + query expansion, which we want to avoid.
159
+ const reranker = store.internal && store.internal.rerank;
160
+ if (typeof reranker !== 'function') {
161
+ // No standalone rerank available — return vector hits as-is.
162
+ return candidates.slice(0, limit);
163
+ }
164
+ const ranked: { file: string; score: number }[] = await reranker(q, docs);
165
+ const scoreByFile = new Map(ranked.map(r => [r.file, r.score]));
166
+ const merged = candidates.map(c => ({
167
+ ...c,
168
+ rerankScore: scoreByFile.get(c.file) ?? 0,
169
+ }));
170
+ merged.sort((a, b) => (b.rerankScore ?? 0) - (a.rerankScore ?? 0));
171
+ return merged.slice(0, limit);
172
+ } finally {
173
+ await store.close();
174
+ }
175
+ } catch (e) {
176
+ // eslint-disable-next-line no-console
177
+ console.error(`qmd query error: ${errMsg(e)}`);
178
+ return null;
179
+ }
180
+ }
181
+
182
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
183
+ function normalizeHit(h: any): QmdHit {
184
+ return {
185
+ file: h.file ?? h.displayPath ?? '',
186
+ displayPath: h.displayPath,
187
+ title: h.title,
188
+ body: h.bestChunk ?? h.body ?? '',
189
+ score: typeof h.score === 'number' ? h.score : 0,
190
+ };
191
+ }
192
+
193
+ /** Wipe the per-wiki SQLite store on disk. Caller should then qmdEmbed. */
194
+ export function qmdResetStore(wikiName: string): void {
195
+ const p = dbPathFor(wikiName);
196
+ for (const suffix of ['', '-wal', '-shm']) {
197
+ const f = p + suffix;
198
+ if (fs.existsSync(f)) fs.rmSync(f, { force: true });
199
+ }
200
+ }
201
+
202
+ function errMsg(e: unknown): string {
203
+ if (e && typeof e === 'object' && 'message' in e) return String((e as { message: unknown }).message);
204
+ return String(e);
205
+ }
@@ -0,0 +1,144 @@
1
+ // `rig wiki query` — vector retrieval over a registered wiki.
2
+ //
3
+ // Pipeline: Qwen3-Embedding-0.6B (sqlite-vec) → Qwen3-Reranker-0.6B → top-k.
4
+ // No BM25, no query expansion. Cross-lingual (Chinese ↔ English) works
5
+ // because both Qwen3 models are multilingual.
6
+ //
7
+ // Default output is human-readable; --json emits the raw payload. --synth
8
+ // invokes the Claude adapter to write a short answer paragraph with
9
+ // [[wikilink]] citations.
10
+
11
+ import path from 'path';
12
+ import print from '../print';
13
+ import { loadWikiConfig, resolveWiki, loadRigConfig, WikiEntry } from './config';
14
+ import { qmdQuery, QmdHit } from './qmd';
15
+ import { adapters } from './agent/registry';
16
+
17
+ interface QueryOpts {
18
+ wiki?: string;
19
+ json?: boolean;
20
+ limit?: number;
21
+ synth?: boolean;
22
+ // Commander resolves `--no-rerank` → `opts.rerank: false`. Default true.
23
+ rerank?: boolean;
24
+ }
25
+
26
+ export default async function wikiQuery(q: string, opts: QueryOpts): Promise<void> {
27
+ if (!q || !q.trim()) {
28
+ print.error('empty query.');
29
+ process.exit(1);
30
+ }
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
+ }
37
+
38
+ const limit = Math.max(1, Math.min(50, opts.limit || 10));
39
+ const hits = await qmdQuery(q, target.name, { limit, rerank: opts.rerank !== false });
40
+ if (hits === null) {
41
+ print.error('qmd query failed. Run `rig wiki index` first to (re)build the vector store.');
42
+ process.exit(1);
43
+ }
44
+
45
+ if (opts.json) {
46
+ // eslint-disable-next-line no-console
47
+ console.log(JSON.stringify({ ok: true, code: 0, data: { query: q, hits } }, null, 2));
48
+ return;
49
+ }
50
+
51
+ printHits(target, q, hits);
52
+ if (opts.synth) await synthesize(target, q, hits);
53
+ }
54
+
55
+ function printHits(wiki: WikiEntry, q: string, hits: QmdHit[]): void {
56
+ print.info(`query: ${q}`);
57
+ if (hits.length === 0) {
58
+ print.warn('no hits.');
59
+ return;
60
+ }
61
+ // eslint-disable-next-line no-console
62
+ console.log('');
63
+ hits.forEach((h, i) => {
64
+ const filePath = h.file || h.displayPath || '<unknown>';
65
+ const wlink = toWikilink(wiki, filePath);
66
+ const score = typeof h.rerankScore === 'number' ? h.rerankScore : h.score;
67
+ const scoreStr = typeof score === 'number' ? score.toFixed(4) : '—';
68
+ const head = wlink ? `[[${wlink}]]` : filePath;
69
+ // eslint-disable-next-line no-console
70
+ console.log(`${String(i + 1).padStart(2)}. ${head} (score=${scoreStr})`);
71
+ const snippet = (h.body || '').trim().replace(/\s+/g, ' ').slice(0, 220);
72
+ if (snippet) {
73
+ // eslint-disable-next-line no-console
74
+ console.log(` ${snippet}${snippet.length === 220 ? '…' : ''}`);
75
+ }
76
+ });
77
+ // eslint-disable-next-line no-console
78
+ console.log('');
79
+ }
80
+
81
+ // "/abs/.../wiki/sources/foo.md" → "foo". Outside wiki/<sub>/ → null so the
82
+ // caller falls back to printing the literal path.
83
+ function toWikilink(wiki: WikiEntry, filePath: string): string | null {
84
+ try {
85
+ 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;
88
+ return path.basename(abs, path.extname(abs));
89
+ } catch { return null; }
90
+ }
91
+
92
+ async function synthesize(wiki: WikiEntry, q: string, hits: QmdHit[]): Promise<void> {
93
+ const rig = loadRigConfig();
94
+ const which = rig.wiki?.defaultAgent || 'claude';
95
+ const adapter = adapters.find(a => a.name === which);
96
+ if (!adapter) {
97
+ print.warn(`no agent adapter named ${which}; skipping synthesis.`);
98
+ return;
99
+ }
100
+ const detect = await adapter.detect();
101
+ if (!detect.installed) {
102
+ print.warn(`${which} not installed on PATH; skipping synthesis.`);
103
+ return;
104
+ }
105
+
106
+ const ctx = hits.slice(0, 8).map((h, i) => {
107
+ const filePath = h.file || h.displayPath || '<unknown>';
108
+ const wlink = toWikilink(wiki, filePath);
109
+ const cite = wlink ? `[[${wlink}]]` : filePath;
110
+ const body = (h.body || '').trim().slice(0, 1200);
111
+ return `## hit ${i + 1} ${cite}\n${body}`;
112
+ }).join('\n\n');
113
+
114
+ const prompt = [
115
+ `You are answering a question about a personal wiki.`,
116
+ `Question: ${q}`,
117
+ ``,
118
+ `Top retrieval results follow. Synthesize a concise answer (≤ 6 sentences)`,
119
+ `that cites the hits using [[wikilink]] format inline. If the hits don't`,
120
+ `support an answer, say so. Do NOT write to any files. Output text only.`,
121
+ ``,
122
+ ctx,
123
+ ].join('\n');
124
+
125
+ print.start(`${which} synthesize`);
126
+ const res = await adapter.run({
127
+ prompt,
128
+ cwd: wiki.path,
129
+ allowWrite: false,
130
+ tools: [],
131
+ timeoutMs: 5 * 60 * 1000,
132
+ });
133
+ if (!res.ok) {
134
+ print.error(`${which} failed (code ${res.exitCode})${res.stderr ? `: ${res.stderr.trim().slice(0, 300)}` : ''}`);
135
+ return;
136
+ }
137
+ print.succeed(`${which} answer:`);
138
+ // eslint-disable-next-line no-console
139
+ console.log('');
140
+ // eslint-disable-next-line no-console
141
+ console.log(res.stdout.trim());
142
+ // eslint-disable-next-line no-console
143
+ console.log('');
144
+ }
@@ -0,0 +1,56 @@
1
+ // `rig wiki rebuild` — refresh local-only caches for a wiki.
2
+ //
3
+ // Use cases:
4
+ // 1. New device: source markdown is checked out, but ~/.rig/state.db and
5
+ // ~/.rig/cache/qmd/<wiki>.sqlite are empty. Rebuild populates both.
6
+ // 2. Switched embedding model: old vectors are now meaningless. Rebuild
7
+ // re-embeds against the current QMD_EMBED_MODEL.
8
+ // 3. Local cache corruption: nuke and start over.
9
+ //
10
+ // What it does:
11
+ // - clear `source_sha` rows for the wiki in ~/.rig/state.db
12
+ // - drop the per-wiki qmd sqlite store
13
+ // - full re-embed (Qwen3-Embedding by default)
14
+
15
+ import print from '../print';
16
+ import { loadWikiConfig, resolveWiki, WikiEntry } from './config';
17
+ import { getDb, recordLastRun } from './db';
18
+ import { qmdEmbed, qmdResetStore } from './qmd';
19
+
20
+ interface RebuildOpts { wiki?: string; all?: boolean; skipEmbed?: boolean; }
21
+
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
+
32
+ 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
+
38
+ qmdResetStore(t.name);
39
+
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
+ }
50
+
51
+ recordLastRun(t.name, 'rebuild', 0);
52
+ print.succeed(`rebuilt: ${t.name}`);
53
+ }
54
+
55
+ print.info('next: run `rig wiki scan` to baseline the new sha index.');
56
+ }
@@ -0,0 +1,94 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import JSON5 from 'json5';
4
+ import print from '../print';
5
+ import { loadWikiConfig, saveWikiConfig, WikiEntry } from './config';
6
+
7
+ interface RegisterOpts {
8
+ as?: string;
9
+ force?: boolean;
10
+ }
11
+
12
+ export default function wikiRegister(givenPath: string | undefined, opts: RegisterOpts): void {
13
+ const wikiPath = path.resolve(givenPath || detectWikiPath(process.cwd()) || process.cwd());
14
+ if (!fs.existsSync(wikiPath)) {
15
+ print.error(`path does not exist: ${wikiPath}`);
16
+ process.exit(1);
17
+ }
18
+ const project = detectProjectRoot(wikiPath);
19
+ const name = (opts.as || detectName(project, wikiPath)).trim();
20
+ if (!name) {
21
+ print.error('failed to derive a wiki name; pass --name <n>');
22
+ process.exit(1);
23
+ }
24
+
25
+ const cfg = loadWikiConfig();
26
+ const existing = cfg.wikis.findIndex(w => w.name === name);
27
+ if (existing >= 0 && !opts.force) {
28
+ print.error(`wiki "${name}" already registered at ${cfg.wikis[existing].path}; pass --force to overwrite`);
29
+ process.exit(1);
30
+ }
31
+
32
+ const entry: WikiEntry = {
33
+ name,
34
+ path: wikiPath,
35
+ project: project || undefined,
36
+ include: ['**/*.md'],
37
+ exclude: [`${path.basename(wikiPath)}/**`, 'node_modules/**', '.git/**'],
38
+ schedule: { scan: '0 */6 * * *', lint: '0 3 * * *' },
39
+ ingestRules: [{ match: 'raw/**/*.md', mode: 'auto-on-new' }],
40
+ };
41
+
42
+ if (existing >= 0) cfg.wikis[existing] = entry;
43
+ else cfg.wikis.push(entry);
44
+ saveWikiConfig(cfg);
45
+
46
+ // bidirectional: also write to project's package.rig.json5 if present
47
+ if (project) writeProjectWikiBlock(project, name, wikiPath);
48
+
49
+ print.succeed(`registered wiki "${name}" -> ${wikiPath}`);
50
+ }
51
+
52
+ function detectWikiPath(start: string): string | undefined {
53
+ const candidates = ['harness/llm-wiki', 'wiki'];
54
+ let dir = start;
55
+ while (true) {
56
+ for (const c of candidates) {
57
+ const cand = path.join(dir, c);
58
+ if (fs.existsSync(path.join(cand, 'purpose.md'))) return cand;
59
+ }
60
+ const parent = path.dirname(dir);
61
+ if (parent === dir) return undefined;
62
+ dir = parent;
63
+ }
64
+ }
65
+
66
+ function detectProjectRoot(wikiPath: string): string | undefined {
67
+ let dir = wikiPath;
68
+ while (true) {
69
+ if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
70
+ const parent = path.dirname(dir);
71
+ if (parent === dir) return undefined;
72
+ dir = parent;
73
+ }
74
+ }
75
+
76
+ function detectName(project: string | undefined, wikiPath: string): string {
77
+ if (project) {
78
+ try {
79
+ const pkg = JSON.parse(fs.readFileSync(path.join(project, 'package.json'), 'utf8'));
80
+ if (pkg && typeof pkg.name === 'string') return pkg.name.replace(/^@.*\//, '');
81
+ } catch { /* fall through */ }
82
+ }
83
+ return path.basename(path.dirname(wikiPath));
84
+ }
85
+
86
+ function writeProjectWikiBlock(project: string, name: string, wikiPath: string): void {
87
+ const file = path.join(project, 'package.rig.json5');
88
+ let cfg: Record<string, unknown> = {};
89
+ if (fs.existsSync(file)) {
90
+ try { cfg = JSON5.parse(fs.readFileSync(file, 'utf8')); } catch { cfg = {}; }
91
+ }
92
+ cfg.wiki = { name, path: path.relative(project, wikiPath) };
93
+ fs.writeFileSync(file, JSON5.stringify(cfg, null, 2) + '\n', 'utf8');
94
+ }
Binary file
@@ -0,0 +1,37 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import print from '../print';
4
+ import { paths } from './paths';
5
+
6
+ export default function wikiUninstallSkill(): void {
7
+ const targetDir = path.join(paths.claudeSkillsDir, 'rig-wiki');
8
+ const target = path.join(targetDir, 'SKILL.md');
9
+
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
+ }
16
+
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}`);
23
+ }
24
+ } catch { /* non-fatal */ }
25
+ }
26
+
27
+ if (!removed) print.info(`nothing to remove at ${target}`);
28
+ }
29
+
30
+ function isBrokenSymlink(p: string): boolean {
31
+ try {
32
+ fs.statSync(p);
33
+ return false;
34
+ } catch {
35
+ try { return Boolean(fs.readlinkSync(p)); } catch { return false; }
36
+ }
37
+ }
@@ -0,0 +1,16 @@
1
+ import path from 'path';
2
+ import print from '../print';
3
+ import { loadWikiConfig, saveWikiConfig } from './config';
4
+
5
+ export default function wikiUnregister(nameOrPath: string): void {
6
+ const cfg = loadWikiConfig();
7
+ const target = path.resolve(nameOrPath);
8
+ const before = cfg.wikis.length;
9
+ cfg.wikis = cfg.wikis.filter(w => w.name !== nameOrPath && w.path !== target);
10
+ if (cfg.wikis.length === before) {
11
+ print.error(`no registered wiki matches "${nameOrPath}"`);
12
+ process.exit(1);
13
+ }
14
+ saveWikiConfig(cfg);
15
+ print.succeed(`unregistered "${nameOrPath}" (disk contents untouched)`);
16
+ }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "rigjs",
3
- "version": "3.0.33",
3
+ "version": "4.0.2",
4
+ "versionCode": 26052409,
4
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.",
5
6
  "keywords": [
6
7
  "modular",
@@ -13,15 +14,37 @@
13
14
  "workspaces"
14
15
  ],
15
16
  "main": "index.js",
16
- "repository":{
17
+ "repository": {
17
18
  "type": "git",
18
- "url": "https://github.com/FlashHand/rig"
19
+ "url": "https://github.com/FlashHand/rig"
19
20
  },
20
21
  "author": "ralwayne",
21
22
  "license": "MIT",
22
23
  "bin": {
23
24
  "rig": "bin/rig.js"
24
25
  },
26
+ "engines": {
27
+ "node": ">=22 <27"
28
+ },
29
+ "os": [
30
+ "darwin"
31
+ ],
32
+ "files": [
33
+ "bin",
34
+ "built",
35
+ "lib",
36
+ "doc",
37
+ "scripts",
38
+ ".claude/skills/rig-wiki",
39
+ ".claude-plugin",
40
+ "RIG_WIKI_SKILL.md",
41
+ "RIG_CREW_SKILL.md",
42
+ "skills.md",
43
+ "index.js",
44
+ "README.md",
45
+ "README_CN.md",
46
+ "LICENSE"
47
+ ],
25
48
  "scripts": {
26
49
  "test": "jest",
27
50
  "test:init": "yarn build && cd demo && ts-node ../built/index.js init",
@@ -31,11 +54,16 @@
31
54
  "c": "cd demo && node ../lib/rig/index.js check",
32
55
  "envmake": "cd demo && node ../lib/rig/index.js --env prod_view_zhs",
33
56
  "t": "node lib/rig/index.js tag",
34
- "deliver": "npm publish --registry=https://registry.npmjs.org",
35
- "deliver:alpha": "rig tag & yarn build & npm publish --registry=https://registry.npmjs.org --tag alpha",
36
- "build": "esbuild lib/rig/index.ts --platform=node --bundle --sourcemap=inline --minify --outfile=built/index.js --external:shelljs"
57
+ "deliver": "node scripts/publish.mjs",
58
+ "deliver:alpha": "rig tag && yarn build && node scripts/publish.mjs --tag alpha",
59
+ "build": "esbuild lib/rig/index.ts --platform=node --bundle --sourcemap=inline --minify --outfile=built/index.js --external:shelljs --external:better-sqlite3 --external:bindings --external:proxy-agent --external:@tobilu/qmd",
60
+ "version:code": "node scripts/version-code.mjs",
61
+ "version:code:peek": "node scripts/version-code.mjs --peek",
62
+ "prepublishOnly": "node scripts/version-code.mjs && node scripts/sync-skill.mjs && yarn build",
63
+ "postinstall": "node scripts/postinstall.mjs"
37
64
  },
38
65
  "dependencies": {
66
+ "@tobilu/qmd": "~2.5.2",
39
67
  "@types/ali-oss": "^6.16.3",
40
68
  "@types/json5": "^2.2.0",
41
69
  "@types/qs": "^6.9.7",
@@ -44,6 +72,7 @@
44
72
  "@types/uuid": "^8.3.4",
45
73
  "ali-oss": "^6.17.1",
46
74
  "axios": "^0.26.1",
75
+ "better-sqlite3": "12.10.0",
47
76
  "chalk": "^4.1.0",
48
77
  "commander": "6.1.0",
49
78
  "compare-versions": "^4.1.3",
@@ -59,6 +88,7 @@
59
88
  "devDependencies": {
60
89
  "@types/jest": "^28.1.1",
61
90
  "@types/node": "^17.0.21",
91
+ "esbuild": "0.28.0",
62
92
  "jest": "^27.5.1",
63
93
  "ts-jest": "^28.0.5",
64
94
  "ts-node": "^10.8.1",