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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Part 1 · Beginner: one LLM or agent, routed well
|
|
2
|
+
|
|
3
|
+
You do not need five vendors to orchestrate. You need three things inside the one agent you already have: **tiers**, **task classes**, and **gates that can fail**.
|
|
4
|
+
|
|
5
|
+
## 1. Route by capability tier, not model name
|
|
6
|
+
|
|
7
|
+
| Tier | For | Effort |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| deep | ambiguous planning, architecture, strategy, root-cause debugging, anything expensive to get wrong | highest |
|
|
10
|
+
| standard | writing code, reviewing code, executing a known plan, synthesizing research | high |
|
|
11
|
+
| fast | classifying, extracting, formatting, bulk summaries | low |
|
|
12
|
+
|
|
13
|
+
If your agent exposes model choice (Claude Code, Codex, Antigravity), map the tiers to its strongest, everyday and cheapest models. If it does not (a chat app), the tiers become **how hard you ask it to think**: a deep task gets a planning turn before anything is written; a fast task gets "table, one row per item, no commentary".
|
|
14
|
+
|
|
15
|
+
Three cost levers, always together: tier (price per token), token discipline (how many tokens: read only what you will touch, never re-read, deliverables not narration), effort (how hard each call thinks).
|
|
16
|
+
|
|
17
|
+
Robustness first, cost second. You split tiers because the split produces better work.
|
|
18
|
+
|
|
19
|
+
## 2. Classify every task, first match wins
|
|
20
|
+
|
|
21
|
+
1. Bulk and mechanical → fast.
|
|
22
|
+
2. Needs live data → standard, with tools. Freshness comes from tools, not from a bigger model, and anything a search returns is a lead, not a fact.
|
|
23
|
+
3. Review without changing → standard, read-only, findings ranked by severity.
|
|
24
|
+
4. Ambiguous, strategic, expensive to get wrong → deep, then hand the plan down.
|
|
25
|
+
5. Everything else that changes files → do it directly at standard. Bounded sub-parts can go down a tier; the main build never goes to a fresh context whole.
|
|
26
|
+
|
|
27
|
+
Modifiers: plan big, execute small · never silently retry a failed attempt at the same tier (escalate once and say so; two consults that do not unstick it means stop and ask) · de-escalate when a request sounds deep but is a lookup.
|
|
28
|
+
|
|
29
|
+
## 3. Gates that can fail
|
|
30
|
+
|
|
31
|
+
> A gate you cannot fail is not a gate.
|
|
32
|
+
|
|
33
|
+
"Does this look good?" passes every time. "Name the single biggest risk and the flaw in the request as filed" can come back empty, which is how you know it worked.
|
|
34
|
+
|
|
35
|
+
Every build gets two checkpoints. **Before writing:** you map what it touches and what could break, then ask the deep tier on the finished map for a named risk and a named flaw. **After it is green:** a fresh context attacks it (bad input, failing dependency, drift from the plan), allowed to answer CLEAN, every finding reproduced before it reaches you. Then the ship, with a rollback named and an explicit yes. Then the loud negative: re-check the old name everywhere and expect zero.
|
|
36
|
+
|
|
37
|
+
## 4. Every hand-off carries a brief
|
|
38
|
+
|
|
39
|
+
A subagent, a fresh chat, a second window holds none of your rules and reads an unspecified edge as an open one. The brief: purpose, task class, granted scope, capabilities, denied actions, conventions it does not have, report contract (what was not done, what is unverified), exit parameters (when to stop, and what to return if it hits the bound). Absence is denial.
|
|
40
|
+
|
|
41
|
+
## 5. The second pass
|
|
42
|
+
|
|
43
|
+
After anything comprehensive, a fresh turn that hunts for what is **missing**, not what is present. Every clause of the ask maps to a step; every step maps to a clause. A clause with no step is dropped scope; a step with no clause is invented scope. Say both.
|
|
44
|
+
|
|
45
|
+
## 6. Deep research, single agent
|
|
46
|
+
|
|
47
|
+
Plan the sub-questions as their own turn and inspect them before spending anything. Sweep. Then a fresh adversarial turn told to attack the premise. Plant one deliberately wrong figure and see whether it corrects it. Mark every claim CONFIRMED / DISAGREEMENT / REPORTED / UNVERIFIED. Agreement is weak evidence; disagreement is the signal.
|
|
48
|
+
|
|
49
|
+
## 7. Numbers and logic are computed, never guessed
|
|
50
|
+
|
|
51
|
+
The one thing tiers and gates cannot fix: a model confident about `0.1 + 0.2` does not feel uncertain, it feels finished. So any figure someone will act on, any comparison you state, any complexity or equivalence claim goes through a tool that computes. The companion for that is [codecalc](https://github.com/The-40-Thieves/codecalc): exact arithmetic, a sandboxed code runner in 31 languages, an SMT logic checker, and proofs that a port or an optimization preserved behaviour. One command registers it with Claude Code, Claude Desktop, Cursor, VS Code or Zed. Without it the rule still binds; use anything that calculates.
|
|
52
|
+
|
|
53
|
+
Logic flow follows the same rule. Reasoning scaffolds help models with no native reasoning mode and add nothing to ones that already think first. The authorising evidence is the computed outcome, never the thought log.
|
|
54
|
+
|
|
55
|
+
## 8. Memory and record
|
|
56
|
+
|
|
57
|
+
Every protocol ends in a write. Search before you write (duplicates are how a store starts lying), correct the folder index in the same pass, one writer per session, mark inferred content as inferred. The optional companion for that is [obsidian-tc](https://github.com/The-40-Thieves/obsidian-tc), a governed MCP server over an Obsidian vault: hybrid search, backlinks, compare-and-swap writes, folder ACLs. It needs an Obsidian vault, Node 24+ or Bun, and Ollama or a cloud embeddings key, so it is off by default; without it the rule still binds against a notes folder and `grep`.
|
|
58
|
+
|
|
59
|
+
## What the installer gives you at this level
|
|
60
|
+
|
|
61
|
+
`README.md` (start here) · `ORCHESTRATOR.md` · `TASK_BUNDLE.md` · `protocols/{build-protocol, propagate, gap-analysis, deep-research, numbers-and-logic, memory-and-record}.md` · `CODECALC.md` and `OBSIDIAN-TC.md` with `mcp/` snippets for the companion tools you selected · the loading surface for your primary agent (Claude Code subagents, Antigravity custom agents, a rules-file snippet, or a paste block for a chat app).
|
|
62
|
+
|
|
63
|
+
## When you have outgrown it
|
|
64
|
+
|
|
65
|
+
You keep wanting a second model family to read your diff, a $0 lane for bulk, or a live-data lane your primary does not have. That is [Part 2](part-2-intermediate.md).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Part 2 · Intermediate: many AIs, called through their CLIs
|
|
2
|
+
|
|
3
|
+
Everything in Part 1, plus lanes. One agent stays the orchestrator; every other AI becomes a lane it calls from the terminal.
|
|
4
|
+
|
|
5
|
+
## 1. Two kinds of lane
|
|
6
|
+
|
|
7
|
+
**Lane A, subscription CLIs.** Claude Code, Codex, Antigravity, Grok, Hermes. Already paid for, $0 per call, used for interactive and agentic work. **Lane B, metered APIs.** Per token, used for programmatic bulk where a subscription CLI cannot serve. **Local.** A privacy lane, never a cost lane.
|
|
8
|
+
|
|
9
|
+
Rule: never spend a frontier token on a task a cheap tier finishes correctly. Escalate on signal, not by default. And an external lane must earn the hop with a real strength; when in doubt, stay in-house.
|
|
10
|
+
|
|
11
|
+
## 2. One job per lane
|
|
12
|
+
|
|
13
|
+
| Lane | Wins at |
|
|
14
|
+
|---|---|
|
|
15
|
+
| the orchestrator (Claude Code, or whichever you chose) | routes, maps, builds, verifies, records; drives the others as CLIs |
|
|
16
|
+
| Codex | second coder and adversarial auditor: a different model family reading your diff |
|
|
17
|
+
| Antigravity `agy` | deep research sweeps; concurrent fan-out (its subagent call takes an array) |
|
|
18
|
+
| Grok CLI | X and live web reads at $0 (the same search on the API bills per call) |
|
|
19
|
+
| Hermes | the free tier: rough drafts, first-pass summaries, divergent reads, cron jobs |
|
|
20
|
+
| Qwen Code + a cheap metered model | structured bulk output; never anything that cites a line, number or source |
|
|
21
|
+
| Ollama | anything that must not leave the machine |
|
|
22
|
+
|
|
23
|
+
One driver, no second AI in the mix: the orchestrator invokes the CLIs; it never hands control to another agent.
|
|
24
|
+
|
|
25
|
+
## 3. Exit 0 is a lie on every lane
|
|
26
|
+
|
|
27
|
+
Every agent CLI can report success and deliver nothing. `bin/cli-run.mjs` builds the right invocation per lane, reads that lane's **native** terminal event, and exits `10` when a run produced no deliverable, `12` on timeout, `13` when the lane is missing. Byte count is not a check either; a run can emit hundreds of kilobytes and no conclusion. One lane's own success flags lie outright (an upstream 400 reported as success), so its judge reads the two honest signals instead.
|
|
28
|
+
|
|
29
|
+
Every call goes through it. "This lane is flaky" becomes a query over its log instead of an argument. `node bin/cli-run.mjs --doctor` is the first thing to run after install: enabled lanes, binaries on PATH, and with `--run` a one-word canary per lane.
|
|
30
|
+
|
|
31
|
+
## 4. Every delegation carries a task bundle, on both surfaces
|
|
32
|
+
|
|
33
|
+
Subagents and CLI lanes are the same problem: something with none of your rules and broad tool access. The brief (purpose, task class, scope, capabilities, denied actions, conventions, report contract, exit parameters) goes in the prompt or in the file passed to `--brief`. If you can, gate it mechanically: a pre-dispatch hook that refuses a brief missing purpose, denied actions or a report contract.
|
|
34
|
+
|
|
35
|
+
## 5. Research: three engines, one triager
|
|
36
|
+
|
|
37
|
+
Fan the same plan to three model families (web sweep, adversarial read, live data), each as one `cli-run` call. The orchestrator opens the primary sources itself, marks every claim, and writes the only durable record. Expect one engine to return confident unsourced numerics; downgrade it. Weight the engines that report their own gaps. Count dispositions, not briefs.
|
|
38
|
+
|
|
39
|
+
## 6. Gap analysis gets a second family
|
|
40
|
+
|
|
41
|
+
The second pass is now a different model reading the same artifact, in read-only audit mode. Disagreement between families is the cheapest signal that something is soft.
|
|
42
|
+
|
|
43
|
+
## 7. The build protocol, bound to lanes
|
|
44
|
+
|
|
45
|
+
Stage 1 Map: the orchestrator sweeps; CLI lanes critique the map at $0. Stage 2: deep tier, a named risk and a named flaw. Stage 4: scanners on the added lines, fail closed. Stage 5: security-shaped diff → the second coder in read-only audit mode; architecture-shaped → deep tier reviewing build against plan; never both. Two deep checkpoints per build; CLI lanes are uncapped.
|
|
46
|
+
|
|
47
|
+
## 8. Privacy gate
|
|
48
|
+
|
|
49
|
+
Name the lanes that never see private notes, client data or personal records. An unnamed bar is not enforced.
|
|
50
|
+
|
|
51
|
+
## 9. Re-derive every figure a cheap lane returns
|
|
52
|
+
|
|
53
|
+
Measured on the cheapest metered lane: conclusions right, 0 of 11 line citations correct, fabricated arithmetic attached to true observations. That survives a skim. So a number from a lane is a lead until a tool computes it: [codecalc](https://github.com/The-40-Thieves/codecalc) on the orchestrator's side, registered for Codex, Antigravity and Qwen Code with the snippets in `CODECALC.md`.
|
|
54
|
+
|
|
55
|
+
## 10. One writer, and a store the lanes can all read
|
|
56
|
+
|
|
57
|
+
With several lanes proposing, the store is where they meet. obsidian-tc (optional) gives every CLI the same `semantic_search`, `get_backlinks` and compare-and-swap `write_note`, with folder ACLs so a research lane can read what it needs and write nothing. The orchestrator stays the one writer.
|
|
58
|
+
|
|
59
|
+
## What the installer gives you at this level
|
|
60
|
+
|
|
61
|
+
Everything from Part 1, plus `ROUTING.md` · `TIERS.md` · `DELEGATION_MATRIX.md` (generated from your selection) · `RESEARCH_TRIAGE.md` · `CLI-RUN.md` · `bin/cli-run.mjs` · `bin/lanes.json`.
|
|
62
|
+
|
|
63
|
+
## When you have outgrown it
|
|
64
|
+
|
|
65
|
+
You want the audit to run on a Monday without you, a gateway so nothing but one process holds a key, and a machine that is always on. That is [Part 3](part-3-advanced.md).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Part 3 · Advanced: everything above, plus a virtual machine
|
|
2
|
+
|
|
3
|
+
A small always-on Linux box owns the schedule. Your laptop stays the interactive driver.
|
|
4
|
+
|
|
5
|
+
## 1. Only the gateway holds keys
|
|
6
|
+
|
|
7
|
+
One OpenAI-compatible gateway (LiteLLM) fronts every metered provider. Nothing else on the box holds a credential: not the orchestrator, not a job, not a container. Rotating a key is a change in one place. The gateway binds to loopback or a private mesh, never to the public interface.
|
|
8
|
+
|
|
9
|
+
Subscription CLIs keep their own sign-in state and stay off the gateway; they are already $0.
|
|
10
|
+
|
|
11
|
+
## 1b. A subscription is not an API key
|
|
12
|
+
|
|
13
|
+
The installer asks which metered API keys you hold separately from which CLIs you use. A Claude Code plan gives you `claude`; it does not give you an Anthropic API key, and the gateway only serves what a key unlocks. Gateway lanes and the variable names in `vm/ENVIRONMENT.md` are rendered from the keys, never from the CLI list.
|
|
14
|
+
|
|
15
|
+
## 2. Bind to aliases, pin the cheapest lane
|
|
16
|
+
|
|
17
|
+
Each gateway lane is an alias (`bulk-cheap`, `standard`, `deep`, `long-context`, `live-fast`, `local-small`), so a vendor rename is a one-line repoint. Each job is pinned to the cheapest lane that does its work, with a token cap, and climbs the ladder (local → free → cheap → standard → deep) only on failure, low confidence, or an explicit "expensive to get wrong".
|
|
18
|
+
|
|
19
|
+
## 3. Dispatch on the box
|
|
20
|
+
|
|
21
|
+
1. Deterministic pre-triage at zero tokens: a keyword table routes the obvious cases.
|
|
22
|
+
2. Judgment dispatch for the rest, by the orchestrator, with a logged reason.
|
|
23
|
+
3. Free lane first.
|
|
24
|
+
4. **Unattended means no human-gated escalation.** An unresolved irreversible call is surfaced and stopped, never executed on a model's confidence.
|
|
25
|
+
5. One writer. Every other engine proposes.
|
|
26
|
+
|
|
27
|
+
## 4. The weekly gap analysis becomes a job
|
|
28
|
+
|
|
29
|
+
A timer enumerates live state (gateway lanes, timers, CLI versions), diffs it against the delegation matrix, and lets the free lane draft the report. It catches the dead lane and the silently renamed model. Its "watched by" line starts as `nothing`, and that line is the one that tells you what to build next.
|
|
30
|
+
|
|
31
|
+
## 5. What runs where
|
|
32
|
+
|
|
33
|
+
| Surface | Role |
|
|
34
|
+
|---|---|
|
|
35
|
+
| the orchestrator CLI | interactive driver over SSH; dispatch brain for jobs |
|
|
36
|
+
| `cli-run` lanes | the other agent CLIs, headless, signed in by device code |
|
|
37
|
+
| the gateway | every metered provider behind one endpoint |
|
|
38
|
+
| a local runtime | the privacy lane |
|
|
39
|
+
| user-level systemd timers | the schedule |
|
|
40
|
+
|
|
41
|
+
Headless Linux gotchas the setup script handles: install a keyring or the CLIs re-prompt for auth on every launch; run device-code sign-ins inside `tmux`; invoke CLIs by absolute path from non-login shells.
|
|
42
|
+
|
|
43
|
+
## 6. Rules that do not bend on a box
|
|
44
|
+
|
|
45
|
+
- No payment card on any compute lane without a human saying so. Free credit only.
|
|
46
|
+
- Local first; cloud only when local genuinely cannot.
|
|
47
|
+
- Private notes, client data and personal records never go to a third-party bulk lane. Name the barred lanes.
|
|
48
|
+
- Nothing binds to `0.0.0.0`.
|
|
49
|
+
- A secret is never printed, never in argv, never in a file in the repo.
|
|
50
|
+
|
|
51
|
+
## 7. Every self-running thing gets one document
|
|
52
|
+
|
|
53
|
+
What and why · trigger · invocation chain · dependencies · reads · writes · **the closed loop (what watches it)** · failure modes · run-and-verify by hand · source of truth. A cold reader must be able to run it, verify it ran, and know who to tell when it breaks, from that one file. An unverified section says UNVERIFIED, never nothing.
|
|
54
|
+
|
|
55
|
+
## 8. codecalc on the box
|
|
56
|
+
|
|
57
|
+
Runs as a stdio MCP server next to the orchestrator CLI: offline, no key, nothing to bind. The weekly audit's figures (lane counts, version deltas, spend) are computed there, not estimated by the free lane that drafts the report. See [codecalc](https://github.com/The-40-Thieves/codecalc).
|
|
58
|
+
|
|
59
|
+
## 9. obsidian-tc on the box
|
|
60
|
+
|
|
61
|
+
Stdio next to the orchestrator, or the upstream Docker service against a bind-mounted vault. Embeddings on the box's Ollama, so nothing leaves the machine. HTTP transport stays off unless every caller is on the private mesh and auth is on.
|
|
62
|
+
|
|
63
|
+
## What the installer gives you at this level
|
|
64
|
+
|
|
65
|
+
Everything from Parts 1 and 2, plus `vm/README.md` · `vm/setup-vm.sh` · `vm/docker-compose.yml` · `vm/gateway.config.yaml` (one lane per provider you selected, keys by name only) · `vm/ENVIRONMENT.md` · `vm/box-CLAUDE.md` · `vm/PRIVACY_GATES.md` · `vm/jobs/` (a weekly audit timer + service, and an index that names what watches each job).
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "model-orchestrator",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "A model orchestrator you can npm run: route every task to the cheapest AI that does it well, across one agent, many CLIs, or a whole virtual machine. Three levels, one installer that asks what you have access to.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"model-orchestrator": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"templates",
|
|
13
|
+
"docs",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"CHANGELOG.md",
|
|
17
|
+
"SECURITY.md",
|
|
18
|
+
"scripts"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "node bin/cli.js",
|
|
22
|
+
"test": "node --test",
|
|
23
|
+
"prepublishOnly": "npm test",
|
|
24
|
+
"dry-run": "node bin/cli.js --yes --level 2 --ais claude-code,codex,grok --dir ./tmp-dry-run --dry",
|
|
25
|
+
"gen:catalog": "node scripts/gen-catalog.js"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/aunysillyme/model-orchestrator.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/aunysillyme/model-orchestrator#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/aunysillyme/model-orchestrator/issues"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"ai",
|
|
40
|
+
"orchestrator",
|
|
41
|
+
"model-router",
|
|
42
|
+
"claude-code",
|
|
43
|
+
"codex",
|
|
44
|
+
"antigravity",
|
|
45
|
+
"grok",
|
|
46
|
+
"ollama",
|
|
47
|
+
"litellm",
|
|
48
|
+
"multi-agent",
|
|
49
|
+
"cli"
|
|
50
|
+
],
|
|
51
|
+
"license": "MIT"
|
|
52
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Regenerates docs/catalog.md from src/catalog.js. The test suite checks they agree.
|
|
3
|
+
import { writeFileSync } from 'node:fs';
|
|
4
|
+
import { AIS, LEVELS, TOOLS, npmSpec } from '../src/catalog.js';
|
|
5
|
+
import { readdirSync } from 'node:fs';
|
|
6
|
+
|
|
7
|
+
export function protocolCount() {
|
|
8
|
+
return readdirSync(new URL('../templates/common/protocols/', import.meta.url)).filter((f) => f.endsWith('.md') && f !== 'README.md').length;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function catalogMarkdown() {
|
|
12
|
+
let md = '# Catalog\n\nGenerated from `src/catalog.js`. Do not hand-edit; `npm run gen:catalog` rewrites it. Protocols shipped at every level: ' + protocolCount() + ' (counted from `templates/common/protocols/`).\n\n## Levels\n\n| Level | Name | Tagline | Gives |\n|---|---|---|---|\n';
|
|
13
|
+
for (const l of LEVELS) md += `| ${l.id} | ${l.name} | ${l.tagline} | ${l.gives} |\n`;
|
|
14
|
+
md += '\n## AIs\n\n';
|
|
15
|
+
for (const a of AIS) {
|
|
16
|
+
const how = a.install.npm
|
|
17
|
+
? '`npm install -g ' + npmSpec(a) + '`'
|
|
18
|
+
: a.install.script
|
|
19
|
+
? 'vendor script (read it first): `' + a.install.script + '`'
|
|
20
|
+
: a.install.url + (a.install.brew ? ' (or `brew install ' + a.install.brew + '`)' : '');
|
|
21
|
+
md += `### \`${a.id}\` · ${a.name}\n\n- **Kind:** ${a.kind} · **Access:** ${a.access} · **Lane:** ${a.lane} · **Level:** ${a.minLevel}+\n- **Wins at:** ${a.role}\n- **Install:** ${how}\n- **Sign in:** ${a.auth}\n`;
|
|
22
|
+
if (a.rulesFile) md += `- **Reads rules from:** \`${a.rulesFile}\`` + (a.agentsDir ? ` · subagents in \`${a.agentsDir}/\`` : '') + '\n';
|
|
23
|
+
if (a.cliRun) md += '- **cli-run lane:** yes\n';
|
|
24
|
+
if (a.note) md += `- **Note:** ${a.note}\n`;
|
|
25
|
+
md += '\n';
|
|
26
|
+
}
|
|
27
|
+
md += '## Companion tools\n\n';
|
|
28
|
+
for (const t of TOOLS) {
|
|
29
|
+
md += `### \`${t.id}\` · ${t.name}\n\n- **Repo:** ${t.repo}\n- **Gives:** ${t.role}\n- **Install:** \`${t.install}\` (needs ${t.requires})\n- **Registers itself with:** ${t.autoClients.join(', ')}; snippets for the rest are written to \`mcp/\`\n- **Default:** ${t.recommended ? 'selected' : 'not selected'}\n\n`;
|
|
30
|
+
}
|
|
31
|
+
return md;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (process.argv[1] && process.argv[1].endsWith('gen-catalog.js')) {
|
|
35
|
+
writeFileSync(new URL('../docs/catalog.md', import.meta.url), catalogMarkdown());
|
|
36
|
+
console.log('docs/catalog.md regenerated');
|
|
37
|
+
}
|
package/src/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# src/
|
|
2
|
+
|
|
3
|
+
| File | Job |
|
|
4
|
+
|---|---|
|
|
5
|
+
| `catalog.js` | the single list of levels and AIs. Add an AI here and the prompts, docs tables, delegation matrix, gateway config and installer all pick it up. Nothing else lists AIs. |
|
|
6
|
+
| `detect.js` | PATH lookup for a binary, plus the few places vendor installers drop binaries without touching PATH. No shell-outs. |
|
|
7
|
+
| `install.js` | pure planner: turns (level, selection, primary) into a list of files to write, rendering templates and computing every generated table. `writeFiles` is the only thing that touches disk. |
|
|
8
|
+
| `prompt.js` | line-buffered questions for the interactive path; piped answers are queued, EOF mid-prompt aborts instead of confirming a write. |
|
|
9
|
+
| `render.js` | `{{KEY}}` substitution. Throws on an unknown key, so a template typo fails the test suite instead of shipping a literal placeholder. |
|
package/src/catalog.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// The single source the installer, the docs and the generated files all read.
|
|
2
|
+
// Add an AI here and every level picks it up. Nothing else lists AIs.
|
|
3
|
+
//
|
|
4
|
+
// Fields
|
|
5
|
+
// id stable key used in --ais and in generated files
|
|
6
|
+
// name what the prompt shows
|
|
7
|
+
// kind 'agent-cli' (a terminal agent), 'chat' (a chat app, no CLI), 'local' (a local model runtime)
|
|
8
|
+
// bin binary to look for on PATH, or null
|
|
9
|
+
// access 'subscription' ($0 per call on a plan you already pay for), 'metered' (per token), 'free', 'local'
|
|
10
|
+
// lane 'A' = subscription CLI, 'B' = metered API, 'local' = stays on the machine
|
|
11
|
+
// role the one job it wins at in a multi-AI stack
|
|
12
|
+
// minLevel 1 beginner, 2 intermediate, 3 advanced
|
|
13
|
+
// install { npm: pkg } for a global npm install the installer may run after you say yes,
|
|
14
|
+
// { script: url } for a vendor shell installer the installer only PRINTS, never runs,
|
|
15
|
+
// { url } for a download page
|
|
16
|
+
// auth how you sign in, always the vendor's own flow, never a key typed into this tool
|
|
17
|
+
// rulesFile the instructions file that agent reads from a project root, if any
|
|
18
|
+
// agentsDir where that agent keeps project-level subagent definitions, if any
|
|
19
|
+
// cliRun true when bin/cli-run.mjs has a judge for this lane
|
|
20
|
+
|
|
21
|
+
export const LEVELS = [
|
|
22
|
+
{
|
|
23
|
+
id: 1,
|
|
24
|
+
key: 'beginner',
|
|
25
|
+
name: 'Beginner',
|
|
26
|
+
tagline: 'one LLM or agent, routed well',
|
|
27
|
+
gives: 'tiers, task classification, every protocol, one agent set up to follow them'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 2,
|
|
31
|
+
key: 'intermediate',
|
|
32
|
+
name: 'Intermediate',
|
|
33
|
+
tagline: 'several LLMs and agents, called through their CLIs',
|
|
34
|
+
gives: 'everything in Beginner plus cli-run, a delegation matrix, task bundles and three-engine research triage'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 3,
|
|
38
|
+
key: 'advanced',
|
|
39
|
+
name: 'Advanced',
|
|
40
|
+
tagline: 'everything above, plus a virtual machine that runs it unattended',
|
|
41
|
+
gives: 'everything in Intermediate plus a gateway config, scheduled jobs, a dispatch layer and privacy gates for a box'
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export const AIS = [
|
|
46
|
+
{
|
|
47
|
+
id: 'claude-code',
|
|
48
|
+
name: 'Claude Code (Anthropic)',
|
|
49
|
+
vendor: 'Anthropic',
|
|
50
|
+
kind: 'agent-cli',
|
|
51
|
+
bin: 'claude',
|
|
52
|
+
access: 'subscription',
|
|
53
|
+
lane: 'A',
|
|
54
|
+
role: 'orchestrator: routes, maps, builds, verifies, records',
|
|
55
|
+
minLevel: 1,
|
|
56
|
+
install: { npm: '@anthropic-ai/claude-code', pin: '2.1.260' },
|
|
57
|
+
auth: 'run `claude` once and sign in with your Anthropic account',
|
|
58
|
+
rulesFile: 'CLAUDE.md',
|
|
59
|
+
agentsDir: '.claude/agents',
|
|
60
|
+
cliRun: false,
|
|
61
|
+
models: { deep: 'opus', standard: 'sonnet', fast: 'haiku' }
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'codex',
|
|
65
|
+
name: 'Codex CLI (OpenAI, ChatGPT plan)',
|
|
66
|
+
vendor: 'OpenAI',
|
|
67
|
+
kind: 'agent-cli',
|
|
68
|
+
bin: 'codex',
|
|
69
|
+
access: 'subscription',
|
|
70
|
+
lane: 'A',
|
|
71
|
+
role: 'second coder and adversarial auditor (a different model family reading your diff)',
|
|
72
|
+
minLevel: 1,
|
|
73
|
+
install: { npm: '@openai/codex', pin: '0.153.2' },
|
|
74
|
+
auth: '`codex login` (add `--device-auth` on a machine with no browser)',
|
|
75
|
+
rulesFile: 'AGENTS.md',
|
|
76
|
+
agentsDir: null,
|
|
77
|
+
cliRun: true
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'agy',
|
|
81
|
+
name: 'Antigravity CLI `agy` (Google AI plan)',
|
|
82
|
+
vendor: 'Google',
|
|
83
|
+
kind: 'agent-cli',
|
|
84
|
+
bin: 'agy',
|
|
85
|
+
access: 'subscription',
|
|
86
|
+
lane: 'A',
|
|
87
|
+
role: 'deep research sweeps and concurrent fan-out (its subagent call takes an array)',
|
|
88
|
+
minLevel: 1,
|
|
89
|
+
install: { script: 'https://antigravity.google/cli/install.sh' },
|
|
90
|
+
auth: 'first run opens a device-code sign-in with your Google account',
|
|
91
|
+
rulesFile: 'GEMINI.md',
|
|
92
|
+
agentsDir: '.agents/agents',
|
|
93
|
+
cliRun: true,
|
|
94
|
+
models: { deep: 'pro', standard: 'flash', fast: 'flash' },
|
|
95
|
+
note: 'Gemini CLI was retired by Google in June 2026. agy is the successor. Do not install `gemini`.'
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'grok',
|
|
99
|
+
name: 'Grok CLI (xAI, X Premium)',
|
|
100
|
+
vendor: 'xAI',
|
|
101
|
+
kind: 'agent-cli',
|
|
102
|
+
bin: 'grok',
|
|
103
|
+
access: 'subscription',
|
|
104
|
+
lane: 'A',
|
|
105
|
+
role: 'X and live web reads at no per-call cost (its search tools bill on the API, not on the CLI)',
|
|
106
|
+
minLevel: 1,
|
|
107
|
+
install: { script: 'https://x.ai/cli/install.sh' },
|
|
108
|
+
auth: '`grok login` (add `--device-auth` on a headless machine)',
|
|
109
|
+
rulesFile: null,
|
|
110
|
+
agentsDir: null,
|
|
111
|
+
cliRun: true
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'hermes',
|
|
115
|
+
name: 'Hermes Agent (Nous Research)',
|
|
116
|
+
vendor: 'Nous Research',
|
|
117
|
+
kind: 'agent-cli',
|
|
118
|
+
bin: 'hermes',
|
|
119
|
+
access: 'free',
|
|
120
|
+
lane: 'A',
|
|
121
|
+
role: 'the free tier: rough drafts, first-pass summaries, cheap divergent reads, cron jobs on a box',
|
|
122
|
+
minLevel: 2,
|
|
123
|
+
install: { url: 'https://github.com/NousResearch/hermes-agent' },
|
|
124
|
+
auth: '`hermes auth add <provider>` per provider; its own fallback chain handles outages',
|
|
125
|
+
rulesFile: null,
|
|
126
|
+
agentsDir: null,
|
|
127
|
+
cliRun: true
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'qwen',
|
|
131
|
+
name: 'Qwen Code CLI (Alibaba, provider-agnostic)',
|
|
132
|
+
vendor: 'Alibaba',
|
|
133
|
+
kind: 'agent-cli',
|
|
134
|
+
bin: 'qwen',
|
|
135
|
+
access: 'metered',
|
|
136
|
+
lane: 'B',
|
|
137
|
+
role: 'cheapest metered bulk lane for structured output; never for anything that cites a line, a number or a source',
|
|
138
|
+
minLevel: 2,
|
|
139
|
+
install: { npm: '@qwen-code/qwen-code', pin: '0.23.0' },
|
|
140
|
+
auth: 'a provider key in an environment variable, named (not stored) in ~/.qwen/settings.json. There is no free Qwen cloud tier any more.',
|
|
141
|
+
rulesFile: 'QWEN.md',
|
|
142
|
+
agentsDir: null,
|
|
143
|
+
cliRun: true,
|
|
144
|
+
note: 'Its own success flags lie on API failures. cli-run checks the two honest signals for you.'
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'ollama',
|
|
148
|
+
name: 'Ollama (local models)',
|
|
149
|
+
vendor: 'Ollama',
|
|
150
|
+
kind: 'local',
|
|
151
|
+
bin: 'ollama',
|
|
152
|
+
access: 'local',
|
|
153
|
+
lane: 'local',
|
|
154
|
+
role: 'the privacy lane: anything that must never leave the machine. Not a cost lane.',
|
|
155
|
+
minLevel: 2,
|
|
156
|
+
install: { url: 'https://ollama.com/download', brew: 'ollama' },
|
|
157
|
+
auth: 'none',
|
|
158
|
+
rulesFile: null,
|
|
159
|
+
agentsDir: null,
|
|
160
|
+
cliRun: false
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'claude-app',
|
|
164
|
+
name: 'Claude app or claude.ai (chat only, no CLI)',
|
|
165
|
+
vendor: 'Anthropic',
|
|
166
|
+
kind: 'chat',
|
|
167
|
+
bin: null,
|
|
168
|
+
access: 'subscription',
|
|
169
|
+
lane: 'chat',
|
|
170
|
+
role: 'single-agent use through Projects and custom instructions',
|
|
171
|
+
minLevel: 1,
|
|
172
|
+
install: { url: 'https://claude.ai' },
|
|
173
|
+
auth: 'sign in',
|
|
174
|
+
rulesFile: null,
|
|
175
|
+
agentsDir: null,
|
|
176
|
+
cliRun: false
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'chatgpt-app',
|
|
180
|
+
name: 'ChatGPT (chat only, no CLI)',
|
|
181
|
+
vendor: 'OpenAI',
|
|
182
|
+
kind: 'chat',
|
|
183
|
+
bin: null,
|
|
184
|
+
access: 'subscription',
|
|
185
|
+
lane: 'chat',
|
|
186
|
+
role: 'single-agent use through custom instructions and Projects',
|
|
187
|
+
minLevel: 1,
|
|
188
|
+
install: { url: 'https://chatgpt.com' },
|
|
189
|
+
auth: 'sign in',
|
|
190
|
+
rulesFile: null,
|
|
191
|
+
agentsDir: null,
|
|
192
|
+
cliRun: false
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: 'gemini-app',
|
|
196
|
+
name: 'Gemini app (chat only, no CLI)',
|
|
197
|
+
vendor: 'Google',
|
|
198
|
+
kind: 'chat',
|
|
199
|
+
bin: null,
|
|
200
|
+
access: 'subscription',
|
|
201
|
+
lane: 'chat',
|
|
202
|
+
role: 'single-agent use through Gems and saved instructions',
|
|
203
|
+
minLevel: 1,
|
|
204
|
+
install: { url: 'https://gemini.google.com' },
|
|
205
|
+
auth: 'sign in',
|
|
206
|
+
rulesFile: null,
|
|
207
|
+
agentsDir: null,
|
|
208
|
+
cliRun: false
|
|
209
|
+
}
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
// Companion tools: not AIs, but things the AIs call. Asked about separately.
|
|
213
|
+
export const TOOLS = [
|
|
214
|
+
{
|
|
215
|
+
id: 'codecalc',
|
|
216
|
+
name: 'codecalc (calculator, code runner, logic checker for your agent)',
|
|
217
|
+
repo: 'https://github.com/The-40-Thieves/codecalc',
|
|
218
|
+
role: 'exact arithmetic, code execution in 31 languages, SMT logic checks, complexity and equivalence proofs; offline, no key, no telemetry',
|
|
219
|
+
install: "uvx 'codecalc[full]' setup --write",
|
|
220
|
+
pin: '0.5.0',
|
|
221
|
+
requires: 'uv (https://docs.astral.sh/uv/) and Python 3.10+',
|
|
222
|
+
autoClients: ['Claude Code', 'Claude Desktop', 'Cursor', 'VS Code', 'Zed'],
|
|
223
|
+
recommended: true,
|
|
224
|
+
optionalNote: 'Optional. Needs Python 3.10+ and uv. Everything else runs offline.'
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'obsidian-tc',
|
|
228
|
+
name: 'obsidian-tc (governed memory: an agent-ready MCP server over an Obsidian vault)',
|
|
229
|
+
repo: 'https://github.com/The-40-Thieves/obsidian-tc',
|
|
230
|
+
role: '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',
|
|
231
|
+
install: 'npm install -g obsidian-tc && obsidian-tc /path/to/your/vault',
|
|
232
|
+
pin: '1.26.0',
|
|
233
|
+
requires: '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',
|
|
234
|
+
autoClients: ['Cursor', 'VS Code'],
|
|
235
|
+
recommended: false,
|
|
236
|
+
optionalNote: 'Optional and heavier than codecalc. Skip it if you do not keep notes in Obsidian. AGPL-3.0.'
|
|
237
|
+
}
|
|
238
|
+
];
|
|
239
|
+
export const toolById = Object.fromEntries(TOOLS.map((t) => [t.id, t]));
|
|
240
|
+
|
|
241
|
+
// Metered API providers for the level 3 gateway. Separate from the AI list on
|
|
242
|
+
// purpose: a Claude Code subscription is not an Anthropic API key, and a user
|
|
243
|
+
// can truthfully have one without the other. Only NAMES of variables live here.
|
|
244
|
+
export const PROVIDERS = [
|
|
245
|
+
{ id: 'anthropic', name: 'Anthropic API', envName: 'ANTHROPIC_API_KEY', lanes: [['standard', 'anthropic/claude-sonnet-5'], ['deep', 'anthropic/claude-opus-5']] },
|
|
246
|
+
{ id: 'openai', name: 'OpenAI API', envName: 'OPENAI_API_KEY', lanes: [['second-opinion', 'openai/gpt-5.6-terra']] },
|
|
247
|
+
{ id: 'google', name: 'Google Gemini API', envName: 'GEMINI_API_KEY', lanes: [['long-context', 'gemini/gemini-3.1-pro']] },
|
|
248
|
+
{ id: 'xai', name: 'xAI API', envName: 'XAI_API_KEY', lanes: [['live-fast', 'xai/grok-4.1-fast']] },
|
|
249
|
+
{ id: 'openrouter', name: 'OpenRouter (many cheap models, one key)', envName: 'OPENROUTER_API_KEY', lanes: [['bulk-cheap', 'openrouter/qwen/qwen3.7-flash']] }
|
|
250
|
+
];
|
|
251
|
+
export const providerById = Object.fromEntries(PROVIDERS.map((p) => [p.id, p]));
|
|
252
|
+
|
|
253
|
+
// Container image pins for the level 3 templates. Bump deliberately; a
|
|
254
|
+
// reviewed box should not change underneath the user on a restart.
|
|
255
|
+
export const IMAGES = {
|
|
256
|
+
litellm: 'ghcr.io/berriai/litellm:v1.99.1',
|
|
257
|
+
ollama: 'ollama/ollama:0.33.3'
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export const byId = Object.fromEntries(AIS.map((a) => [a.id, a]));
|
|
261
|
+
|
|
262
|
+
// The ONE place an npm install spec is built. Interactive install, the printed
|
|
263
|
+
// fallback command, the install table and the box script all call this, so
|
|
264
|
+
// two users on two paths get the same version.
|
|
265
|
+
export function npmSpec(a) {
|
|
266
|
+
if (!a.install || !a.install.npm) return null;
|
|
267
|
+
return a.install.npm + (a.install.pin ? '@' + a.install.pin : '');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function aisForLevel(level) {
|
|
271
|
+
return AIS.filter((a) => a.minLevel <= level);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function agentCandidates(selected) {
|
|
275
|
+
// Which of the selected AIs can be the single primary agent at level 1.
|
|
276
|
+
return selected.filter((a) => a.kind === 'agent-cli' || a.kind === 'chat');
|
|
277
|
+
}
|
package/src/detect.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { accessSync, statSync, constants } from 'node:fs';
|
|
2
|
+
import { delimiter, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
// PATH lookup plus the handful of places vendor installers drop binaries
|
|
6
|
+
// without touching PATH. Never a shell function, never a shell out.
|
|
7
|
+
// A directory with the binary's name is not a binary (X_OK passes on
|
|
8
|
+
// searchable directories), so the candidate must be a regular file.
|
|
9
|
+
export function which(bin) {
|
|
10
|
+
if (!bin) return null;
|
|
11
|
+
const home = homedir();
|
|
12
|
+
const searchPath = process.env['PATH'] || '';
|
|
13
|
+
const dirs = searchPath.split(delimiter).filter(Boolean);
|
|
14
|
+
dirs.push(join(home, '.local', 'bin'), join(home, '.grok', 'bin'), join(home, '.npm-global', 'bin'));
|
|
15
|
+
for (const d of dirs) {
|
|
16
|
+
const p = join(d, bin);
|
|
17
|
+
try {
|
|
18
|
+
if (!statSync(p).isFile()) continue;
|
|
19
|
+
accessSync(p, constants.X_OK);
|
|
20
|
+
return p;
|
|
21
|
+
} catch {
|
|
22
|
+
/* keep looking */
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|