rigjs 3.0.33 → 4.0.3
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/.claude/skills/rig-wiki/SKILL.md +104 -0
- package/.claude-plugin/plugin.json +14 -0
- package/README.md +18 -1
- package/README_CN.md +17 -1
- package/RIG_CREW_SKILL.md +274 -0
- package/RIG_WIKI_SKILL.md +104 -0
- package/bin/rig.js +0 -0
- package/built/index.js +376 -299
- package/doc/architecture/README.md +139 -0
- package/doc/architecture/agents.md +180 -0
- package/doc/architecture/fc.md +17 -0
- package/doc/architecture/wiki.md +278 -0
- package/lib/crew/ask.ts +24 -0
- package/lib/crew/board.ts +123 -0
- package/lib/crew/config.ts +109 -0
- package/lib/crew/doctor.ts +40 -0
- package/lib/crew/inbox.ts +29 -0
- package/lib/crew/index.ts +108 -0
- package/lib/crew/init.ts +113 -0
- package/lib/crew/paths.ts +13 -0
- package/lib/crew/project.ts +84 -0
- package/lib/crew/role.ts +121 -0
- package/lib/crew/roleCommand.ts +150 -0
- package/lib/crew/state.ts +19 -0
- package/lib/crew/status.ts +27 -0
- package/lib/crew/stub.ts +9 -0
- package/lib/crew/sync.ts +15 -0
- package/lib/crew/task.ts +92 -0
- package/lib/crew/vault.ts +266 -0
- package/lib/installLocal.ts +189 -0
- package/lib/rig/index.ts +26 -3
- package/lib/wiki/README.md +79 -0
- package/lib/wiki/agent/claude.ts +65 -0
- package/lib/wiki/agent/codex.ts +22 -0
- package/lib/wiki/agent/index.ts +11 -0
- package/lib/wiki/agent/list.ts +27 -0
- package/lib/wiki/agent/pi.ts +21 -0
- package/lib/wiki/agent/registry.ts +16 -0
- package/lib/wiki/agent/types.ts +37 -0
- package/lib/wiki/agent/use.ts +21 -0
- package/lib/wiki/config.ts +99 -0
- package/lib/wiki/daemon/index.ts +25 -0
- package/lib/wiki/daemon/install.ts +69 -0
- package/lib/wiki/daemon/logs.ts +16 -0
- package/lib/wiki/daemon/runner.ts +42 -0
- package/lib/wiki/daemon/start.ts +20 -0
- package/lib/wiki/daemon/status.ts +23 -0
- package/lib/wiki/daemon/stop.ts +16 -0
- package/lib/wiki/daemon/uninstall.ts +17 -0
- package/lib/wiki/db.ts +71 -0
- package/lib/wiki/fetch.ts +206 -0
- package/lib/wiki/index.ts +106 -0
- package/lib/wiki/indexCmd.ts +23 -0
- package/lib/wiki/ingest.ts +271 -0
- package/lib/wiki/init.ts +125 -0
- package/lib/wiki/installSkill.ts +92 -0
- package/lib/wiki/lint.ts +252 -0
- package/lib/wiki/list.ts +69 -0
- package/lib/wiki/pathGuard.ts +87 -0
- package/lib/wiki/paths.ts +29 -0
- package/lib/wiki/platform.ts +8 -0
- package/lib/wiki/qmd.ts +205 -0
- package/lib/wiki/query.ts +144 -0
- package/lib/wiki/rebuild.ts +56 -0
- package/lib/wiki/register.ts +131 -0
- package/lib/wiki/scan.ts +0 -0
- package/lib/wiki/uninstallSkill.ts +37 -0
- package/lib/wiki/unregister.ts +16 -0
- package/package.json +36 -6
- package/scripts/postinstall.mjs +108 -0
- package/scripts/publish.mjs +93 -0
- package/scripts/sync-skill.mjs +33 -0
- package/scripts/version-code.mjs +86 -0
- package/skills.md +54 -0
- package/.github/workflows/npm-publish.yml +0 -22
- package/demo/.env.oem1 +0 -4
- package/demo/.env.oem2 +0 -4
- package/demo/babel.config.js +0 -5
- package/demo/env.rig.json5 +0 -8
- package/demo/jsconfig.json +0 -19
- package/demo/package.json +0 -59
- package/demo/package.rig.json5 +0 -78
- package/demo/public/favicon.ico +0 -0
- package/demo/public/index.html +0 -17
- package/demo/rig_dev/.gitkeep +0 -0
- package/demo/rig_helper.d.ts +0 -4
- package/demo/rig_helper.js +0 -10
- package/demo/rigs/.gitkeep +0 -0
- package/demo/src/App.vue +0 -34
- package/demo/src/assets/logo.png +0 -0
- package/demo/src/components/HelloWorld.vue +0 -58
- package/demo/src/main.js +0 -8
- package/demo/vue.config.js +0 -8
- package/demo/yarn.lock +0 -6312
- package/develop.png +0 -0
- package/jest/test.rig.json5 +0 -14
- package/jest.config.ts +0 -16
- package/production.png +0 -0
- package/tsconfig.json +0 -53
package/lib/wiki/qmd.ts
ADDED
|
@@ -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,131 @@
|
|
|
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
|
+
// Read project-local overrides (include / exclude / ingestRules / schedule)
|
|
33
|
+
// from package.rig.json5 if present. The user is the authoritative voice for
|
|
34
|
+
// what gets scanned. Falls back to safe defaults only when fields are absent.
|
|
35
|
+
const projectWiki = project ? readProjectWikiBlock(project) : null;
|
|
36
|
+
const wikiBasename = path.basename(wikiPath);
|
|
37
|
+
const entry: WikiEntry = {
|
|
38
|
+
name,
|
|
39
|
+
path: wikiPath,
|
|
40
|
+
project: project || undefined,
|
|
41
|
+
include: projectWiki?.include ?? ['**/*.md'],
|
|
42
|
+
exclude: projectWiki?.exclude ?? [`${wikiBasename}/**`, 'node_modules/**', '.git/**'],
|
|
43
|
+
schedule: projectWiki?.schedule ?? { scan: '0 */6 * * *', lint: '0 3 * * *' },
|
|
44
|
+
ingestRules: projectWiki?.ingestRules ?? [{ match: 'raw/**/*.md', mode: 'auto-on-new' }],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (existing >= 0) cfg.wikis[existing] = entry;
|
|
48
|
+
else cfg.wikis.push(entry);
|
|
49
|
+
saveWikiConfig(cfg);
|
|
50
|
+
|
|
51
|
+
// bidirectional: write back to project's package.rig.json5 — preserve any
|
|
52
|
+
// existing include/exclude/ingestRules/schedule fields the user authored.
|
|
53
|
+
if (project) writeProjectWikiBlock(project, name, wikiPath, projectWiki);
|
|
54
|
+
|
|
55
|
+
print.succeed(`registered wiki "${name}" -> ${wikiPath}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ProjectWikiBlock {
|
|
59
|
+
name?: string;
|
|
60
|
+
path?: string;
|
|
61
|
+
include?: string[];
|
|
62
|
+
exclude?: string[];
|
|
63
|
+
schedule?: { scan?: string; lint?: string; ingest?: string | null };
|
|
64
|
+
ingestRules?: { match: string; mode: 'auto-on-new' | 'propose-only' }[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readProjectWikiBlock(project: string): ProjectWikiBlock | null {
|
|
68
|
+
const file = path.join(project, 'package.rig.json5');
|
|
69
|
+
if (!fs.existsSync(file)) return null;
|
|
70
|
+
try {
|
|
71
|
+
const cfg = JSON5.parse(fs.readFileSync(file, 'utf8'));
|
|
72
|
+
const wiki = cfg && typeof cfg === 'object' && cfg.wiki;
|
|
73
|
+
return wiki && typeof wiki === 'object' ? wiki as ProjectWikiBlock : null;
|
|
74
|
+
} catch { return null; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function detectWikiPath(start: string): string | undefined {
|
|
78
|
+
const candidates = ['harness/llm-wiki', 'wiki'];
|
|
79
|
+
let dir = start;
|
|
80
|
+
while (true) {
|
|
81
|
+
for (const c of candidates) {
|
|
82
|
+
const cand = path.join(dir, c);
|
|
83
|
+
if (fs.existsSync(path.join(cand, 'purpose.md'))) return cand;
|
|
84
|
+
}
|
|
85
|
+
const parent = path.dirname(dir);
|
|
86
|
+
if (parent === dir) return undefined;
|
|
87
|
+
dir = parent;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function detectProjectRoot(wikiPath: string): string | undefined {
|
|
92
|
+
let dir = wikiPath;
|
|
93
|
+
while (true) {
|
|
94
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
|
|
95
|
+
const parent = path.dirname(dir);
|
|
96
|
+
if (parent === dir) return undefined;
|
|
97
|
+
dir = parent;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function detectName(project: string | undefined, wikiPath: string): string {
|
|
102
|
+
if (project) {
|
|
103
|
+
try {
|
|
104
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(project, 'package.json'), 'utf8'));
|
|
105
|
+
if (pkg && typeof pkg.name === 'string') return pkg.name.replace(/^@.*\//, '');
|
|
106
|
+
} catch { /* fall through */ }
|
|
107
|
+
}
|
|
108
|
+
return path.basename(path.dirname(wikiPath));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function writeProjectWikiBlock(
|
|
112
|
+
project: string,
|
|
113
|
+
name: string,
|
|
114
|
+
wikiPath: string,
|
|
115
|
+
existingWiki: ProjectWikiBlock | null,
|
|
116
|
+
): void {
|
|
117
|
+
const file = path.join(project, 'package.rig.json5');
|
|
118
|
+
let cfg: Record<string, unknown> = {};
|
|
119
|
+
if (fs.existsSync(file)) {
|
|
120
|
+
try { cfg = JSON5.parse(fs.readFileSync(file, 'utf8')); } catch { cfg = {}; }
|
|
121
|
+
}
|
|
122
|
+
// Always update name + path (those are derived from invocation). Preserve
|
|
123
|
+
// every other field the user authored (include / exclude / schedule /
|
|
124
|
+
// ingestRules / anything else they put in there).
|
|
125
|
+
cfg.wiki = {
|
|
126
|
+
...(existingWiki || {}),
|
|
127
|
+
name,
|
|
128
|
+
path: path.relative(project, wikiPath),
|
|
129
|
+
};
|
|
130
|
+
fs.writeFileSync(file, JSON5.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
131
|
+
}
|
package/lib/wiki/scan.ts
ADDED
|
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
|
+
"version": "4.0.3",
|
|
4
|
+
"versionCode": 26052410,
|
|
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":
|
|
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": "
|
|
35
|
-
"deliver:alpha": "rig tag
|
|
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",
|