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.
- 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 +94 -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
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CrewEntry, CrewProject, DEFAULT_CREW_ROOT } from './config';
|
|
4
|
+
import { CrewRoleDefinition, roleDefinitionsForCrew } from './role';
|
|
5
|
+
|
|
6
|
+
export function crewRoot(crew: CrewEntry): string {
|
|
7
|
+
return path.join(crew.vault, crew.root || DEFAULT_CREW_ROOT);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function crewPath(crew: CrewEntry, rel: string): string {
|
|
11
|
+
return path.join(crew.vault, rel);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function rootPath(crew: CrewEntry, rel: string): string {
|
|
15
|
+
return path.join(crewRoot(crew), rel);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ensureCrewVault(crew: CrewEntry): void {
|
|
19
|
+
ensureDir(crew.vault);
|
|
20
|
+
ensureDir(crewPath(crew, 'tmp'));
|
|
21
|
+
writeIfMissing(crewPath(crew, 'tmp/.gitkeep'), '');
|
|
22
|
+
fs.mkdirSync(crewRoot(crew), { recursive: true });
|
|
23
|
+
writeIfMissing(rootPath(crew, 'Current-Goal.md'), '# Current Goal\n\n');
|
|
24
|
+
writeIfMissing(rootPath(crew, 'Team-Dashboard.md'), '# Team Dashboard\n\nCoding agents can run `rig crew board` to refresh this dashboard.\n');
|
|
25
|
+
writeIfMissing(rootPath(crew, 'Inbox.md'), '# Crew Inbox\n\n## Open\n\n## Resolved\n');
|
|
26
|
+
|
|
27
|
+
ensureDir(rootPath(crew, 'Shared'));
|
|
28
|
+
writeIfMissing(rootPath(crew, 'Shared/Spec.md'), '# Spec\n\n');
|
|
29
|
+
writeIfMissing(rootPath(crew, 'Shared/Decisions.md'), '# Decisions\n\n');
|
|
30
|
+
writeIfMissing(rootPath(crew, 'Shared/Glossary.md'), '# Glossary\n\n');
|
|
31
|
+
writeIfMissing(rootPath(crew, 'Shared/Context.md'), '# Context\n\n');
|
|
32
|
+
ensureDir(rootPath(crew, 'Shared/PRDs'));
|
|
33
|
+
writeIfMissing(rootPath(crew, 'Shared/PRDs/.gitkeep'), '');
|
|
34
|
+
|
|
35
|
+
const roles = roleDefinitionsForCrew(crew);
|
|
36
|
+
for (const role of roles) ensureRole(crew, role);
|
|
37
|
+
writeRoleRegistry(crew, roles);
|
|
38
|
+
ensureDir(rootPath(crew, 'Projects'));
|
|
39
|
+
for (const project of crew.projects || []) ensureProject(crew, project);
|
|
40
|
+
ensureDir(rootPath(crew, 'Logs'));
|
|
41
|
+
writeVaultAgentInstructions(crew);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ensureProject(crew: CrewEntry, project: CrewProject): void {
|
|
45
|
+
const base = rootPath(crew, path.join('Projects', project.name));
|
|
46
|
+
ensureDir(base);
|
|
47
|
+
writeProjectOwnerFile(path.join(base, 'Owner.md'), project);
|
|
48
|
+
writeIfMissing(path.join(base, 'Context.md'), `# ${project.name} Context\n\n`);
|
|
49
|
+
writeIfMissing(path.join(base, 'Tasks.md'), `# ${project.name} Tasks\n\n`);
|
|
50
|
+
writeIfMissing(path.join(base, 'Decisions.md'), `# ${project.name} Decisions\n\n`);
|
|
51
|
+
writeIfMissing(path.join(base, 'Test-Plan.md'), `# ${project.name} Test Plan\n\n`);
|
|
52
|
+
ensureDir(path.join(base, 'PRDs'));
|
|
53
|
+
writeIfMissing(path.join(base, 'PRDs', '.gitkeep'), '');
|
|
54
|
+
ensureDir(path.join(base, 'Reports'));
|
|
55
|
+
writeIfMissing(path.join(base, 'Reports', '.gitkeep'), '');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function readText(file: string): string {
|
|
59
|
+
if (!fs.existsSync(file)) return '';
|
|
60
|
+
return fs.readFileSync(file, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function writeText(file: string, content: string): void {
|
|
64
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
65
|
+
fs.writeFileSync(file, content, 'utf8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function appendLog(crew: CrewEntry, message: string): void {
|
|
69
|
+
const d = new Date();
|
|
70
|
+
const name = d.toISOString().slice(0, 10) + '.md';
|
|
71
|
+
const file = rootPath(crew, path.join('Logs', name));
|
|
72
|
+
if (!fs.existsSync(file)) writeText(file, `# ${name.slice(0, 10)}\n\n`);
|
|
73
|
+
fs.appendFileSync(file, `- ${d.toISOString()} ${message}\n`, 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ensureRole(crew: CrewEntry, role: CrewRoleDefinition): void {
|
|
77
|
+
const folder = role.folder;
|
|
78
|
+
const base = rootPath(crew, folder);
|
|
79
|
+
ensureDir(base);
|
|
80
|
+
writeIfMissing(path.join(base, 'Tasks.md'), `# ${folder} Tasks\n\n`);
|
|
81
|
+
writeIfMissing(path.join(base, 'Notes.md'), `# ${folder} Notes\n\n`);
|
|
82
|
+
if (role.name === 'lead') {
|
|
83
|
+
ensureDir(path.join(base, 'Reports'));
|
|
84
|
+
writeIfMissing(path.join(base, 'Reports', '.gitkeep'), '');
|
|
85
|
+
}
|
|
86
|
+
if (role.name === 'pm') {
|
|
87
|
+
ensureDir(path.join(base, 'PRDs'));
|
|
88
|
+
writeIfMissing(path.join(base, 'PRDs', '.gitkeep'), '');
|
|
89
|
+
writeIfMissing(path.join(base, 'Reviews.md'), '# PM Reviews\n\n');
|
|
90
|
+
}
|
|
91
|
+
if (role.name === 'researcher') {
|
|
92
|
+
ensureDir(path.join(base, 'Reports'));
|
|
93
|
+
writeIfMissing(path.join(base, 'Reports', '.gitkeep'), '');
|
|
94
|
+
writeIfMissing(path.join(base, 'Index.md'), '# Research Index\n\n');
|
|
95
|
+
}
|
|
96
|
+
if (!role.builtIn) {
|
|
97
|
+
ensureDir(path.join(base, 'Reports'));
|
|
98
|
+
writeIfMissing(path.join(base, 'Reports', '.gitkeep'), '');
|
|
99
|
+
writeIfMissing(path.join(base, 'Role.md'), renderRoleFile(role));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function writeRoleRegistry(crew: CrewEntry, roles: CrewRoleDefinition[]): void {
|
|
104
|
+
const file = rootPath(crew, 'Shared/Roles.md');
|
|
105
|
+
cleanupLegacyRoleRegistry(file);
|
|
106
|
+
const rows = roles.map(role => {
|
|
107
|
+
const prompt = role.promptPath ? role.promptPath.replace(crewPathForHome(), '$RIG_HOME') : '-';
|
|
108
|
+
return `| ${role.name} | ${role.title} | ${role.agent || '-'} | ${role.defaultExecutor || '-'} | ${role.folder} | ${prompt} |`;
|
|
109
|
+
});
|
|
110
|
+
upsertManagedBlock(file, [
|
|
111
|
+
'<!-- rig-crew-roles:start -->',
|
|
112
|
+
'## Generated Role Registry',
|
|
113
|
+
'',
|
|
114
|
+
'Generated by `rig crew`. Edit global custom roles under `$RIG_HOME/crew/roles/` (`~/.rig/crew/roles/` by default). Add human notes outside this managed block.',
|
|
115
|
+
'',
|
|
116
|
+
'| Role | Title | Agent | Executor | Vault Folder | Prompt |',
|
|
117
|
+
'|---|---|---|---|---|---|',
|
|
118
|
+
...rows,
|
|
119
|
+
'',
|
|
120
|
+
'<!-- rig-crew-roles:end -->',
|
|
121
|
+
].join('\n'), '# Crew Role Registry\n\n');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function cleanupLegacyRoleRegistry(file: string): void {
|
|
125
|
+
if (!fs.existsSync(file)) return;
|
|
126
|
+
const current = readText(file);
|
|
127
|
+
if (!current.includes('Generated by `rig crew`.')) return;
|
|
128
|
+
if (!current.includes('| Role | Title | Agent | Executor | Vault Folder | Prompt |')) return;
|
|
129
|
+
const marker = '<!-- rig-crew-roles:start -->';
|
|
130
|
+
const markerIndex = current.indexOf(marker);
|
|
131
|
+
const before = markerIndex >= 0 ? current.slice(0, markerIndex) : current;
|
|
132
|
+
const after = markerIndex >= 0 ? current.slice(markerIndex) : '';
|
|
133
|
+
const lines = before.split(/\r?\n/);
|
|
134
|
+
const generated = lines.findIndex(line => line.startsWith('Generated by `rig crew`.'));
|
|
135
|
+
const table = lines.findIndex((line, i) => i > generated && line.startsWith('| Role | Title | Agent | Executor | Vault Folder | Prompt |'));
|
|
136
|
+
if (generated < 0 || table < 0) return;
|
|
137
|
+
let end = table + 1;
|
|
138
|
+
while (end < lines.length && lines[end].trim().startsWith('|')) end++;
|
|
139
|
+
while (end < lines.length && lines[end].trim() === '') end++;
|
|
140
|
+
const cleanedBefore = lines.slice(0, generated).concat(lines.slice(end)).join('\n').trimEnd();
|
|
141
|
+
const next = [cleanedBefore, after.trimStart()].filter(Boolean).join('\n\n').trimEnd() + '\n';
|
|
142
|
+
if (next !== current) writeText(file, next);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderRoleFile(role: CrewRoleDefinition): string {
|
|
146
|
+
return [
|
|
147
|
+
`# ${role.title}`,
|
|
148
|
+
'',
|
|
149
|
+
`Role: ${role.name}`,
|
|
150
|
+
`Agent: ${role.agent || '-'}`,
|
|
151
|
+
`Executor: ${role.defaultExecutor || '-'}`,
|
|
152
|
+
`Prompt: ${role.promptPath || '-'}`,
|
|
153
|
+
'',
|
|
154
|
+
role.description || '',
|
|
155
|
+
'',
|
|
156
|
+
].join('\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function crewPathForHome(): string {
|
|
160
|
+
return process.env.RIG_HOME || path.join(process.env.HOME || '', '.rig');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const AGENT_RULES_START = '<!-- rig-crew:start -->';
|
|
164
|
+
const AGENT_RULES_END = '<!-- rig-crew:end -->';
|
|
165
|
+
|
|
166
|
+
function writeVaultAgentInstructions(crew: CrewEntry): void {
|
|
167
|
+
const targets = ['CLAUDE.md', 'AGENTS.md'].map(file => crewPath(crew, file));
|
|
168
|
+
const seen = new Set<string>();
|
|
169
|
+
for (const file of targets) {
|
|
170
|
+
const key = fs.existsSync(file) ? fs.realpathSync(file) : file;
|
|
171
|
+
if (seen.has(key)) continue;
|
|
172
|
+
seen.add(key);
|
|
173
|
+
upsertManagedBlock(file, renderVaultAgentInstructions(crew));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function upsertManagedBlock(file: string, block: string, missingContent?: string): void {
|
|
178
|
+
const current = fs.existsSync(file) ? readText(file) : (missingContent || `# ${path.basename(file, '.md')}\n\n`);
|
|
179
|
+
const markers = managedMarkers(block);
|
|
180
|
+
const re = new RegExp(`${escapeRegExp(markers.start)}[\\s\\S]*?${escapeRegExp(markers.end)}`);
|
|
181
|
+
const next = re.test(current)
|
|
182
|
+
? current.replace(re, block.trimEnd())
|
|
183
|
+
: `${current.trimEnd()}\n\n${block.trimEnd()}`;
|
|
184
|
+
if (next !== current) writeText(file, `${next.trimEnd()}\n`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function managedMarkers(block: string): { start: string; end: string } {
|
|
188
|
+
const lines = block.split(/\r?\n/).map(l => l.trim());
|
|
189
|
+
const start = lines.find(l => /^<!--\s*[-a-z0-9]+:start\s*-->$/.test(l));
|
|
190
|
+
const end = lines.find(l => /^<!--\s*[-a-z0-9]+:end\s*-->$/.test(l));
|
|
191
|
+
if (!start || !end) throw new Error('managed block requires start/end markers');
|
|
192
|
+
return { start, end };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function renderVaultAgentInstructions(crew: CrewEntry): string {
|
|
196
|
+
const root = crew.root || DEFAULT_CREW_ROOT;
|
|
197
|
+
return [
|
|
198
|
+
AGENT_RULES_START,
|
|
199
|
+
'## Rig Crew',
|
|
200
|
+
'',
|
|
201
|
+
'This Vault uses `rig crew` as an agent-facing coordination layer. Humans talk to the current Claude/Codex coding session; the coding agent uses `rig crew` and Vault files to communicate with Crew Lead and coordinate other roles.',
|
|
202
|
+
'',
|
|
203
|
+
`- Crew root: \`${root}\``,
|
|
204
|
+
`- Dashboard: \`${root}/Team-Dashboard.md\``,
|
|
205
|
+
`- Inbox: \`${root}/Inbox.md\``,
|
|
206
|
+
`- Role registry: \`${root}/Shared/Roles.md\``,
|
|
207
|
+
`- Project owner memory: \`${root}/Projects/<project>/\``,
|
|
208
|
+
`- Custom role workspaces: \`${root}/Roles/<role>/\``,
|
|
209
|
+
'- Vault-local scratch projects belong under `tmp/<project>/`.',
|
|
210
|
+
'- User-level rules, test accounts, custom roles, and research output policy live under `~/.rig/`.',
|
|
211
|
+
'- Coordinate through Vault files; do not start or assume a separate multi-agent runtime inside a project repo.',
|
|
212
|
+
'- Do not copy secrets, auth state, production traces, or personal data into project repositories.',
|
|
213
|
+
'- When working on project code, also read that project\'s `RIG.md` or `rig.md` if present.',
|
|
214
|
+
'',
|
|
215
|
+
'Default collaboration protocol for coding agents:',
|
|
216
|
+
'',
|
|
217
|
+
'0. Do not treat `rig crew` as a human-facing command workflow. If you can run the command or update the Vault files yourself, do it instead of asking the human to run it.',
|
|
218
|
+
'1. If the user asks for planning, multi-agent coordination, PRD, research, testing strategy, project owner work, role routing, reports, or broad project changes, hand the request to Crew Lead first.',
|
|
219
|
+
`2. Preferred handoff: run \`rig crew "<user request>"\`, then read \`${root}/Team-Dashboard.md\`, \`${root}/Inbox.md\`, and \`${root}/Shared/Roles.md\`.`,
|
|
220
|
+
`3. Maintain status awareness before and after work by checking \`${root}/Team-Dashboard.md\`, \`${root}/Inbox.md\`, \`${root}/Shared/Roles.md\`, role \`Tasks.md\`, and project \`Tasks.md\` files.`,
|
|
221
|
+
`4. If the CLI is unavailable, append the request to \`${root}/Current-Goal.md\` and create/update a Lead task in \`${root}/Lead/Tasks.md\`; then refresh the dashboard when possible.`,
|
|
222
|
+
'5. Treat Crew Lead as the default orchestrator prompt/protocol, not as a required Claude/Codex subagent. Subagents may be used as optional executors for specific roles, but Vault files are the source of truth.',
|
|
223
|
+
'6. Lead communicates with other roles through Markdown tasks and delegation packets, not private chat state. Use `[role:: <role>]`, `[owner:: <owner>]`, `[project:: <project>]`, `[executor:: <executor>]`, and status fields in the relevant `Tasks.md`.',
|
|
224
|
+
`7. Worker results must be written back to the relevant role/project files under \`${root}/\`; user-facing questions go to \`${root}/Inbox.md\` for Lead to surface.`,
|
|
225
|
+
'',
|
|
226
|
+
AGENT_RULES_END,
|
|
227
|
+
].join('\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function escapeRegExp(value: string): string {
|
|
231
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function writeProjectOwnerFile(file: string, project: CrewProject): void {
|
|
235
|
+
const owner = project.owner || `maintainer:${project.name}`;
|
|
236
|
+
const fresh = `# ${owner}\n\nProject: ${project.name}\nPath: ${project.path}\n`;
|
|
237
|
+
if (!fs.existsSync(file)) {
|
|
238
|
+
writeText(file, fresh);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const current = readText(file);
|
|
242
|
+
let next = current;
|
|
243
|
+
if (!next.trim()) next = fresh;
|
|
244
|
+
else {
|
|
245
|
+
const lines = next.split(/\r?\n/);
|
|
246
|
+
if (lines[0].startsWith('# maintainer:') || lines[0] === `# ${owner}`) lines[0] = `# ${owner}`;
|
|
247
|
+
next = lines.join('\n');
|
|
248
|
+
next = upsertLine(next, /^Project:\s*.*$/m, `Project: ${project.name}`);
|
|
249
|
+
next = upsertLine(next, /^Path:\s*.*$/m, `Path: ${project.path}`);
|
|
250
|
+
}
|
|
251
|
+
if (next !== current) writeText(file, next);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function upsertLine(text: string, pattern: RegExp, line: string): string {
|
|
255
|
+
if (pattern.test(text)) return text.replace(pattern, line);
|
|
256
|
+
return text.endsWith('\n') ? `${text}${line}\n` : `${text}\n${line}\n`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function ensureDir(dir: string): void {
|
|
260
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function writeIfMissing(file: string, content: string): void {
|
|
264
|
+
if (fs.existsSync(file)) return;
|
|
265
|
+
writeText(file, content);
|
|
266
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// `rig install-local` — build the current source tree and install it as the
|
|
2
|
+
// global `rig` binary so edits become reachable as the on-PATH `rig`.
|
|
3
|
+
//
|
|
4
|
+
// Strategy (in order):
|
|
5
|
+
// 1. Build (yarn build).
|
|
6
|
+
// 2. If <npm prefix>/lib/node_modules/rigjs exists and is user-writable,
|
|
7
|
+
// do an in-place sync of the package.json `files[]` payload. This avoids
|
|
8
|
+
// EACCES on a root-owned npm prefix that already has a user-owned rigjs
|
|
9
|
+
// subdir (the common Homebrew-then-sudo-first-install pattern).
|
|
10
|
+
// 3. Otherwise fall back to `npm install -g <repoRoot>`.
|
|
11
|
+
// 4. Re-link the agent skill via `rig wiki install-skill --force`.
|
|
12
|
+
//
|
|
13
|
+
// A subsequent `npm i -g rigjs` (registry install) cleanly overwrites step 2.
|
|
14
|
+
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { execSync, spawnSync } from 'child_process';
|
|
18
|
+
import print from './print';
|
|
19
|
+
|
|
20
|
+
interface InstallLocalOpts {
|
|
21
|
+
skipBuild?: boolean;
|
|
22
|
+
manager?: 'npm' | 'yarn';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function installLocal(opts: InstallLocalOpts = {}): void {
|
|
26
|
+
// `__dirname/..` would point at the *installed* rigjs dir when invoked via
|
|
27
|
+
// the global binary — that's the destination, not the source. Walk up from
|
|
28
|
+
// CWD instead, so the user gets "install whatever I'm hacking on right now".
|
|
29
|
+
const repoRoot = findSourceRoot();
|
|
30
|
+
if (!repoRoot) {
|
|
31
|
+
print.error('run `rig install-local` from inside the rigjs source tree (a dir with package.json {"name":"rigjs"}).');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!opts.skipBuild) {
|
|
36
|
+
print.start('yarn build');
|
|
37
|
+
const build = spawnSync('yarn', ['build'], { cwd: repoRoot, stdio: 'inherit' });
|
|
38
|
+
if (build.status !== 0) {
|
|
39
|
+
print.error(`yarn build exited with code ${build.status}`);
|
|
40
|
+
process.exit(build.status ?? 1);
|
|
41
|
+
}
|
|
42
|
+
print.succeed('build done');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dest = findInstallDir();
|
|
46
|
+
if (dest && canWriteInside(dest)) {
|
|
47
|
+
syncInPlace(repoRoot, dest);
|
|
48
|
+
} else {
|
|
49
|
+
runPackageManager(repoRoot, opts.manager || 'npm');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Always refresh the agent skill symlink so edits to RIG_WIKI_SKILL.md
|
|
53
|
+
// take effect on next Claude Code restart.
|
|
54
|
+
const link = spawnSync('rig', ['wiki', 'install-skill', '--force'], { stdio: 'inherit' });
|
|
55
|
+
if (link.status !== 0) {
|
|
56
|
+
print.warn('skill relink exited non-zero (often safe to ignore; re-run `rig wiki install-skill --force` manually if needed).');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const which = whichRig();
|
|
60
|
+
print.succeed(`installed: ${which || 'rig'} — verify with \`rig -v\` (expect ${readVersion(repoRoot)}) and \`rig -c\` (expect ${readVersionCode(repoRoot) ?? '<empty>'}).`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function findSourceRoot(start: string = process.cwd()): string | null {
|
|
64
|
+
let dir = path.resolve(start);
|
|
65
|
+
while (true) {
|
|
66
|
+
const pkg = path.join(dir, 'package.json');
|
|
67
|
+
if (fs.existsSync(pkg)) {
|
|
68
|
+
try {
|
|
69
|
+
const p = JSON.parse(fs.readFileSync(pkg, 'utf8'));
|
|
70
|
+
if (p && p.name === 'rigjs') return dir;
|
|
71
|
+
} catch { /* keep walking */ }
|
|
72
|
+
}
|
|
73
|
+
const parent = path.dirname(dir);
|
|
74
|
+
if (parent === dir) return null;
|
|
75
|
+
dir = parent;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Locate the existing global rigjs install dir without invoking npm (faster,
|
|
80
|
+
// and tolerates broken npm configs).
|
|
81
|
+
function findInstallDir(): string | null {
|
|
82
|
+
try {
|
|
83
|
+
const prefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
|
84
|
+
const candidate = path.join(prefix, 'lib', 'node_modules', 'rigjs');
|
|
85
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function canWriteInside(dir: string): boolean {
|
|
92
|
+
try {
|
|
93
|
+
const probe = path.join(dir, '.rig-write-probe');
|
|
94
|
+
fs.writeFileSync(probe, '');
|
|
95
|
+
fs.unlinkSync(probe);
|
|
96
|
+
return true;
|
|
97
|
+
} catch { return false; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Copy the subset that `package.json files[]` would ship into the existing
|
|
101
|
+
// rigjs install dir. Uses rsync for speed + deletion of stale files inside
|
|
102
|
+
// per-entry dirs. Does not touch the rigjs/node_modules tree.
|
|
103
|
+
function syncInPlace(repoRoot: string, dest: string): void {
|
|
104
|
+
print.start(`sync in place -> ${dest}`);
|
|
105
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
106
|
+
const files: string[] = Array.isArray(pkg.files) ? pkg.files : [];
|
|
107
|
+
|
|
108
|
+
// Always-shipped top-level files alongside package.json
|
|
109
|
+
const alwaysFiles = ['package.json'];
|
|
110
|
+
|
|
111
|
+
// 1. top-level files
|
|
112
|
+
for (const f of [...alwaysFiles, ...files.filter(isFile(repoRoot))]) {
|
|
113
|
+
const src = path.join(repoRoot, f);
|
|
114
|
+
const dst = path.join(dest, f);
|
|
115
|
+
if (!fs.existsSync(src)) continue;
|
|
116
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
117
|
+
fs.copyFileSync(src, dst);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. directory entries -> rsync --delete (mirrors the source for that dir)
|
|
121
|
+
const dirs = files.filter(isDir(repoRoot));
|
|
122
|
+
for (const d of dirs) {
|
|
123
|
+
const src = path.join(repoRoot, d) + '/';
|
|
124
|
+
const dst = path.join(dest, d) + '/';
|
|
125
|
+
fs.mkdirSync(path.dirname(dst.replace(/\/$/, '')), { recursive: true });
|
|
126
|
+
const r = spawnSync('rsync', ['-a', '--delete', src, dst], { stdio: 'inherit' });
|
|
127
|
+
if (r.status !== 0) {
|
|
128
|
+
print.error(`rsync ${d} failed (code ${r.status})`);
|
|
129
|
+
process.exit(r.status ?? 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// node_modules is not in package.json `files[]` (npm install handles it
|
|
134
|
+
// for registry installs), so the in-place sync must mirror it explicitly.
|
|
135
|
+
// rsync's delta transfer keeps this cheap on incremental dep changes.
|
|
136
|
+
const srcNm = path.join(repoRoot, 'node_modules');
|
|
137
|
+
if (fs.existsSync(srcNm)) {
|
|
138
|
+
const dstNm = path.join(dest, 'node_modules');
|
|
139
|
+
const r = spawnSync('rsync', ['-a', '--delete', srcNm + '/', dstNm + '/'], { stdio: 'inherit' });
|
|
140
|
+
if (r.status !== 0) {
|
|
141
|
+
print.error(`rsync node_modules failed (code ${r.status})`);
|
|
142
|
+
process.exit(r.status ?? 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Always force exec bit on the launcher — rsync preserves source mode, and
|
|
147
|
+
// repos that lost the exec bit (common after fresh clones on some setups)
|
|
148
|
+
// would leave the global `rig` un-runnable. Cheap to always re-set.
|
|
149
|
+
const launcher = path.join(dest, 'bin', 'rig.js');
|
|
150
|
+
try { fs.chmodSync(launcher, 0o755); } catch { /* not fatal */ }
|
|
151
|
+
|
|
152
|
+
print.succeed(`sync done`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isFile(repoRoot: string) {
|
|
156
|
+
return (f: string) => {
|
|
157
|
+
try { return fs.statSync(path.join(repoRoot, f)).isFile(); } catch { return false; }
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isDir(repoRoot: string) {
|
|
162
|
+
return (f: string) => {
|
|
163
|
+
try { return fs.statSync(path.join(repoRoot, f)).isDirectory(); } catch { return false; }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function runPackageManager(repoRoot: string, manager: 'npm' | 'yarn') {
|
|
168
|
+
const cmd = manager === 'yarn'
|
|
169
|
+
? ['yarn', ['global', 'add', `file:${repoRoot}`]]
|
|
170
|
+
: ['npm', ['install', '-g', repoRoot]];
|
|
171
|
+
print.start(`${cmd[0]} ${(cmd[1] as string[]).join(' ')}`);
|
|
172
|
+
const r = spawnSync(cmd[0] as string, cmd[1] as string[], { cwd: repoRoot, stdio: 'inherit' });
|
|
173
|
+
if (r.status !== 0) {
|
|
174
|
+
print.error(`${cmd[0]} exited with code ${r.status}. If this is EACCES on /usr/local, either rerun with sudo, switch the npm prefix to a user dir, or chown the rigjs subtree.`);
|
|
175
|
+
process.exit(r.status ?? 1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readVersion(repoRoot: string): string {
|
|
180
|
+
return JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')).version;
|
|
181
|
+
}
|
|
182
|
+
function readVersionCode(repoRoot: string): number | null {
|
|
183
|
+
const v = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')).versionCode;
|
|
184
|
+
return v != null ? v : null;
|
|
185
|
+
}
|
|
186
|
+
function whichRig(): string | null {
|
|
187
|
+
try { return execSync('command -v rig', { encoding: 'utf8' }).trim() || null; }
|
|
188
|
+
catch { return null; }
|
|
189
|
+
}
|
package/lib/rig/index.ts
CHANGED
|
@@ -9,13 +9,22 @@ import deploy from '../deploy';
|
|
|
9
9
|
import publish from '../publish';
|
|
10
10
|
|
|
11
11
|
import sync from '../sync';
|
|
12
|
-
const nodeMin = '
|
|
12
|
+
const nodeMin = '22.0.0';
|
|
13
13
|
if (semver.gte(nodeMin,process.version)){
|
|
14
|
-
print.error('NodeJS version must be at least
|
|
14
|
+
print.error('NodeJS version must be at least 22 (better-sqlite3 12.x prebuilds require it).');
|
|
15
15
|
process.exit(0);
|
|
16
16
|
}
|
|
17
17
|
import {Command} from 'commander';
|
|
18
18
|
|
|
19
|
+
// Short-circuit `-c` / `--versioncode` before commander parses subcommands,
|
|
20
|
+
// mirroring how commander handles `-v` itself.
|
|
21
|
+
if (process.argv.some(a => a === '-c' || a === '--versioncode')) {
|
|
22
|
+
const pkg = require('../../package.json');
|
|
23
|
+
// eslint-disable-next-line no-console
|
|
24
|
+
console.log(pkg.versionCode ?? '');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
const program = new Command();
|
|
20
29
|
|
|
21
30
|
import check from '../check';
|
|
@@ -29,6 +38,14 @@ import install from '../install';
|
|
|
29
38
|
program.command('install').action(install);
|
|
30
39
|
program.command('i').action(install);
|
|
31
40
|
|
|
41
|
+
import installLocal from '../installLocal';
|
|
42
|
+
|
|
43
|
+
program.command('install-local')
|
|
44
|
+
.description('build the current source tree and install it as the global `rig`')
|
|
45
|
+
.option('--skip-build', 'skip `yarn build` (use existing built/index.js)')
|
|
46
|
+
.option('--manager <name>', 'npm | yarn (default: npm)')
|
|
47
|
+
.action(installLocal);
|
|
48
|
+
|
|
32
49
|
program.command('preinstall').action(preinstall);
|
|
33
50
|
program.command('postinstall').action(postinstall);
|
|
34
51
|
import tag from '../tag';
|
|
@@ -66,10 +83,16 @@ program.command('sync')
|
|
|
66
83
|
.option('-f, --force <force>', 'force to overwrite files from package.rig.json5')
|
|
67
84
|
.action(sync);
|
|
68
85
|
|
|
86
|
+
import { registerWikiCommands } from '../wiki';
|
|
87
|
+
registerWikiCommands(program);
|
|
88
|
+
|
|
89
|
+
import { registerCrewCommands } from '../crew';
|
|
90
|
+
registerCrewCommands(program);
|
|
91
|
+
|
|
69
92
|
import env from '../env';
|
|
70
93
|
|
|
71
94
|
program.option('--env <env>', 'specify env').action(env.load);
|
|
72
95
|
|
|
73
96
|
program.version(require('../../package.json').version, '-v,--version');
|
|
97
|
+
program.option('-c, --versioncode', 'output the version code (YYMMDDNN)');
|
|
74
98
|
program.parse(process.argv);
|
|
75
|
-
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# lib/wiki — `rig wiki *` source map
|
|
2
|
+
|
|
3
|
+
> Companion to `doc/architecture/wiki.md`. This file documents *what each source file does*; the architecture doc covers *why* and *how it fits together*.
|
|
4
|
+
|
|
5
|
+
Convention: **one file per subcommand**, plus a small set of shared infra files at the top of the directory. Subcommand groups (`agent`, `daemon`) live in their own subfolder, and each sub-subcommand also gets its own file there.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Shared infrastructure
|
|
10
|
+
|
|
11
|
+
| File | Purpose |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `index.ts` | Commander wiring. Builds the `rig wiki` subtree and attaches every action. Imported once from `lib/rig/index.ts`. |
|
|
14
|
+
| `paths.ts` | Centralized filesystem paths (`~/.rig/`, launchd plist, Claude skills dir). Override with `RIG_HOME`. Also exports the launchd label. |
|
|
15
|
+
| `platform.ts` | `requireMacOS()` — hard-exits with code 32 on non-Darwin platforms. v1 is macOS-only by decision; see roadmap P5. |
|
|
16
|
+
| `config.ts` | JSON5 read/write for `~/.rig/config.json5` (`RigConfig`) and `~/.rig/wiki.config.json5` (`WikiConfig`). `resolveWiki()` picks the target wiki for a command (flag → CWD walk → undefined). |
|
|
17
|
+
| `db.ts` | Lazy-loaded `better-sqlite3` singleton. WAL mode. Idempotent migrations on every open. Exposes `getDb()`, `recordLastRun()`, `getLastRun()`. |
|
|
18
|
+
| `qmd.ts` | Detects `qmd` on PATH, wraps `qmd query --json` and `qmd embed`. All callers must handle `installed=false` gracefully — qmd is optional. |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Subcommands (one file each)
|
|
23
|
+
|
|
24
|
+
| File | Subcommand | What it does |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `init.ts` | `rig wiki init [path]` | Bootstraps a fresh wiki dir: `purpose.md` + `schema.md` from templates, empty `index.md` / `overview.md` / `log.md` / `reviews.md`, `raw/` + five `wiki/<sub>/` dirs. Idempotent — never overwrites existing files. Does **not** register. |
|
|
27
|
+
| `register.ts` | `rig wiki register [path]` | Adds (or `--force`-replaces) an entry in `~/.rig/wiki.config.json5`. Auto-detects path (`harness/llm-wiki` / `wiki`) walking up from CWD. Also writes a `wiki:` block back to the project's `package.rig.json5` for bidirectional consistency. |
|
|
28
|
+
| `unregister.ts` | `rig wiki unregister <nameOrPath>` | Removes the entry. Disk wiki content is untouched. |
|
|
29
|
+
| `list.ts` | `rig wiki list` | Prints a table: name, path, page count, last scan / ingest / lint. Banner row shows detected agent CLI, qmd status. |
|
|
30
|
+
| `scan.ts` | `rig wiki scan [path]` | Walks `include` globs + `raw/`, sha256-compares against the `source_sha` table in `state.db`. Emits NEW / MODIFIED / DELETED / RAW DRIFT report. Returns exit code 10 if any RAW DRIFT (raw/ files are immutable). No agent calls. |
|
|
31
|
+
| `fetch.ts` | `rig wiki fetch <url>` | (stub — P1) Agent-as-fetcher. Will WebFetch the URL via Claude adapter and write `raw/YYYY-MM-DD-<slug>.md` verbatim with frontmatter. **Never** summarizes. |
|
|
32
|
+
| `ingest.ts` | `rig wiki ingest <source>` | (stub — P1) Two-step CoT (analysis → generation). Sandbox-copies the wiki dir, runs Claude adapter, then host-diffs sandbox vs original to extract writes. Filters out edits to `raw/` / `purpose.md` / `schema.md`. `--dry-run` prints diff without applying. |
|
|
33
|
+
| `query.ts` | `rig wiki query "..."` | (stub — P1) Answer a question with `[[wikilink]]` citations. Prefers `qmd query --json` for retrieval; falls back to injecting `index.md` + `overview.md` + heuristic page picks. |
|
|
34
|
+
| `lint.ts` | `rig wiki lint` | (stub — P1) Walks the wiki for contradictions, orphans, stale claims, broken `sources[]` refs, reviews.md backlog. Writes `lint-report-YYYY-MM-DD.md`. Non-zero exit on severe findings (code 11). |
|
|
35
|
+
| `indexCmd.ts` | `rig wiki index` | qmd-only. Ensures the wiki's qmd collection exists, then runs `qmd embed`. When qmd is absent: warns and exits 0 (qmd is optional). Named `indexCmd` to avoid clashing with `index.ts`. |
|
|
36
|
+
| `installSkill.ts` | `rig wiki install-skill` | Locates rig's bundled `.claude/skills/rig-wiki/` inside the rigjs install, then symlinks it into `~/.claude/skills/rig-wiki`. Lets every machine with rig installed get the slash command without manual copy. |
|
|
37
|
+
| `uninstallSkill.ts` | `rig wiki uninstall-skill` | Removes the symlink. |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## `agent/` — Agent CLI adapter
|
|
42
|
+
|
|
43
|
+
One adapter per agent CLI. Only Claude Code is implemented in v1; others are stubs whose `detect()` works but `run()` throws `NotImplementedError`.
|
|
44
|
+
|
|
45
|
+
| File | Purpose |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `index.ts` | Registers the `agent` Commander subtree. Re-exports `adapters` + `getAdapter` from `registry.ts` for callers that don't want to know about the registry split. |
|
|
48
|
+
| `registry.ts` | Constructs the singleton `adapters` array (one of each adapter class) and exports `getAdapter(name)`. Kept separate from `index.ts` to avoid a circular import with `list.ts`. |
|
|
49
|
+
| `types.ts` | The `AgentAdapter` interface and run-options/result types. All adapters obey it so the host can swap them. |
|
|
50
|
+
| `claude.ts` | **Full implementation.** Spawns `claude -p` (non-interactive) with `--allowedTools` derived from `allowWrite` + requested tools. Prepends a hard-coded system-prompt header that forbids editing `raw/` / `purpose.md` / `schema.md`. |
|
|
51
|
+
| `codex.ts` | Stub. Detection works; `run()` throws. Open questions on codex's permission flags live in `doc/architecture/agents.md §4`. |
|
|
52
|
+
| `pi.ts` | Stub. Same shape as codex. Upstream CLI name not yet fixed. |
|
|
53
|
+
| `list.ts` | `rig wiki agent list` — iterates `adapters`, calls `detect()`, prints a table. Marks the default agent with `*`. |
|
|
54
|
+
| `use.ts` | `rig wiki agent use <name>` — writes `~/.rig/config.json5` `wiki.defaultAgent`. Rejects un-implemented adapters with exit code 20. |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## `daemon/` — launchd-managed background runner
|
|
59
|
+
|
|
60
|
+
| File | Purpose |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `index.ts` | Registers the `daemon` Commander subtree. |
|
|
63
|
+
| `install.ts` | Writes `~/Library/LaunchAgents/ai.flashhand.rig.wiki.plist` (with discovered node + rig entry paths), then `launchctl bootout` (idempotent) + `bootstrap`. |
|
|
64
|
+
| `uninstall.ts` | `launchctl bootout` + remove the plist. |
|
|
65
|
+
| `start.ts` | `launchctl bootstrap` only (use after `install` if you've stopped manually). |
|
|
66
|
+
| `stop.ts` | `launchctl bootout` only. |
|
|
67
|
+
| `status.ts` | `launchctl print gui/<uid>/<label>`, parses `state=` and `pid=`. |
|
|
68
|
+
| `logs.ts` | Tails `~/.rig/logs/wiki-daemon.log` (with optional `-f`). |
|
|
69
|
+
| `runner.ts` | **The launchd entry.** `launchctl` invokes `node <rigjs>/built/index.js wiki daemon runner`. v1: heartbeat-only loop that logs every 10 min and reloads `wiki.config.json5`. P2 will add cron-based scan/lint scheduling and `auto-on-new` ingest rules. |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Conventions
|
|
74
|
+
|
|
75
|
+
- Every action default-exports a function the Commander wiring imports as `fooAction`. Subcommand groups (`agent`, `daemon`) instead expose `registerXyzCommands(parent)`.
|
|
76
|
+
- Human output goes through `lib/print` (ora + chalk). Machine output is `--json` and goes to plain `console.log` (so JSON doesn't get ANSI-painted).
|
|
77
|
+
- Anything that writes to disk under `~/.rig/` first calls a helper in `config.ts` that `mkdir -p`s the home dir. Subcommands don't open-code path creation.
|
|
78
|
+
- The DB and qmd helpers are lazy-loaded so subcommands that don't need them (e.g. `init`, `agent list`) start instantly and don't drag in the native binary.
|
|
79
|
+
- No subcommand calls another subcommand directly — they share state via `config.ts` + `db.ts`. This keeps each file a leaf.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
|
+
import { AgentAdapter, AgentDetect, AgentRunOpts, AgentRunResult } from './types';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
|
|
5
|
+
|
|
6
|
+
const SYSTEM_PROMPT_HEADER = `You are the executor for \`rig wiki <op>\`.
|
|
7
|
+
You MUST follow <wiki>/schema.md exactly.
|
|
8
|
+
You MUST NOT edit raw/, purpose.md, schema.md. The host will reject any such patch.
|
|
9
|
+
Output is consumed by a CLI; keep stdout to status updates only. Persist actual content by writing files.
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
export class ClaudeAdapter implements AgentAdapter {
|
|
13
|
+
name = 'claude' as const;
|
|
14
|
+
|
|
15
|
+
async detect(): Promise<AgentDetect> {
|
|
16
|
+
const which = spawnSync('command', ['-v', 'claude'], { encoding: 'utf8', shell: '/bin/sh' });
|
|
17
|
+
const binPath = (which.stdout || '').trim();
|
|
18
|
+
if (which.status !== 0 || !binPath) return { installed: false };
|
|
19
|
+
const v = spawnSync('claude', ['--version'], { encoding: 'utf8' });
|
|
20
|
+
const version = (v.stdout || '').trim() || undefined;
|
|
21
|
+
return { installed: true, path: binPath, version };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async run(opts: AgentRunOpts): Promise<AgentRunResult> {
|
|
25
|
+
const args = ['-p'];
|
|
26
|
+
args.push('--allowedTools', allowedToolsCsv(opts));
|
|
27
|
+
if (opts.systemPrompt || SYSTEM_PROMPT_HEADER) {
|
|
28
|
+
args.push('--append-system-prompt', SYSTEM_PROMPT_HEADER + (opts.systemPrompt || ''));
|
|
29
|
+
}
|
|
30
|
+
args.push(opts.prompt);
|
|
31
|
+
const start = Date.now();
|
|
32
|
+
return new Promise<AgentRunResult>((resolve) => {
|
|
33
|
+
const child = spawn('claude', args, {
|
|
34
|
+
cwd: opts.cwd,
|
|
35
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
36
|
+
env: process.env,
|
|
37
|
+
});
|
|
38
|
+
let stdout = '', stderr = '';
|
|
39
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
40
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
41
|
+
const killer = setTimeout(() => child.kill('SIGTERM'),
|
|
42
|
+
opts.timeoutMs || DEFAULT_TIMEOUT_MS);
|
|
43
|
+
child.on('close', (code) => {
|
|
44
|
+
clearTimeout(killer);
|
|
45
|
+
resolve({
|
|
46
|
+
ok: code === 0,
|
|
47
|
+
exitCode: code ?? -1,
|
|
48
|
+
stdout, stderr,
|
|
49
|
+
durationMs: Date.now() - start,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function allowedToolsCsv(opts: AgentRunOpts): string {
|
|
57
|
+
const tools = new Set<string>(['Read']);
|
|
58
|
+
if (opts.allowWrite) { tools.add('Write'); tools.add('Edit'); }
|
|
59
|
+
for (const t of opts.tools || []) {
|
|
60
|
+
if (t === 'webfetch') tools.add('WebFetch');
|
|
61
|
+
if (t === 'qmd') tools.add('Bash(qmd:*)');
|
|
62
|
+
if (t === 'bash') tools.add('Bash');
|
|
63
|
+
}
|
|
64
|
+
return Array.from(tools).join(',');
|
|
65
|
+
}
|