model-orchestrator 0.1.4
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/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/SECURITY.md +17 -0
- package/bin/README.md +10 -0
- package/bin/cli-run.mjs +599 -0
- package/bin/cli.js +372 -0
- package/docs/README.md +13 -0
- package/docs/audit-brief.md +83 -0
- package/docs/catalog.md +113 -0
- package/docs/part-1-beginner.md +65 -0
- package/docs/part-2-intermediate.md +65 -0
- package/docs/part-3-advanced.md +65 -0
- package/package.json +52 -0
- package/scripts/README.md +5 -0
- package/scripts/gen-catalog.js +37 -0
- package/src/README.md +9 -0
- package/src/catalog.js +277 -0
- package/src/detect.js +26 -0
- package/src/install.js +628 -0
- package/src/prompt.js +34 -0
- package/src/render.js +8 -0
- package/templates/README.md +14 -0
- package/templates/advanced/README.md +14 -0
- package/templates/advanced/vm/ENVIRONMENT.md +18 -0
- package/templates/advanced/vm/PRIVACY_GATES.md +33 -0
- package/templates/advanced/vm/README.md +60 -0
- package/templates/advanced/vm/box-CLAUDE.md +28 -0
- package/templates/advanced/vm/docker-compose.yml +19 -0
- package/templates/advanced/vm/gateway.config.yaml +12 -0
- package/templates/advanced/vm/jobs/README.md +39 -0
- package/templates/advanced/vm/jobs/weekly-audit.service +17 -0
- package/templates/advanced/vm/jobs/weekly-audit.sh +107 -0
- package/templates/advanced/vm/jobs/weekly-audit.timer +10 -0
- package/templates/advanced/vm/setup-vm.sh +46 -0
- package/templates/agents/README.md +13 -0
- package/templates/agents/agy/README.md +5 -0
- package/templates/agents/agy/builder.md +17 -0
- package/templates/agents/agy/bulk-worker.md +17 -0
- package/templates/agents/agy/code-reviewer.md +17 -0
- package/templates/agents/agy/deep-planner.md +17 -0
- package/templates/agents/agy/live-researcher.md +17 -0
- package/templates/agents/claude-code/README.md +13 -0
- package/templates/agents/claude-code/builder.md +17 -0
- package/templates/agents/claude-code/bulk-worker.md +18 -0
- package/templates/agents/claude-code/code-reviewer.md +19 -0
- package/templates/agents/claude-code/deep-planner.md +18 -0
- package/templates/agents/claude-code/live-researcher.md +18 -0
- package/templates/agents/snippets/chat.md +25 -0
- package/templates/agents/snippets/claude-code.md +27 -0
- package/templates/agents/snippets/generic.md +21 -0
- package/templates/beginner/ORCHESTRATOR.md +55 -0
- package/templates/beginner/README.md +3 -0
- package/templates/common/README.md +52 -0
- package/templates/common/TASK_BUNDLE.md +56 -0
- package/templates/common/protocols/README.md +14 -0
- package/templates/common/protocols/build-protocol.md +133 -0
- package/templates/common/protocols/deep-research.md +44 -0
- package/templates/common/protocols/gap-analysis.md +28 -0
- package/templates/common/protocols/memory-and-record.md +30 -0
- package/templates/common/protocols/numbers-and-logic.md +35 -0
- package/templates/common/protocols/propagate.md +34 -0
- package/templates/intermediate/CLI-RUN.md +100 -0
- package/templates/intermediate/DELEGATION_MATRIX.md +41 -0
- package/templates/intermediate/README.md +13 -0
- package/templates/intermediate/RESEARCH_TRIAGE.md +30 -0
- package/templates/intermediate/ROUTING.md +73 -0
- package/templates/intermediate/TIERS.md +44 -0
- package/templates/tools/README.md +10 -0
- package/templates/tools/codecalc/CODECALC.md +43 -0
- package/templates/tools/codecalc/mcp/agy.mcp_config.json +8 -0
- package/templates/tools/codecalc/mcp/codex.config.toml +4 -0
- package/templates/tools/codecalc/mcp/mcpServers.json +8 -0
- package/templates/tools/codecalc/mcp/vscode.mcp.json +8 -0
- package/templates/tools/codecalc/mcp/zed.settings.json +9 -0
- package/templates/tools/obsidian-tc/OBSIDIAN-TC.md +65 -0
- package/templates/tools/obsidian-tc/mcp/obsidian-tc.agy.mcp_config.json +9 -0
- package/templates/tools/obsidian-tc/mcp/obsidian-tc.codex.config.toml +7 -0
- package/templates/tools/obsidian-tc/mcp/obsidian-tc.mcpServers.json +9 -0
- package/templates/tools/obsidian-tc/mcp/obsidian-tc.vscode.mcp.json +10 -0
- package/templates/tools/obsidian-tc/mcp/obsidian-tc.zed.settings.json +9 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// model-orchestrator installer.
|
|
3
|
+
// Asks which level you want and which AIs you have access to, then writes the
|
|
4
|
+
// matching files into a folder. It never writes a secret, never runs a vendor
|
|
5
|
+
// shell script, and never overwrites a file you already have unless --force.
|
|
6
|
+
|
|
7
|
+
import { stdin, stdout } from 'node:process';
|
|
8
|
+
import { makeAsker } from '../src/prompt.js';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { resolve, join } from 'node:path';
|
|
11
|
+
import { which } from '../src/detect.js';
|
|
12
|
+
import { AIS, LEVELS, TOOLS, PROVIDERS, aisForLevel, agentCandidates, byId, npmSpec } from '../src/catalog.js';
|
|
13
|
+
import { planFiles, writeFiles, resolveSelection, resolveTools, resolveApis, dirProblems, readManifest, MACHINE_OWNED, RUNTIME, GENERATOR_VERSION } from '../src/install.js';
|
|
14
|
+
|
|
15
|
+
// One strict parse. Unknown flags, missing values and duplicates are usage
|
|
16
|
+
// errors (exit 2) before anything is planned, so a typo like --dryy can never
|
|
17
|
+
// turn a dry run into a real one.
|
|
18
|
+
const SPEC = {
|
|
19
|
+
level: 'value', ais: 'value', primary: 'value', dir: 'value', project: 'value', tools: 'value', apis: 'value',
|
|
20
|
+
yes: 'bool', force: 'bool', dry: 'bool', 'no-install': 'bool', 'no-tools': 'bool', 'no-apis': 'bool', 'upgrade-runtime': 'bool', 'update-docs': 'bool', list: 'bool', help: 'bool', h: 'bool'
|
|
21
|
+
};
|
|
22
|
+
export function parseArgs(argv) {
|
|
23
|
+
const out = {};
|
|
24
|
+
const errors = [];
|
|
25
|
+
for (let i = 0; i < argv.length; i++) {
|
|
26
|
+
const a = argv[i];
|
|
27
|
+
if (!a.startsWith('--')) {
|
|
28
|
+
errors.push(`unexpected argument: ${a}`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
let name = a.slice(2);
|
|
32
|
+
let inline = null;
|
|
33
|
+
const eq = name.indexOf('=');
|
|
34
|
+
if (eq !== -1) {
|
|
35
|
+
inline = name.slice(eq + 1);
|
|
36
|
+
name = name.slice(0, eq);
|
|
37
|
+
}
|
|
38
|
+
const kind = SPEC[name];
|
|
39
|
+
if (!kind) {
|
|
40
|
+
errors.push(`unknown flag: --${name}`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (name in out) errors.push(`--${name} given more than once`);
|
|
44
|
+
if (kind === 'bool') {
|
|
45
|
+
if (inline !== null) errors.push(`--${name} takes no value`);
|
|
46
|
+
out[name] = true;
|
|
47
|
+
} else {
|
|
48
|
+
let v = inline;
|
|
49
|
+
if (v === null) {
|
|
50
|
+
const next = argv[i + 1];
|
|
51
|
+
if (next === undefined || next.startsWith('--')) {
|
|
52
|
+
errors.push(`--${name} requires a value`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
v = next;
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
if (v === '') errors.push(`--${name} requires a non-empty value`);
|
|
59
|
+
out[name] = v;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { out, errors };
|
|
63
|
+
}
|
|
64
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
65
|
+
if (parsed.errors.length) {
|
|
66
|
+
console.error('model-orchestrator: ' + parsed.errors.join('; ') + '\nrun with --help');
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
const flag = (name) => parsed.out[name] === true;
|
|
70
|
+
const opt = (name) => (typeof parsed.out[name] === 'string' ? parsed.out[name] : null);
|
|
71
|
+
|
|
72
|
+
if (flag('help') || flag('h')) {
|
|
73
|
+
console.log(`model-orchestrator: set up a model orchestrator for the AIs you actually have.
|
|
74
|
+
|
|
75
|
+
Usage
|
|
76
|
+
npx model-orchestrator interactive
|
|
77
|
+
npx model-orchestrator --list show the AI catalog and exit
|
|
78
|
+
npx model-orchestrator --yes --level 2 --ais claude-code,codex,grok [--primary claude-code] [--dir ./ai-orchestrator]
|
|
79
|
+
|
|
80
|
+
Flags
|
|
81
|
+
--level 1|2|3 1 beginner (one agent), 2 intermediate (many CLIs), 3 advanced (plus a VM)
|
|
82
|
+
--ais a,b,c catalog ids you have access to (see --list)
|
|
83
|
+
--primary id the agent that runs the system and receives the subagents (any level; required when several qualify)
|
|
84
|
+
--tools a,b companion tools to set up, all optional (default with --yes: codecalc only); --no-tools for none
|
|
85
|
+
--apis a,b level 3 only: metered API keys you HOLD (anthropic,openai,google,xai,openrouter); --no-apis for none.
|
|
86
|
+
Asked separately from the CLIs because a subscription is not an API key.
|
|
87
|
+
--dir path where to write the docs and protocols (default ./ai-orchestrator)
|
|
88
|
+
--project path the project root your agent runs from; subagent definitions go here (default: current directory)
|
|
89
|
+
--yes skip confirmations
|
|
90
|
+
--force overwrite every file that already exists, documents included
|
|
91
|
+
--upgrade-runtime replace the runtime files (cli-run, the audit job, compose, gateway config, setup script) even
|
|
92
|
+
when they cannot be verified as untouched; documents are still kept
|
|
93
|
+
--update-docs regenerate the documents a previous run wrote and nobody edited since (hash-checked against
|
|
94
|
+
MANIFEST.json), so a changed selection reaches ROUTING.md and friends; edited documents are kept
|
|
95
|
+
and reported, and nothing happens without a manifest
|
|
96
|
+
--dry print the plan, write nothing
|
|
97
|
+
--no-install never offer to run npm installs
|
|
98
|
+
--list print the catalog
|
|
99
|
+
`);
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (flag('list')) {
|
|
104
|
+
for (const l of LEVELS) console.log(`level ${l.id} ${l.name}: ${l.tagline}`);
|
|
105
|
+
console.log('');
|
|
106
|
+
for (const a of AIS) {
|
|
107
|
+
const here = a.bin ? (which(a.bin) ? 'installed' : 'not on PATH') : 'app';
|
|
108
|
+
console.log(`${a.id.padEnd(13)} ${a.name}\n${''.padEnd(13)} level ${a.minLevel}+ · ${a.access} · ${here}\n${''.padEnd(13)} ${a.role}`);
|
|
109
|
+
}
|
|
110
|
+
console.log('\nmetered API providers (--apis a,b, level 3 gateway only):');
|
|
111
|
+
for (const prov of PROVIDERS) console.log(`${prov.id.padEnd(13)} ${prov.name} (variable name: ${prov.envName})`);
|
|
112
|
+
console.log('\ncompanion tools (--tools a,b), all optional:');
|
|
113
|
+
for (const t of TOOLS) console.log(`${t.id.padEnd(13)} ${t.name}\n${''.padEnd(13)} ${t.role}\n${''.padEnd(13)} needs: ${t.requires}\n${''.padEnd(13)} ${t.optionalNote}\n${''.padEnd(13)} ${t.repo}`);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const yes = flag('yes');
|
|
118
|
+
const rl = yes ? null : makeAsker({ input: stdin, output: stdout });
|
|
119
|
+
const ask = (q, fallback) => (rl ? rl.ask(q, fallback) : Promise.resolve(fallback));
|
|
120
|
+
|
|
121
|
+
function bad(msg) {
|
|
122
|
+
console.error('model-orchestrator: ' + msg);
|
|
123
|
+
process.exit(2);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function main() {
|
|
127
|
+
console.log('\nmodel-orchestrator\nRoute every task to the cheapest AI that does it well.\n');
|
|
128
|
+
|
|
129
|
+
// 1. Level
|
|
130
|
+
let level = Number(opt('level'));
|
|
131
|
+
if (![1, 2, 3].includes(level)) {
|
|
132
|
+
if (yes) bad('--level must be 1, 2 or 3 when --yes is set');
|
|
133
|
+
console.log('Which level?');
|
|
134
|
+
for (const l of LEVELS) console.log(` ${l.id} ${l.name}: ${l.tagline}`);
|
|
135
|
+
level = Number(await ask('\nLevel [1]: ', '1'));
|
|
136
|
+
if (![1, 2, 3].includes(level)) bad('level must be 1, 2 or 3');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. Access
|
|
140
|
+
const available = aisForLevel(level);
|
|
141
|
+
let ids;
|
|
142
|
+
if (opt('ais')) {
|
|
143
|
+
ids = opt('ais').split(',').map((s) => s.trim()).filter(Boolean);
|
|
144
|
+
} else {
|
|
145
|
+
if (yes) bad('--ais is required with --yes (comma-separated ids, see --list)');
|
|
146
|
+
console.log('\nWhich AIs do you have access to? (numbers, comma-separated; detected ones are marked)');
|
|
147
|
+
available.forEach((a, i) => {
|
|
148
|
+
const mark = a.bin && which(a.bin) ? '*' : ' ';
|
|
149
|
+
console.log(` ${String(i + 1).padStart(2)} ${mark} ${a.name}`);
|
|
150
|
+
});
|
|
151
|
+
const detected = available.map((a, i) => (a.bin && which(a.bin) ? i + 1 : null)).filter(Boolean);
|
|
152
|
+
const fallback = detected.join(',');
|
|
153
|
+
const answer = await ask(`\nYour picks${fallback ? ' [' + fallback + ']' : ''}: `, fallback);
|
|
154
|
+
ids = answer
|
|
155
|
+
.split(',')
|
|
156
|
+
.map((s) => s.trim())
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.map((n) => {
|
|
159
|
+
const a = available[Number(n) - 1];
|
|
160
|
+
if (!a) bad(`no AI numbered ${n}`);
|
|
161
|
+
return a.id;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const { selected, unknown } = resolveSelection(ids);
|
|
165
|
+
if (unknown.length) bad('unknown AI id(s): ' + unknown.join(', ') + ' (see --list)');
|
|
166
|
+
if (!selected.length) bad('pick at least one AI');
|
|
167
|
+
const tooHigh = selected.filter((a) => a.minLevel > level);
|
|
168
|
+
if (tooHigh.length) bad(`${tooHigh.map((a) => a.id).join(', ')} need level ${Math.max(...tooHigh.map((a) => a.minLevel))} or higher`);
|
|
169
|
+
|
|
170
|
+
// 3. Primary agent (the one that runs the system)
|
|
171
|
+
const candidates = agentCandidates(selected);
|
|
172
|
+
let primary = null;
|
|
173
|
+
if (opt('primary')) {
|
|
174
|
+
primary = byId[opt('primary')];
|
|
175
|
+
if (!primary || !candidates.includes(primary)) bad('--primary must be one of: ' + candidates.map((a) => a.id).join(', '));
|
|
176
|
+
} else if (candidates.length === 1) {
|
|
177
|
+
primary = candidates[0];
|
|
178
|
+
} else if (candidates.length === 0) {
|
|
179
|
+
bad('pick at least one agent or chat app to be the orchestrator; a local model runtime on its own cannot run the system');
|
|
180
|
+
} else if (candidates.length > 1) {
|
|
181
|
+
if (yes) primary = candidates.find((a) => a.id === 'claude-code') || candidates[0];
|
|
182
|
+
else {
|
|
183
|
+
console.log('\nWhich one is your primary agent (the one that runs the system)?');
|
|
184
|
+
candidates.forEach((a, i) => console.log(` ${i + 1} ${a.name}`));
|
|
185
|
+
const n = Number(await ask('\nPrimary [1]: ', '1'));
|
|
186
|
+
primary = candidates[n - 1];
|
|
187
|
+
if (!primary) bad('pick a listed number');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 3b. Companion tools (not AIs: things the AIs call)
|
|
192
|
+
let tools = [];
|
|
193
|
+
if (flag('no-tools') && opt('tools')) bad('--no-tools and --tools contradict each other');
|
|
194
|
+
if (!flag('no-tools')) {
|
|
195
|
+
if (opt('tools')) {
|
|
196
|
+
const r = resolveTools(opt('tools').split(',').map((s) => s.trim()).filter(Boolean));
|
|
197
|
+
if (r.unknown.length) bad('unknown tool id(s): ' + r.unknown.join(', ') + ' (see --list)');
|
|
198
|
+
tools = r.tools;
|
|
199
|
+
} else if (yes) {
|
|
200
|
+
tools = TOOLS.filter((t) => t.recommended);
|
|
201
|
+
} else {
|
|
202
|
+
console.log('\nCompanion tools (all optional): things your agents call. Selecting one writes docs and config snippets; it installs nothing.');
|
|
203
|
+
for (const t of TOOLS) {
|
|
204
|
+
console.log(`\n ${t.id}: ${t.role}\n ${t.repo}\n needs: ${t.requires}\n ${t.optionalNote}`);
|
|
205
|
+
const def = t.recommended ? 'y' : 'n';
|
|
206
|
+
const a = await ask(` Set up ${t.id}? [${t.recommended ? 'Y/n' : 'y/N'}]: `, def);
|
|
207
|
+
if (/^y/i.test(a)) tools.push(t);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 3c. Level 3: which metered API keys the user HOLDS. Separate from the CLI
|
|
213
|
+
// question on purpose: a Claude Code plan is not an Anthropic API key.
|
|
214
|
+
let apis = [];
|
|
215
|
+
if (flag('no-apis') && opt('apis')) bad('--no-apis and --apis contradict each other');
|
|
216
|
+
if (level >= 3 && !flag('no-apis')) {
|
|
217
|
+
if (opt('apis')) {
|
|
218
|
+
const r = resolveApis(opt('apis').split(',').map((s) => s.trim()).filter(Boolean));
|
|
219
|
+
if (r.unknown.length) bad('unknown provider id(s): ' + r.unknown.join(', ') + ' (see --list)');
|
|
220
|
+
apis = r.apis;
|
|
221
|
+
} else if (!yes) {
|
|
222
|
+
console.log('\nLevel 3 gateway: which metered API keys do you HOLD? (numbers, comma-separated, or none)');
|
|
223
|
+
console.log(' This is separate from the CLIs above: a subscription is not an API key. Only variable NAMES are written; you keep the values in your secrets manager.');
|
|
224
|
+
PROVIDERS.forEach((prov, i) => console.log(` ${String(i + 1).padStart(2)} ${prov.name} (${prov.envName})`));
|
|
225
|
+
const a = await ask('\nYour keys [none]: ', '');
|
|
226
|
+
apis = a
|
|
227
|
+
.split(',')
|
|
228
|
+
.map((s) => s.trim())
|
|
229
|
+
.filter(Boolean)
|
|
230
|
+
.map((n) => {
|
|
231
|
+
const prov = PROVIDERS[Number(n) - 1];
|
|
232
|
+
if (!prov) bad(`no provider numbered ${n}`);
|
|
233
|
+
return prov;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
} else if (level < 3 && opt('apis')) {
|
|
237
|
+
bad('--apis only applies at level 3 (the gateway)');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 4. Targets: the docs folder, and the project root the agent runs from
|
|
241
|
+
const dir = resolve(opt('dir') || (await ask('\nWrite docs and protocols into [./ai-orchestrator]: ', './ai-orchestrator')));
|
|
242
|
+
const dirBad = dirProblems(dir);
|
|
243
|
+
if (dirBad.length) bad(dirBad.join('; '));
|
|
244
|
+
const project = resolve(opt('project') || (primary && primary.agentsDir && !yes ? await ask(`\nProject root your agent runs from (subagents go in ${primary.agentsDir}/ there) [.]: `, '.') : '.'));
|
|
245
|
+
const projectBad = dirProblems(project);
|
|
246
|
+
if (projectBad.length) bad('--project: ' + projectBad.join('; '));
|
|
247
|
+
|
|
248
|
+
// 5. Plan
|
|
249
|
+
const files = planFiles({ level, selected, primary, dir, project, tools, apis });
|
|
250
|
+
const lvl = LEVELS.find((l) => l.id === level);
|
|
251
|
+
const agentFiles = files.filter((f) => f.root === 'project');
|
|
252
|
+
console.log(`\nPlan\n level ${lvl.id} ${lvl.name}\n access ${selected.map((a) => a.id).join(', ')}\n primary ${primary ? primary.id : 'none'}\n tools ${tools.map((t) => t.id).join(', ') || 'none'}` + (level >= 3 ? `\n api keys ${apis.map((p) => p.id).join(', ') || 'none'}` : '') + `\n folder ${dir}\n project ${project}${agentFiles.length ? ' (' + agentFiles.length + ' subagent files go here)' : ''}\n files ${files.length}`);
|
|
253
|
+
if (flag('dry')) {
|
|
254
|
+
for (const f of files) console.log(' - ' + (f.root === 'project' ? '[project] ' : '') + f.rel);
|
|
255
|
+
console.log('\n--dry: nothing written.');
|
|
256
|
+
rl && rl.close();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const go = yes ? 'y' : await ask('\nWrite these files? [Y/n]: ', 'y');
|
|
260
|
+
if (!/^y/i.test(go)) {
|
|
261
|
+
console.log('Nothing written.');
|
|
262
|
+
rl && rl.close();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Reconfiguration: compare what a previous run recorded with what was asked now.
|
|
267
|
+
const prev = readManifest(dir);
|
|
268
|
+
const changed = prev
|
|
269
|
+
? ['level', 'primary'].filter((k) => String(prev[k]) !== String(k === 'level' ? level : primary ? primary.id : null))
|
|
270
|
+
.concat(['ais', 'tools', 'apis'].filter((k) => JSON.stringify(prev[k] || []) !== JSON.stringify({ ais: selected, tools, apis }[k].map((x) => x.id))))
|
|
271
|
+
: [];
|
|
272
|
+
|
|
273
|
+
let written, skipped, upgraded, conflicts, unverifiable, docsUpdated, docsConflict, docsUnverifiable;
|
|
274
|
+
try {
|
|
275
|
+
({ written, skipped, upgraded, conflicts, unverifiable, docsUpdated, docsConflict, docsUnverifiable } = writeFiles(files, { dir, project, force: flag('force'), upgradeRuntime: flag('upgrade-runtime'), updateDocs: flag('update-docs'), prevManifest: prev }));
|
|
276
|
+
} catch (e) {
|
|
277
|
+
if (e && e.code === 'PREFLIGHT') bad(e.message);
|
|
278
|
+
throw e;
|
|
279
|
+
}
|
|
280
|
+
const ownedWritten = written.filter((w) => MACHINE_OWNED.has(w));
|
|
281
|
+
console.log(`\nWrote ${written.length} file(s)` + (skipped.length ? `, kept ${skipped.length} existing:` : '.'));
|
|
282
|
+
for (const s of skipped) console.log(' kept ' + s);
|
|
283
|
+
const existingRuntime = files.filter((f) => f.root !== 'project' && RUNTIME.has(f.rel)).length;
|
|
284
|
+
if (prev || existingRuntime && (upgraded.length || conflicts.length || unverifiable.length) || docsUnverifiable.length) {
|
|
285
|
+
console.log(`\nExisting installation found${prev ? ` (MANIFEST.json from generator ${prev.generatorVersion || 'pre-0.1.1'}, ${prev.generatedAt || 'undated'}; this run is ${GENERATOR_VERSION})` : ' (no MANIFEST.json: it predates 0.1.1)'}.`);
|
|
286
|
+
if (prev) {
|
|
287
|
+
if (changed.length) {
|
|
288
|
+
console.log(` selection changed: ${changed.join(', ')}`);
|
|
289
|
+
console.log(` applied: ${ownedWritten.join(', ') || 'nothing'} (machine-owned files are always rewritten, so the new lanes are live)`);
|
|
290
|
+
} else console.log(' selection identical.');
|
|
291
|
+
}
|
|
292
|
+
if (upgraded.length) console.log(` runtime upgraded: ${upgraded.join(', ')} ${flag('upgrade-runtime') ? '(--upgrade-runtime: replaced whether or not you had edited them)' : '(each installed copy matched the hash of a previous run, so nobody had edited it)'}`);
|
|
293
|
+
if (conflicts.length) {
|
|
294
|
+
console.log(` runtime CONFLICT, kept: ${conflicts.join(', ')}`);
|
|
295
|
+
console.log(' these differ from what a previous run generated, so you edited them. The fixes in this release were NOT applied to them.');
|
|
296
|
+
console.log(' Options: move your copy aside and re-run; or --upgrade-runtime to replace runtime files only; or --force to replace everything.');
|
|
297
|
+
}
|
|
298
|
+
if (unverifiable.length) {
|
|
299
|
+
console.log(` runtime kept, UNVERIFIABLE: ${unverifiable.join(', ')}`);
|
|
300
|
+
console.log(' the previous install left no manifest, so it is not possible to tell whether you edited these. Executable fixes were NOT applied.');
|
|
301
|
+
console.log(' Re-run with --upgrade-runtime to replace runtime files only (documents stay), or --force to replace everything.');
|
|
302
|
+
}
|
|
303
|
+
if (docsUpdated.length) console.log(` documents updated: ${docsUpdated.join(', ')} (each matched the hash of a previous run, so nobody had edited them)`);
|
|
304
|
+
if (docsConflict.length) {
|
|
305
|
+
console.log(` document CONFLICT, kept: ${docsConflict.join(', ')}`);
|
|
306
|
+
console.log(' these differ from what a previous run generated, so you edited them. Edit them by hand, or --force to replace everything.');
|
|
307
|
+
}
|
|
308
|
+
if (docsUnverifiable.length) {
|
|
309
|
+
console.log(` documents kept, UNVERIFIABLE: ${docsUnverifiable.join(', ')}`);
|
|
310
|
+
console.log(' the previous install left no manifest, so --update-docs cannot tell your edits from generated text. --force replaces everything.');
|
|
311
|
+
}
|
|
312
|
+
if (skipped.length && prev && changed.length && !flag('update-docs')) console.log(' documents kept: they may describe the old selection. --update-docs regenerates the ones you have not edited; --force regenerates all of them (this overwrites your edits).');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 6. Installs, opt-in per CLI, npm only. Vendor scripts are printed, never run.
|
|
316
|
+
const missing = selected.filter((a) => a.bin && !which(a.bin));
|
|
317
|
+
if (missing.length) {
|
|
318
|
+
console.log('\nNot found on this machine (presence is checked; installed versions are not validated):');
|
|
319
|
+
for (const a of missing) {
|
|
320
|
+
if (a.install.npm) {
|
|
321
|
+
const spec = npmSpec(a); // the same pinned spec the table and the box script use
|
|
322
|
+
const run = flag('no-install') || yes ? 'n' : await ask(` ${a.name}: run \`npm install -g ${spec}\` now? [y/N]: `, 'n');
|
|
323
|
+
if (/^y/i.test(run)) {
|
|
324
|
+
const r = spawnSync('npm', ['install', '-g', spec], { stdio: 'inherit' });
|
|
325
|
+
console.log(r.status === 0 ? ` installed ${spec}` : ` npm exited ${r.status}; install it by hand`);
|
|
326
|
+
} else {
|
|
327
|
+
console.log(` ${a.name}: npm install -g ${spec} (pinned to the version this installer was released with)`);
|
|
328
|
+
}
|
|
329
|
+
} else if (a.install.script) {
|
|
330
|
+
console.log(` ${a.name}: the vendor installer is a shell script. Download it, read it, then run it:\n curl -fsSL ${a.install.script} -o /tmp/${a.id}-install.sh && less /tmp/${a.id}-install.sh && bash /tmp/${a.id}-install.sh`);
|
|
331
|
+
} else {
|
|
332
|
+
console.log(` ${a.name}: ${a.install.url}` + (a.install.brew ? ` (or: brew install ${a.install.brew})` : ''));
|
|
333
|
+
}
|
|
334
|
+
console.log(` sign in: ${a.auth}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const t of tools) {
|
|
339
|
+
const doc = t.id.toUpperCase() + '.md';
|
|
340
|
+
console.log(`\n${t.name}\n optional: ${t.optionalNote}\n needs: ${t.requires}\n run: ${t.install}\n one-click or self-registering for: ${t.autoClients.join(', ')}. Other agents and the details: ${dir}/${doc}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 7. Activation summary: writing the folder is half the job. Say exactly what
|
|
344
|
+
// turns it on, in order, with one command that proves it.
|
|
345
|
+
const snippet = files.find((f) => /\.snippet\.md$|^PASTE-INTO-YOUR-AGENT\.md$/.test(f.rel));
|
|
346
|
+
const steps = [];
|
|
347
|
+
if (snippet && primary && primary.rulesFile) steps.push(`copy the block in ${join(dir, snippet.rel)} into ${join(project, primary.rulesFile)} (create it if missing)`);
|
|
348
|
+
else if (snippet) steps.push(`paste ${join(dir, snippet.rel)} into ${primary.name}'s custom instructions or Project`);
|
|
349
|
+
if (primary && primary.agentsDir) steps.push(`subagents are in ${join(project, primary.agentsDir)}; run ${primary.bin} from ${project} to pick them up`);
|
|
350
|
+
for (const a of selected.filter((a) => a.bin && a.kind === 'agent-cli')) steps.push(`sign in to ${a.name}: ${a.auth}`);
|
|
351
|
+
for (const t of tools) steps.push(`${t.id}: ${t.install}`);
|
|
352
|
+
if (level >= 2) steps.push(`smoke test: node ${join(dir, 'bin', 'cli-run.mjs')} --doctor (add --run to send each lane one tiny prompt)`);
|
|
353
|
+
if (level >= 3) steps.push(`box: read ${join(dir, 'vm', 'README.md')}; keys named in vm/ENVIRONMENT.md go in your secrets manager, never a file`);
|
|
354
|
+
console.log('\nTo activate, in order:');
|
|
355
|
+
steps.forEach((st, i) => console.log(` ${i + 1}. ${st}`));
|
|
356
|
+
console.log(`\nStart here: ${join(dir, 'README.md')} (written for level ${level} and the AIs you picked).\n`);
|
|
357
|
+
rl && rl.close();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
import { realpathSync } from 'node:fs';
|
|
361
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
362
|
+
function isEntryPoint() {
|
|
363
|
+
try {
|
|
364
|
+
return pathToFileURL(realpathSync(process.argv[1])).href === pathToFileURL(realpathSync(fileURLToPath(import.meta.url))).href;
|
|
365
|
+
} catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (isEntryPoint()) main().catch((e) => {
|
|
370
|
+
console.error('model-orchestrator: ' + (e && e.message ? e.message : e));
|
|
371
|
+
process.exit(e && e.code === 'EOF' ? 2 : 1);
|
|
372
|
+
});
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# docs/
|
|
2
|
+
|
|
3
|
+
The three parts, as reading. The installer writes the working files; these explain the thinking behind them and how to grow from one level to the next.
|
|
4
|
+
|
|
5
|
+
| Part | Read if | File |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| 1 Beginner | you use one LLM or one agent and want it to route well | [part-1-beginner.md](part-1-beginner.md) |
|
|
8
|
+
| 2 Intermediate | you have several AIs and want to call them through their CLIs from one orchestrator | [part-2-intermediate.md](part-2-intermediate.md) |
|
|
9
|
+
| 3 Advanced | you want the whole thing running unattended on a virtual machine | [part-3-advanced.md](part-3-advanced.md) |
|
|
10
|
+
| Audit brief | the threat model and the two adversarial audit rounds this shipped with | [audit-brief.md](audit-brief.md) |
|
|
11
|
+
| Catalog | what each AI and companion tool in the installer is for, how it installs, how it signs in | [catalog.md](catalog.md) |
|
|
12
|
+
|
|
13
|
+
Each part ends with "what the installer gives you at this level" so the doc and the files agree.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# AUDIT_BRIEF.md: adversarial audit of model-orchestrator (round 1)
|
|
2
|
+
|
|
3
|
+
> Historical document. Counts in it (tests, files) are as of the round they describe; `npm test` prints the current number.
|
|
4
|
+
|
|
5
|
+
Read-only audit. Report findings only; do not modify files. Rank by severity. For each finding: file, line, what breaks, a concrete reproduction. `CLEAN` is a valid answer for any area with no reproducible finding. Skip style.
|
|
6
|
+
|
|
7
|
+
## Task bundle
|
|
8
|
+
|
|
9
|
+
**Purpose.** Adversarial, read-only audit of this npm package before it is published, so that reproducible defects are fixed before strangers run it.
|
|
10
|
+
**Task class.** read_only
|
|
11
|
+
|
|
12
|
+
**Granted scope.**
|
|
13
|
+
- Every file under this repository root: `bin/`, `src/`, `templates/`, `test/`, `scripts/`, `docs/`, `package.json`, `README.md`.
|
|
14
|
+
- Anything outside this repository is out of scope. Do not widen it on your own judgment.
|
|
15
|
+
|
|
16
|
+
**Capabilities.** read files, run `npm test`, run `node bin/cli.js` with `--dry`, run `node bin/cli-run.mjs` with bad arguments, run scripts against a temp directory under /tmp.
|
|
17
|
+
|
|
18
|
+
**Denied actions.** Do not modify, create or delete any file in this repository. Do not run `npm install -g`. Do not run any vendor installer. Do not commit, push, or publish. Do not read files outside this repository except /tmp scratch you created. Do not call any network service.
|
|
19
|
+
- Anything absent from Capabilities is denied. Absence is not permission.
|
|
20
|
+
|
|
21
|
+
**Conventions you do not have.** Report in plain prose with a findings list. No style nitpicks. `CLEAN` is a valid verdict per area. Every finding needs a concrete reproduction (command + observed vs expected). Never print a value that looks like a credential.
|
|
22
|
+
|
|
23
|
+
**Report contract.** Return: a severity-ranked list of findings (file, line, what breaks, reproduction, suggested fix in one or two sentences), then a `CLEAN` line for each area in "Attack these" that had no reproducible finding, then a short "not covered" list naming anything you did not check or could not verify.
|
|
24
|
+
|
|
25
|
+
**Exit parameters.** Stop after 12 minutes of wall clock or after reading every file once and running at most 30 commands, whichever comes first. If you hit a bound, report what you have and name what you did not cover. Never return nothing.
|
|
26
|
+
|
|
27
|
+
## What this is
|
|
28
|
+
An npm package (`npx model-orchestrator`) that asks a user which level (1/2/3) and which AIs they have access to, then writes markdown + config templates into a folder, and optionally runs `npm install -g <pkg>` for known packages after an explicit per-package yes. It also ships `bin/cli-run.mjs`, a wrapper that runs one of five agent CLIs (grok, codex, agy, hermes, qwen) and exits non-zero unless the lane produced a deliverable.
|
|
29
|
+
|
|
30
|
+
## Runtime
|
|
31
|
+
Node >= 18, ESM, zero dependencies. Runs on a stranger's laptop (macOS/Linux) with their PATH and HOME. Level 3 writes shell/systemd/compose templates the user will run on a Linux box.
|
|
32
|
+
|
|
33
|
+
## Threat model
|
|
34
|
+
- The user is not an adversary but is careless: runs it in the wrong directory, passes odd flags, has files with the same names.
|
|
35
|
+
- Untrusted input reaches `cli-run.mjs` through CLI stdout (JSON from third-party binaries) and through `lanes.json` on disk.
|
|
36
|
+
- The installer must never: write a secret value anywhere, overwrite a user file without --force, run a remote shell script, escape the target dir (path traversal via template rel paths or --dir), or leave a placeholder unrendered.
|
|
37
|
+
- `cli-run.mjs` must never: throw on malformed CLI output (a throw is misreported as a usage error), pass a secret in argv, leave temp files, hang on stdin, or report success without a deliverable.
|
|
38
|
+
- Generated templates (`vm/setup-vm.sh`, `vm/jobs/weekly-audit.sh`, `docker-compose.yml`, `gateway.config.yaml`) must not put a key in argv, bind to 0.0.0.0, or pipe a remote script into bash.
|
|
39
|
+
|
|
40
|
+
## Already verified (do not repeat)
|
|
41
|
+
- 60 node --test cases pass, including every judge's failure shapes and a mutation check that turns one case red.
|
|
42
|
+
- Placeholders: every template renders for every level and primary without a leftover `{{KEY}}`.
|
|
43
|
+
- README inside `.claude/agents/` is not installed.
|
|
44
|
+
|
|
45
|
+
## Attack these
|
|
46
|
+
1. `bin/cli.js` argument parsing: `opt()` takes the next argv token; what happens with `--dir --force`, `--ais ""`, duplicate flags, `--level 2.5`, unicode, a `--dir` that is a file, a `--dir` of `/`?
|
|
47
|
+
2. `src/install.js` `writeFiles`: path traversal if a template rel path or the --dir resolves outside; symlink in the target dir; mode handling on Windows; partial writes.
|
|
48
|
+
3. `src/detect.js` `which`: PATH entries that are files, empty PATH, relative PATH entries, a directory named like the binary.
|
|
49
|
+
4. `bin/cli-run.mjs`: `jsonLines` on huge output; `maxBuffer`; `spawnSync` with `timeout` and `killSignal` behaviour; `enabledLanes()` with a malicious lanes.json; `--brief` pointing at a directory or a huge file; prompt containing newlines; the codex `-o` temp file when the CLI writes elsewhere; the `import.meta.url === pathToFileURL(argv[1])` main guard when invoked via a symlink; rc pass-through logic (`if (r.status !== 0 && code === OK) code = r.status`).
|
|
50
|
+
5. Templates: `vm/setup-vm.sh` (set -euo pipefail, the for loop over {{NPM_PACKAGES}} when empty), `vm/jobs/weekly-audit.sh` (curl --config - header injection if GATEWAY_MASTER_KEY contains a quote or newline), `docker-compose.yml` env pass-through, systemd unit paths.
|
|
51
|
+
6. Anything that could make the installer write outside `--dir` or read a file it should not.
|
|
52
|
+
|
|
53
|
+
## Design decisions to challenge, with reasoning
|
|
54
|
+
- Zero dependencies (no inquirer): smaller audit surface, but the prompt code is hand-rolled. Is the readline path safe with piped stdin and EOF?
|
|
55
|
+
- Vendor scripts are printed, never run: correct? Or does printing `curl | bash` still encourage the unsafe pattern?
|
|
56
|
+
- `npm install -g` is run after a per-package yes, with the package name from the catalog (never user input). Confirm user input cannot reach that argv.
|
|
57
|
+
- `writeFiles` refuses existing files unless --force but does not check that the target is inside cwd. Deliberate (users may want `~/project`). Is there a traversal risk from template names?
|
|
58
|
+
|
|
59
|
+
## How to run
|
|
60
|
+
`npm test` · `node bin/cli.js --help` · `node bin/cli.js --yes --level 3 --ais claude-code,codex,agy,grok,hermes,qwen,ollama --dir /tmp/x --dry`
|
|
61
|
+
|
|
62
|
+
## ROUND 2 (after round-1 fixes)
|
|
63
|
+
|
|
64
|
+
Re-audit the same scope. Every round-1 finding was reproduced before it was touched. What changed:
|
|
65
|
+
|
|
66
|
+
| # | Finding | Change |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| 1 | writeFiles escape / symlink follow | `preflight()` in `src/install.js`: containment under the resolved root, lstat every existing component (symlink or non-directory parent refused), exclusive `wx` create unless `--force`, rollback of files this run created if a later write fails. Tests: escape, symlinked component, conflicting parent leaves nothing behind. Mutation-checked. |
|
|
69
|
+
| 2 | prompt in argv + logged head | argv kept (each vendor's documented headless shape); DEFERRED as a vendor constraint, documented in `CLI-RUN.md` and the file header (no secrets in prompts, ARG_MAX, reference big briefs by path). Log now stores a 12-hex sha256 prefix and length, never text. Test: marker absent from log. |
|
|
70
|
+
| 3 | unknown flags / missing values | strict `parseArgs` in `bin/cli.js`: unknown flag, missing value, empty value, duplicate, positional → exit 2 before planning. Tests for each. |
|
|
71
|
+
| 4 | audit job hardcoded hermes | `auditLane()` picks the first ENABLED cli-run lane (hermes, qwen, codex, agy, grok); none → rendered guard exits 13. Tests. |
|
|
72
|
+
| 5 | audit never received live state | script composes `reports/audit-brief-<date>.md` = task bundle + protocol + DELEGATION_MATRIX + live-state, and passes THAT as `--brief`. Test. |
|
|
73
|
+
| 6 | `--dir` ignored by systemd paths | `INSTALL_DIR` rendered into the service and the script from the resolved `--dir`. Test. |
|
|
74
|
+
| 7 | curl config injection | script refuses a key not matching `^[A-Za-z0-9._-]+$` (exit 2) before any curl; documented in ENVIRONMENT.md and vm/README. Test executes the rendered script with an injecting key. |
|
|
75
|
+
| 8 | malformed lanes.json fail-open | `enabledLanes()` returns null on present-but-invalid; main refuses every lane (13) and says so. Test proves no spawn happens. |
|
|
76
|
+
| 9 | signal → exit 0 | `r.signal || r.status === null` → verdict killed, exit 10, partial output discarded. Test. |
|
|
77
|
+
| 10 | partial install | covered by preflight + rollback (finding 1). Test. |
|
|
78
|
+
| 11 | directory detected as binary | `isFile()` check in both `which()` implementations. Test. |
|
|
79
|
+
| 12 | `curl \| bash` printed | download / read / run form printed instead. |
|
|
80
|
+
|
|
81
|
+
Also new since round 1: the companion-tool path (`--tools codecalc`, `--no-tools`, `templates/tools/codecalc/`, `protocols/numbers-and-logic.md`, `resolveTools`). Attack it the same way: unknown tool ids, interaction with `--yes`, the extra interactive question, and whether any written snippet could be confused for a file the installer should not touch.
|
|
82
|
+
|
|
83
|
+
Report only what reproduces on the current tree. `CLEAN` per area is expected where the fix holds.
|
package/docs/catalog.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Catalog
|
|
2
|
+
|
|
3
|
+
Generated from `src/catalog.js`. Do not hand-edit; `npm run gen:catalog` rewrites it. Protocols shipped at every level: 6 (counted from `templates/common/protocols/`).
|
|
4
|
+
|
|
5
|
+
## Levels
|
|
6
|
+
|
|
7
|
+
| Level | Name | Tagline | Gives |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| 1 | Beginner | one LLM or agent, routed well | tiers, task classification, every protocol, one agent set up to follow them |
|
|
10
|
+
| 2 | Intermediate | several LLMs and agents, called through their CLIs | everything in Beginner plus cli-run, a delegation matrix, task bundles and three-engine research triage |
|
|
11
|
+
| 3 | Advanced | everything above, plus a virtual machine that runs it unattended | everything in Intermediate plus a gateway config, scheduled jobs, a dispatch layer and privacy gates for a box |
|
|
12
|
+
|
|
13
|
+
## AIs
|
|
14
|
+
|
|
15
|
+
### `claude-code` · Claude Code (Anthropic)
|
|
16
|
+
|
|
17
|
+
- **Kind:** agent-cli · **Access:** subscription · **Lane:** A · **Level:** 1+
|
|
18
|
+
- **Wins at:** orchestrator: routes, maps, builds, verifies, records
|
|
19
|
+
- **Install:** `npm install -g @anthropic-ai/claude-code@2.1.260`
|
|
20
|
+
- **Sign in:** run `claude` once and sign in with your Anthropic account
|
|
21
|
+
- **Reads rules from:** `CLAUDE.md` · subagents in `.claude/agents/`
|
|
22
|
+
|
|
23
|
+
### `codex` · Codex CLI (OpenAI, ChatGPT plan)
|
|
24
|
+
|
|
25
|
+
- **Kind:** agent-cli · **Access:** subscription · **Lane:** A · **Level:** 1+
|
|
26
|
+
- **Wins at:** second coder and adversarial auditor (a different model family reading your diff)
|
|
27
|
+
- **Install:** `npm install -g @openai/codex@0.153.2`
|
|
28
|
+
- **Sign in:** `codex login` (add `--device-auth` on a machine with no browser)
|
|
29
|
+
- **Reads rules from:** `AGENTS.md`
|
|
30
|
+
- **cli-run lane:** yes
|
|
31
|
+
|
|
32
|
+
### `agy` · Antigravity CLI `agy` (Google AI plan)
|
|
33
|
+
|
|
34
|
+
- **Kind:** agent-cli · **Access:** subscription · **Lane:** A · **Level:** 1+
|
|
35
|
+
- **Wins at:** deep research sweeps and concurrent fan-out (its subagent call takes an array)
|
|
36
|
+
- **Install:** vendor script (read it first): `https://antigravity.google/cli/install.sh`
|
|
37
|
+
- **Sign in:** first run opens a device-code sign-in with your Google account
|
|
38
|
+
- **Reads rules from:** `GEMINI.md` · subagents in `.agents/agents/`
|
|
39
|
+
- **cli-run lane:** yes
|
|
40
|
+
- **Note:** Gemini CLI was retired by Google in June 2026. agy is the successor. Do not install `gemini`.
|
|
41
|
+
|
|
42
|
+
### `grok` · Grok CLI (xAI, X Premium)
|
|
43
|
+
|
|
44
|
+
- **Kind:** agent-cli · **Access:** subscription · **Lane:** A · **Level:** 1+
|
|
45
|
+
- **Wins at:** X and live web reads at no per-call cost (its search tools bill on the API, not on the CLI)
|
|
46
|
+
- **Install:** vendor script (read it first): `https://x.ai/cli/install.sh`
|
|
47
|
+
- **Sign in:** `grok login` (add `--device-auth` on a headless machine)
|
|
48
|
+
- **cli-run lane:** yes
|
|
49
|
+
|
|
50
|
+
### `hermes` · Hermes Agent (Nous Research)
|
|
51
|
+
|
|
52
|
+
- **Kind:** agent-cli · **Access:** free · **Lane:** A · **Level:** 2+
|
|
53
|
+
- **Wins at:** the free tier: rough drafts, first-pass summaries, cheap divergent reads, cron jobs on a box
|
|
54
|
+
- **Install:** https://github.com/NousResearch/hermes-agent
|
|
55
|
+
- **Sign in:** `hermes auth add <provider>` per provider; its own fallback chain handles outages
|
|
56
|
+
- **cli-run lane:** yes
|
|
57
|
+
|
|
58
|
+
### `qwen` · Qwen Code CLI (Alibaba, provider-agnostic)
|
|
59
|
+
|
|
60
|
+
- **Kind:** agent-cli · **Access:** metered · **Lane:** B · **Level:** 2+
|
|
61
|
+
- **Wins at:** cheapest metered bulk lane for structured output; never for anything that cites a line, a number or a source
|
|
62
|
+
- **Install:** `npm install -g @qwen-code/qwen-code@0.23.0`
|
|
63
|
+
- **Sign in:** a provider key in an environment variable, named (not stored) in ~/.qwen/settings.json. There is no free Qwen cloud tier any more.
|
|
64
|
+
- **Reads rules from:** `QWEN.md`
|
|
65
|
+
- **cli-run lane:** yes
|
|
66
|
+
- **Note:** Its own success flags lie on API failures. cli-run checks the two honest signals for you.
|
|
67
|
+
|
|
68
|
+
### `ollama` · Ollama (local models)
|
|
69
|
+
|
|
70
|
+
- **Kind:** local · **Access:** local · **Lane:** local · **Level:** 2+
|
|
71
|
+
- **Wins at:** the privacy lane: anything that must never leave the machine. Not a cost lane.
|
|
72
|
+
- **Install:** https://ollama.com/download (or `brew install ollama`)
|
|
73
|
+
- **Sign in:** none
|
|
74
|
+
|
|
75
|
+
### `claude-app` · Claude app or claude.ai (chat only, no CLI)
|
|
76
|
+
|
|
77
|
+
- **Kind:** chat · **Access:** subscription · **Lane:** chat · **Level:** 1+
|
|
78
|
+
- **Wins at:** single-agent use through Projects and custom instructions
|
|
79
|
+
- **Install:** https://claude.ai
|
|
80
|
+
- **Sign in:** sign in
|
|
81
|
+
|
|
82
|
+
### `chatgpt-app` · ChatGPT (chat only, no CLI)
|
|
83
|
+
|
|
84
|
+
- **Kind:** chat · **Access:** subscription · **Lane:** chat · **Level:** 1+
|
|
85
|
+
- **Wins at:** single-agent use through custom instructions and Projects
|
|
86
|
+
- **Install:** https://chatgpt.com
|
|
87
|
+
- **Sign in:** sign in
|
|
88
|
+
|
|
89
|
+
### `gemini-app` · Gemini app (chat only, no CLI)
|
|
90
|
+
|
|
91
|
+
- **Kind:** chat · **Access:** subscription · **Lane:** chat · **Level:** 1+
|
|
92
|
+
- **Wins at:** single-agent use through Gems and saved instructions
|
|
93
|
+
- **Install:** https://gemini.google.com
|
|
94
|
+
- **Sign in:** sign in
|
|
95
|
+
|
|
96
|
+
## Companion tools
|
|
97
|
+
|
|
98
|
+
### `codecalc` · codecalc (calculator, code runner, logic checker for your agent)
|
|
99
|
+
|
|
100
|
+
- **Repo:** https://github.com/The-40-Thieves/codecalc
|
|
101
|
+
- **Gives:** exact arithmetic, code execution in 31 languages, SMT logic checks, complexity and equivalence proofs; offline, no key, no telemetry
|
|
102
|
+
- **Install:** `uvx 'codecalc[full]' setup --write` (needs uv (https://docs.astral.sh/uv/) and Python 3.10+)
|
|
103
|
+
- **Registers itself with:** Claude Code, Claude Desktop, Cursor, VS Code, Zed; snippets for the rest are written to `mcp/`
|
|
104
|
+
- **Default:** selected
|
|
105
|
+
|
|
106
|
+
### `obsidian-tc` · obsidian-tc (governed memory: an agent-ready MCP server over an Obsidian vault)
|
|
107
|
+
|
|
108
|
+
- **Repo:** https://github.com/The-40-Thieves/obsidian-tc
|
|
109
|
+
- **Gives:** durable memory and record for your agents: hybrid retrieval (BM25 + dense + link graph), backlinks, compare-and-swap writes with a confirmation gate, folder ACLs, a poison scan on inferred writes; 163 tools, local by default
|
|
110
|
+
- **Install:** `npm install -g obsidian-tc && obsidian-tc /path/to/your/vault` (needs an Obsidian vault folder (the Obsidian app itself is only needed for live plugin bridges); Node 24+ or Bun 1.1+ (stricter than this installer); Ollama with `nomic-embed-text` for local embeddings, or a cloud embeddings key; the Local REST API plugin only for bridge tools)
|
|
111
|
+
- **Registers itself with:** Cursor, VS Code; snippets for the rest are written to `mcp/`
|
|
112
|
+
- **Default:** not selected
|
|
113
|
+
|