spectoflow 0.27.2 → 0.29.0
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/README.md +58 -7
- package/bin/spectoflow.js +53 -3
- package/lib/adapters.js +59 -27
- package/lib/brain-setup.js +165 -0
- package/lib/brain.js +257 -0
- package/lib/dashboard/handlers.js +22 -2
- package/lib/dashboard/hub-server.js +22 -2
- package/lib/dashboard/ops.js +25 -3
- package/lib/dashboard/orchestrator.js +4 -4
- package/lib/dashboard/public/app.js +169 -7
- package/lib/dashboard/public/i18n.js +19 -13
- package/lib/dashboard/public/icons.js +2 -0
- package/lib/dashboard/public/index.html +21 -2
- package/lib/dashboard/public/styles.css +46 -0
- package/lib/dashboard/routes.js +7 -0
- package/lib/dashboard/runner.js +26 -2
- package/lib/global-config.js +4 -3
- package/lib/init.js +6 -2
- package/lib/mcp-server.js +115 -0
- package/lib/mcp.js +13 -10
- package/lib/update.js +42 -3
- package/package.json +1 -1
- package/templates/README.md +8 -4
- package/templates/{AGENTS.md → SPECTOFLOW.md} +22 -2
- package/templates/capabilities.md +1 -1
- package/templates/config.json +1 -0
- package/templates/skills/clarify/SKILL.md +1 -1
- package/templates/skills/generate-agent/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -91,6 +91,9 @@ spectoflow dashboard init [--path=<dir>] create/move the dashboard workspa
|
|
|
91
91
|
spectoflow dashboard validate <file> check a custom-view JSON against the block schema
|
|
92
92
|
spectoflow config [get|set <key> [<value>]] global defaults + dashboard URL/path (~/.spectoflow/config.json)
|
|
93
93
|
|
|
94
|
+
spectoflow brain your second brain: entries, and which agents can reach it
|
|
95
|
+
spectoflow brain setup [--dry-run] connect your installed agents to it (once per machine)
|
|
96
|
+
|
|
94
97
|
spectoflow skill create "..." | --auto generate a project skill
|
|
95
98
|
spectoflow agent create "..." | --auto generate a project agent
|
|
96
99
|
|
|
@@ -118,18 +121,20 @@ detected it falls back to claude + codex.
|
|
|
118
121
|
**Empty project** → your agent asks what to build and runs Intake (brainstorm → analysis → spec → plan).
|
|
119
122
|
**Existing project** → an existing `CLAUDE.md` is preserved as `CLAUDE.md.tomerge` (merged on first run);
|
|
120
123
|
an existing `AGENTS.md` or `GEMINI.md` is kept as-is and gets a short, delimited spectoflow section
|
|
121
|
-
appended (`<!-- spectoflow:start -->` … `<!-- spectoflow:end -->`) pointing to `.spectoflow/
|
|
124
|
+
appended (`<!-- spectoflow:start -->` … `<!-- spectoflow:end -->`) pointing to `.spectoflow/SPECTOFLOW.md` —
|
|
122
125
|
`spectoflow update` adds it too to a project installed before that; existing `plans/*.md` tasks are given
|
|
123
126
|
stable ids.
|
|
124
127
|
|
|
125
|
-
**
|
|
126
|
-
reads natively. The real brain (intent router, workflow, rules) is
|
|
127
|
-
framework and refreshed by `spectoflow update`.
|
|
128
|
+
**Entry files vs. the brain.** `CLAUDE.md`, `AGENTS.md` and `GEMINI.md` at your project root are thin
|
|
129
|
+
pointers, in the place each agent reads natively. The real brain (intent router, workflow, rules) is
|
|
130
|
+
`.spectoflow/SPECTOFLOW.md`, owned by the framework and refreshed by `spectoflow update`. (Before 0.28 it
|
|
131
|
+
was named `.spectoflow/AGENTS.md`, easy to confuse with the root one — `update` renames it, keeping any
|
|
132
|
+
edits you made, and rewrites the old path in your entry files.)
|
|
128
133
|
|
|
129
134
|
## Update
|
|
130
135
|
|
|
131
136
|
`init` is idempotent (it never overwrites), so it can't refresh an installed project. `spectoflow
|
|
132
|
-
update` refreshes **framework-owned** files (engine, `
|
|
137
|
+
update` refreshes **framework-owned** files (engine, `SPECTOFLOW.md`, `capabilities.md`, `policy.md`,
|
|
133
138
|
default agents & skills) to the CLI's version — retiring the project's own vendored dashboard folder
|
|
134
139
|
along the way for anyone updating from before v0.24 — while **preserving your work** —
|
|
135
140
|
`config.json`, `workflow.md`, `specs/`, `plans/`, and any agent/skill you created or edited are never
|
|
@@ -254,7 +259,7 @@ it grows one extra, optional connection outward:
|
|
|
254
259
|
| `spectoflow dashboard logout` | your machine | unlink the machine entirely |
|
|
255
260
|
|
|
256
261
|
The header bar always shows the brand, the **active agent**, autonomy mode, language, a global-progress
|
|
257
|
-
meter, a sync dot, and a **Run** quick-action.
|
|
262
|
+
meter, a sync dot, and a **Run** quick-action. Fourteen tabs — and **which ones you see, and in what
|
|
258
263
|
order, is up to you** (Personalize → *Navigation tabs*: enable / disable / reorder; two of them ship
|
|
259
264
|
off by default):
|
|
260
265
|
|
|
@@ -277,6 +282,8 @@ off by default):
|
|
|
277
282
|
- **Info** — a project-at-a-glance summary.
|
|
278
283
|
- **Documentation** — the live supported-agents table (your own install status + links) plus the CLI
|
|
279
284
|
command reference.
|
|
285
|
+
- **Second brain** — what spectoflow has learned about you, shared by all your projects: read, add, fix,
|
|
286
|
+
confirm (see [Second brain](#second-brain)). Local only.
|
|
280
287
|
- **Personalize** — autonomy mode, language, design, the active agent, **navigation tabs**, **slash
|
|
281
288
|
commands**, and **Extend spectoflow** (see *Customize* below).
|
|
282
289
|
|
|
@@ -341,6 +348,50 @@ spectoflow agent create "owns accessibility review" # or: --auto
|
|
|
341
348
|
spectoflow dashboard create "a KPI overview for support" # or: --auto
|
|
342
349
|
```
|
|
343
350
|
|
|
351
|
+
## Second brain
|
|
352
|
+
|
|
353
|
+
spectoflow learns about **you** as you work, and remembers it across all your projects: your role, your
|
|
354
|
+
preferences, how you like to work, what to avoid. Every agent session starts with it, whatever the agent.
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
you work, in any project, with any agent
|
|
358
|
+
│ "commit messages in English, always"
|
|
359
|
+
▼
|
|
360
|
+
the agent records it ── brain_learn ──► ~/.spectoflow/brain.md (one file, yours, never in a repo)
|
|
361
|
+
│
|
|
362
|
+
┌──────────────────────────────────────────┼─────────────────────────────┐
|
|
363
|
+
▼ ▼ ▼
|
|
364
|
+
next session, any project: dashboard → Second brain tab: you can edit the file
|
|
365
|
+
the agent starts with it read, add, fix, confirm by hand, too
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**One-time setup, per machine:**
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
spectoflow brain setup --dry-run # see what it would change
|
|
372
|
+
spectoflow brain setup # connect every coding agent installed on this machine
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
It registers a small MCP server, `spectoflow mcp`, in each installed agent's **user-level** config (Claude
|
|
376
|
+
Code, Codex, Cursor, Gemini, OpenCode, Kiro, Antigravity, Copilot, Amazon Q, Droid, Auggie, Kimi; Goose
|
|
377
|
+
gets a snippet to paste). It never touches an existing entry and never rewrites a file it can't parse. The
|
|
378
|
+
agents then read and grow your second brain through that server: nothing is copied into your projects.
|
|
379
|
+
|
|
380
|
+
- **Four categories:** Profile, Preferences, Working style, Avoid.
|
|
381
|
+
- **Facts an agent records through MCP are added directly by default.** To review them first, untick *Add what the agent learns
|
|
382
|
+
directly* on the page, or run `spectoflow config set brain.autoAdd false`: new facts then wait in
|
|
383
|
+
*To confirm*.
|
|
384
|
+
- **What gets recorded:** durable facts only. Never secrets, credentials or sensitive personal data, never
|
|
385
|
+
a one-off instruction. The rule is written in every agent's instructions and in the MCP tools themselves.
|
|
386
|
+
- **Runs launched from the dashboard:** non-interactive agents often refuse MCP tools, so the agent prints a
|
|
387
|
+
`::spectoflow learn category=… msg=…` line instead. Those always wait in *To confirm*, whatever the setting:
|
|
388
|
+
that output also carries command output and file contents, so a line hidden in a repository can't slip
|
|
389
|
+
in unseen.
|
|
390
|
+
- **Private:** the Second brain tab and its API answer this machine only — not the online dashboard
|
|
391
|
+
(`server/` refuses them for everyone, the project owner included), and not other machines on your network.
|
|
392
|
+
A run started from either of those can't write into it, and a learned fact never goes into a project's chat
|
|
393
|
+
log.
|
|
394
|
+
|
|
344
395
|
## Agents vs skills
|
|
345
396
|
|
|
346
397
|
Agents (`.spectoflow/agents/`) are **stable team personas** (Product Manager, Developer, QA Engineer…).
|
|
@@ -353,7 +404,7 @@ INVEST, Playwright E2E, Conventional Commits, and more — not generic one-liner
|
|
|
353
404
|
### Clarify before acting
|
|
354
405
|
|
|
355
406
|
spectoflow is an **expert analyst, not an order-taker**. When a request is vague ("login displays
|
|
356
|
-
badly, users can't sign in"), an always-on **Clarify reflex** — in the agent's memory (`
|
|
407
|
+
badly, users can't sign in"), an always-on **Clarify reflex** — in the agent's memory (`SPECTOFLOW.md`)
|
|
357
408
|
and backed by the `clarify` skill — reflects it back and asks **one targeted question at a time**,
|
|
358
409
|
each with a recommendation anchored in the project's goals and best practices, until the need is
|
|
359
410
|
crisp; then it runs the normal workflow. It's additive: it feeds the router, never replaces it, and
|
package/bin/spectoflow.js
CHANGED
|
@@ -133,7 +133,7 @@ async function update() {
|
|
|
133
133
|
const r = require('../lib/update').runUpdate({ projectRoot: root, templatesDir: TPL, version: VERSION, dryRun, force });
|
|
134
134
|
|
|
135
135
|
const from = r.fromVersion || 'unknown';
|
|
136
|
-
const changed = r.refreshed.length + r.created.length + r.adopted.length + r.newSidecar.length + r.forced.length + r.removed.length + r.migration.movedViews.length + r.pointers.length;
|
|
136
|
+
const changed = r.refreshed.length + r.created.length + r.adopted.length + r.newSidecar.length + r.forced.length + r.removed.length + r.migration.movedViews.length + r.pointers.appended.length + r.pointers.repointed.length + r.migration.repointedUserFiles.length + (r.migration.renamedBrain ? 1 : 0);
|
|
137
137
|
const row = (sym, label, list, painter, note) => {
|
|
138
138
|
if (!list.length) return;
|
|
139
139
|
const n = c.dim(String(list.length).padStart(2));
|
|
@@ -150,7 +150,9 @@ async function update() {
|
|
|
150
150
|
row(c.y('!'), '.new', r.newSidecar, c.y, 'you edited these — new version saved as *.new, merge by hand (or re-run with --force)');
|
|
151
151
|
row(c.dim('−'), 'removed', r.removed, c.dim, 'no longer part of the kit (the dashboard lives in the spectoflow package now)');
|
|
152
152
|
row(c.y('!'), 'kept', r.kept, c.y, 'you modified these and they are no longer part of the kit — delete them yourself when ready');
|
|
153
|
-
|
|
153
|
+
if (r.migration.renamedBrain) console.log(` ${c.cy('→')} ${c.cy('renamed'.padEnd(9))} ${c.dim(' 1')} ${c.dim('.spectoflow/AGENTS.md → .spectoflow/SPECTOFLOW.md (the brain; your edits, if any, came along)')}`);
|
|
154
|
+
row(c.cy('→'), 'repointed', [...r.pointers.repointed, ...r.migration.repointedUserFiles.map((f) => '.spectoflow/' + f)], c.cy, null);
|
|
155
|
+
row(c.cy('+'), 'linked', r.pointers.appended, c.cy, `${r.pointers.appended.join(', ')} existed without a pointer — a spectoflow section was appended`);
|
|
154
156
|
if (r.migration.movedViews.length) console.log(` ${c.cy('→')} ${c.cy('views'.padEnd(9))} ${c.dim(String(r.migration.movedViews.length).padStart(2))} ${c.dim('custom views moved to .spectoflow/dashboards/')}`);
|
|
155
157
|
r.migration.conflicts.forEach((f) => console.log(` ${c.y('!')} ${c.y('conflict'.padEnd(9))} ${c.dim(`dashboards/${f} already exists — the old copy stays in dashboard/custom/ for you to merge`)}`));
|
|
156
158
|
if (r.legacyLeftovers.length) console.log(` ${c.y('!')} ${c.dim('this project has no install manifest, so nothing was deleted. Safe to remove by hand: ' + r.legacyLeftovers.map((p) => '.spectoflow/' + p).join(', '))}`);
|
|
@@ -372,6 +374,40 @@ function configCmd() {
|
|
|
372
374
|
} catch (e) { console.log(`${c.y('!')} ${e.message}`); process.exitCode = 1; }
|
|
373
375
|
}
|
|
374
376
|
|
|
377
|
+
// ---- brain: the user's second brain (~/.spectoflow/brain.md), shared by every project ----
|
|
378
|
+
function brainCmd() {
|
|
379
|
+
const brain = require('../lib/brain');
|
|
380
|
+
const brainSetup = require('../lib/brain-setup');
|
|
381
|
+
const tilde = (p) => (p.startsWith(os.homedir()) ? '~' + p.slice(os.homedir().length) : p);
|
|
382
|
+
if (argv[1] === 'setup') {
|
|
383
|
+
const dryRun = argv.includes('--dry-run');
|
|
384
|
+
const rows = brainSetup.setup({ binPath: __filename, dryRun });
|
|
385
|
+
console.log(wordmark());
|
|
386
|
+
console.log(` ${c.bold('spectoflow brain setup')}${dryRun ? c.dim(' (dry-run)') : ''} ${c.dim('registers the spectoflow MCP server in each installed agent (user level, once per machine)')}\n`);
|
|
387
|
+
if (!rows.length) { console.log(` ${c.y('!')} No supported coding agent found on PATH.\n`); return; }
|
|
388
|
+
const w = Math.max(...rows.map((r) => r.label.length));
|
|
389
|
+
const sym = { created: c.cy('+'), added: c.g('✓'), exists: c.dim('·'), skipped: c.y('!'), manual: c.y('!'), failed: c.y('✗') };
|
|
390
|
+
for (const r of rows) {
|
|
391
|
+
console.log(` ${sym[r.status] || '?'} ${r.label.padEnd(w)} ${r.status.padEnd(8)} ${c.dim(r.via || tilde(r.file))}`);
|
|
392
|
+
if (r.status === 'skipped') console.log(c.dim(` couldn't parse this file (comments?) — left untouched; add a "spectoflow" server running: spectoflow mcp`));
|
|
393
|
+
if (r.status === 'manual') console.log(c.dim(r.id === 'goose' ? ` YAML isn't edited automatically — add this to ${tilde(r.file)}:\n` : ` this file already mentions spectoflow in a form we can't read safely — check it, and add this if it's missing:\n`) + r.manual.split('\n').map((l) => ' ' + l).join('\n'));
|
|
394
|
+
if (r.status === 'failed') console.log(c.dim(` ${r.detail || 'failed'}${r.manual ? `\n run it yourself: ${r.manual}` : ''}`));
|
|
395
|
+
}
|
|
396
|
+
const changed = rows.filter((r) => ['created', 'added'].includes(r.status)).length;
|
|
397
|
+
console.log(`\n ${dryRun ? c.dim('(dry-run — nothing was written)') : changed ? c.g(`✓ ${changed} agent(s) wired`) + c.dim(' — start a new agent session to load it') : c.dim('Nothing to change')}\n`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const r = brain.read();
|
|
401
|
+
console.log(wordmark());
|
|
402
|
+
console.log(` ${c.bold('Second brain')} ${c.dim(tilde(brain.brainPath()))}`);
|
|
403
|
+
console.log(` ${r.entries.length} entr${r.entries.length === 1 ? 'y' : 'ies'}${r.pending.length ? c.y(` · ${r.pending.length} to confirm`) : ''} ${c.dim('learned facts: ' + (r.autoAdd ? 'added directly' : 'wait for your confirmation') + ' (spectoflow config set brain.autoAdd true|false)')}\n`);
|
|
404
|
+
const agents = brainSetup.status();
|
|
405
|
+
if (!agents.length) console.log(` ${c.y('!')} No supported coding agent found on PATH.`);
|
|
406
|
+
agents.forEach((a) => console.log(` ${a.wired ? c.g('●') : c.dim('○')} ${a.label}${a.wired ? '' : c.dim(' not wired')}`));
|
|
407
|
+
if (agents.some((a) => !a.wired)) console.log(`\n ${c.dim('wire them:')} ${c.g('spectoflow brain setup')} ${c.dim('[--dry-run]')}`);
|
|
408
|
+
console.log('');
|
|
409
|
+
}
|
|
410
|
+
|
|
375
411
|
// ---- Customize: `spectoflow skill/agent/dashboard create` — the CLI mirror of the dashboard's
|
|
376
412
|
// Settings → Customize UI. Both surfaces build the same natural-language prompt (customize-prompts.js)
|
|
377
413
|
// and post it through the same pipeline (runner.js's startRun — the function /api/run itself calls),
|
|
@@ -587,6 +623,10 @@ ${c.bold('Dashboard')}
|
|
|
587
623
|
${c.g('dashboard login')} ${c.dim('--url=<u> --token=<t>')} connect this machine to an online dashboard ${c.dim('(logout · publish · unpublish)')}
|
|
588
624
|
${c.g('projects')} ${c.dim('[remove <id>]')} list every project seen so far
|
|
589
625
|
|
|
626
|
+
${c.bold('Second brain')} ${c.dim('— what spectoflow learns about you, shared by all your projects')}
|
|
627
|
+
${c.g('brain')} what it holds, and which agents can reach it
|
|
628
|
+
${c.g('brain setup')} ${c.dim('[--dry-run]')} register the spectoflow MCP server in your installed agents
|
|
629
|
+
|
|
590
630
|
${c.bold('Customize')} ${c.dim('— same as Settings → Customize, from the terminal')}
|
|
591
631
|
${c.g('skill create')} ${c.dim('"<description>" | --auto')} generate a project skill
|
|
592
632
|
${c.g('agent create')} ${c.dim('"<description>" | --auto')} generate a project agent
|
|
@@ -616,7 +656,7 @@ const HELP = {
|
|
|
616
656
|
${c.dim('an existing AGENTS.md/GEMINI.md is kept and gets a spectoflow pointer section appended.')}
|
|
617
657
|
${c.dim('Full list with docs links: the dashboard\'s Documentation tab, or the README.')}`,
|
|
618
658
|
update: `${c.bold('spectoflow update')} ${c.dim('[--dry-run] [--force|-f]')}\n
|
|
619
|
-
Refresh framework-owned files (engine, default agents & skills,
|
|
659
|
+
Refresh framework-owned files (engine, default agents & skills, SPECTOFLOW.md, policy…)
|
|
620
660
|
to this CLI's version, ${c.bold('preserving your work')}: config.json, workflow.md, specs/, plans/
|
|
621
661
|
and any agent/skill you edited are never overwritten (an edited file's new version lands as
|
|
622
662
|
${c.dim('*.new')} for you to merge). ${c.g('--dry-run')} previews without writing.
|
|
@@ -660,6 +700,14 @@ const HELP = {
|
|
|
660
700
|
agents: `${c.bold('spectoflow agents')}\n List the stable team personas (name · capability · role).`,
|
|
661
701
|
skills: `${c.bold('spectoflow skills')}\n List the evolving procedures (name · capability · what it does).`,
|
|
662
702
|
workflow: `${c.bold('spectoflow workflow')}\n Show the pipeline steps, marking which are enabled (●) or disabled (○).`,
|
|
703
|
+
brain: `${c.bold('spectoflow brain')} ${c.dim('[setup [--dry-run]]')}\n
|
|
704
|
+
Your second brain — what spectoflow has learned about you (profile, preferences, working style,
|
|
705
|
+
things to avoid), in ${c.dim('~/.spectoflow/brain.md')}, shared by all your projects and editable in the
|
|
706
|
+
dashboard's ${c.bold('Second brain')} tab. Agents read and grow it through the ${c.g('spectoflow mcp')} server.
|
|
707
|
+
${c.g('brain')} show it: entries, entries to confirm, and which installed agents are wired
|
|
708
|
+
${c.g('brain setup')} register the MCP server in each installed agent's USER-level config (once
|
|
709
|
+
per machine; never touches an existing entry; Goose gets a snippet to paste)
|
|
710
|
+
Learned facts are added directly by default: ${c.g('spectoflow config set brain.autoAdd false')} to confirm them first.`,
|
|
663
711
|
stop: `${c.bold('spectoflow stop')}\n Stop the running dashboard (alias for ${c.g('spectoflow dashboard stop')}).`,
|
|
664
712
|
config: `${c.bold('spectoflow config')} ${c.dim('[get <key> | set <key> <value>]')}\n
|
|
665
713
|
Global settings that apply to every project on this machine, stored in ${c.dim('~/.spectoflow/config.json')}:
|
|
@@ -675,6 +723,8 @@ const fns = {
|
|
|
675
723
|
init, update, dashboard, stop: stopDashboard, status, list: listAll, help, version,
|
|
676
724
|
projects: projectsCmd,
|
|
677
725
|
config: configCmd,
|
|
726
|
+
mcp: () => require('../lib/mcp-server').serve({ version: VERSION }),
|
|
727
|
+
brain: brainCmd,
|
|
678
728
|
agents: () => { console.log(wordmark()); printAgents(false); },
|
|
679
729
|
skills: () => { console.log(wordmark()); printSkills(false); },
|
|
680
730
|
workflow: () => { console.log(wordmark()); printWorkflow(false); },
|
package/lib/adapters.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
/*
|
|
3
|
-
* Per-agent adapters. The canonical brain is .spectoflow/
|
|
3
|
+
* Per-agent adapters. The canonical brain is .spectoflow/SPECTOFLOW.md. Each agent needs a thin entry
|
|
4
4
|
* file in the place IT reads, pointing back to the canonical brain — so the framework stays in
|
|
5
5
|
* .spectoflow/ (swappable, no pollution) while every agent still finds its native entry point.
|
|
6
6
|
* (Same pattern OpenSpec uses to support claude/codex/cursor/gemini/…)
|
|
@@ -14,7 +14,7 @@ const path = require('path');
|
|
|
14
14
|
|
|
15
15
|
const CLAUDE_MD = `# CLAUDE.md — spectoflow
|
|
16
16
|
|
|
17
|
-
This project uses **spectoflow**. **Read \`.spectoflow/
|
|
17
|
+
This project uses **spectoflow**. **Read \`.spectoflow/SPECTOFLOW.md\` and follow it** as your operating
|
|
18
18
|
instructions (intent router, workflow, standing rules).
|
|
19
19
|
|
|
20
20
|
> If \`CLAUDE.md.tomerge\` exists next to this file, it is your previous CLAUDE.md, preserved on
|
|
@@ -23,7 +23,12 @@ instructions (intent router, workflow, standing rules).
|
|
|
23
23
|
|
|
24
24
|
**Be an expert analyst, not an order-taker.** When a request is ambiguous, **clarify before acting**:
|
|
25
25
|
reflect it back and ask **one targeted question at a time** (each with a recommendation) until the need
|
|
26
|
-
is clear — then execute. See the Clarify reflex in \`.spectoflow/
|
|
26
|
+
is clear — then execute. See the Clarify reflex in \`.spectoflow/SPECTOFLOW.md\`.
|
|
27
|
+
|
|
28
|
+
**Second brain.** Durable facts about the user come from the \`spectoflow\` MCP server — apply them. When you
|
|
29
|
+
learn one (a stated preference, a correction of how you work, their role), record it with \`brain_learn\`, or print
|
|
30
|
+
\`::spectoflow learn category=<profile|preferences|workflow|avoid> msg=<fact>\` if that tool is unavailable. Never
|
|
31
|
+
secrets or sensitive data.
|
|
27
32
|
|
|
28
33
|
- Command: \`/spectoflow\` (\`init\` / \`status\` / or just a request).
|
|
29
34
|
- Dashboard: \`spectoflow dashboard\` → http://localhost:4319
|
|
@@ -32,30 +37,40 @@ is clear — then execute. See the Clarify reflex in \`.spectoflow/AGENTS.md\`.
|
|
|
32
37
|
|
|
33
38
|
const ROOT_AGENTS_MD = `# AGENTS.md — spectoflow
|
|
34
39
|
|
|
35
|
-
This project uses **spectoflow**. **Read \`.spectoflow/
|
|
40
|
+
This project uses **spectoflow**. **Read \`.spectoflow/SPECTOFLOW.md\` and follow it** as your operating
|
|
36
41
|
instructions. Artifacts are markdown in \`specs/\` and \`plans/\`; the workflow is \`.spectoflow/workflow.md\`.
|
|
37
42
|
|
|
38
43
|
**Be an expert analyst, not an order-taker.** When a request is ambiguous, **clarify before acting**:
|
|
39
44
|
reflect it back and ask **one targeted question at a time** (each with a recommendation) until the need
|
|
40
|
-
is clear — then execute. See the Clarify reflex in \`.spectoflow/
|
|
45
|
+
is clear — then execute. See the Clarify reflex in \`.spectoflow/SPECTOFLOW.md\`.
|
|
46
|
+
|
|
47
|
+
**Second brain.** Durable facts about the user come from the \`spectoflow\` MCP server — apply them. When you
|
|
48
|
+
learn one (a stated preference, a correction of how you work, their role), record it with \`brain_learn\`, or print
|
|
49
|
+
\`::spectoflow learn category=<profile|preferences|workflow|avoid> msg=<fact>\` if that tool is unavailable. Never
|
|
50
|
+
secrets or sensitive data.
|
|
41
51
|
`;
|
|
42
52
|
|
|
43
53
|
const GEMINI_MD = `# GEMINI.md — spectoflow
|
|
44
54
|
|
|
45
|
-
This project uses **spectoflow**. **Read \`.spectoflow/
|
|
55
|
+
This project uses **spectoflow**. **Read \`.spectoflow/SPECTOFLOW.md\` and follow it** as your operating
|
|
46
56
|
instructions (intent router, workflow, standing rules). Artifacts are markdown in \`specs/\` and
|
|
47
57
|
\`plans/\`; the workflow is \`.spectoflow/workflow.md\`.
|
|
48
58
|
|
|
49
59
|
**Be an expert analyst, not an order-taker.** When a request is ambiguous, **clarify before acting**:
|
|
50
60
|
reflect it back and ask **one targeted question at a time** (each with a recommendation) until the need
|
|
51
|
-
is clear — then execute. See the Clarify reflex in \`.spectoflow/
|
|
61
|
+
is clear — then execute. See the Clarify reflex in \`.spectoflow/SPECTOFLOW.md\`.
|
|
62
|
+
|
|
63
|
+
**Second brain.** Durable facts about the user come from the \`spectoflow\` MCP server — apply them. When you
|
|
64
|
+
learn one (a stated preference, a correction of how you work, their role), record it with \`brain_learn\`, or print
|
|
65
|
+
\`::spectoflow learn category=<profile|preferences|workflow|avoid> msg=<fact>\` if that tool is unavailable. Never
|
|
66
|
+
secrets or sensitive data.
|
|
52
67
|
`;
|
|
53
68
|
|
|
54
69
|
const SLASH_CMD = `---
|
|
55
70
|
description: spectoflow — spec-driven control (init / status / or just a request)
|
|
56
71
|
---
|
|
57
72
|
|
|
58
|
-
Read \`.spectoflow/
|
|
73
|
+
Read \`.spectoflow/SPECTOFLOW.md\` and \`.spectoflow/config.json\` first.
|
|
59
74
|
|
|
60
75
|
Argument: \`$ARGUMENTS\`
|
|
61
76
|
|
|
@@ -63,7 +78,7 @@ Argument: \`$ARGUMENTS\`
|
|
|
63
78
|
\`specs/\` and \`plans/\` are empty, greet me, state the mode, and start Intake (brainstorm → analysis
|
|
64
79
|
→ spec → plan) by asking what I want to build.
|
|
65
80
|
- \`status\`: summarize progress from \`plans/*.md\` and \`.spectoflow/runtime.json\`.
|
|
66
|
-
- otherwise: treat \`$ARGUMENTS\` as a request and run the Router in \`.spectoflow/
|
|
81
|
+
- otherwise: treat \`$ARGUMENTS\` as a request and run the Router in \`.spectoflow/SPECTOFLOW.md\`.
|
|
67
82
|
`;
|
|
68
83
|
|
|
69
84
|
// Priority order = which agent becomes the default when several are detected.
|
|
@@ -232,43 +247,60 @@ const POINTER_START = '<!-- spectoflow:start -->';
|
|
|
232
247
|
const POINTER_END = '<!-- spectoflow:end -->';
|
|
233
248
|
const POINTER_BLOCK = `${POINTER_START}\n${ROOT_AGENTS_MD.replace(/^# .*\n/, '## spectoflow\n').trimEnd()}\n${POINTER_END}\n`;
|
|
234
249
|
|
|
235
|
-
|
|
250
|
+
// The brain was `.spectoflow/AGENTS.md` before 0.28 — renamed so it can't be mistaken for the
|
|
251
|
+
// project-root AGENTS.md. Pointers written back then are rewritten in place, never duplicated.
|
|
252
|
+
const BRAIN = '.spectoflow/SPECTOFLOW.md';
|
|
253
|
+
const LEGACY_BRAIN = '.spectoflow/AGENTS.md';
|
|
254
|
+
const ENTRY_FILES = [...MEMORY_FILES, '.claude/commands/spectoflow.md'];
|
|
236
255
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
256
|
+
// Bring one existing entry file up to date: rewrite a legacy pointer, then (memory files only)
|
|
257
|
+
// append the pointer section if it still has none. Returns what it did (or would do, on dryRun).
|
|
258
|
+
function linkEntry(fp, rel, dryRun) {
|
|
259
|
+
const before = fs.readFileSync(fp, 'utf8');
|
|
260
|
+
let text = before.split(LEGACY_BRAIN).join(BRAIN);
|
|
261
|
+
const repointed = text !== before;
|
|
262
|
+
const appended = MEMORY_FILES.includes(rel) && !text.includes(BRAIN);
|
|
263
|
+
if (appended) text = text.replace(/\s*$/, '') + '\n\n' + POINTER_BLOCK;
|
|
264
|
+
if ((repointed || appended) && !dryRun) fs.writeFileSync(fp, text);
|
|
265
|
+
return { repointed, appended };
|
|
242
266
|
}
|
|
243
267
|
|
|
244
268
|
// Write the native entry-file shims for each selected agent. Shared files (AGENTS.md across
|
|
245
|
-
// codex/cursor) are handled once.
|
|
269
|
+
// codex/cursor) are handled once. An entry file that already exists is linked instead (linkEntry).
|
|
246
270
|
function generate(projectRoot, agents) {
|
|
247
271
|
const list = (agents && agents.length ? agents : ['claude', 'codex']).map(byId).filter(Boolean);
|
|
248
|
-
const written = [], appended = [];
|
|
272
|
+
const written = [], appended = [], repointed = [], seen = new Set();
|
|
249
273
|
for (const a of list) {
|
|
250
274
|
for (const e of a.entries) {
|
|
251
|
-
if (
|
|
275
|
+
if (seen.has(e.path)) continue;
|
|
276
|
+
seen.add(e.path);
|
|
252
277
|
const fp = path.join(projectRoot, e.path);
|
|
253
278
|
if (!fs.existsSync(fp)) {
|
|
254
279
|
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
255
280
|
fs.writeFileSync(fp, e.content);
|
|
256
281
|
written.push(e.path);
|
|
257
|
-
|
|
258
|
-
appended.push(e.path);
|
|
282
|
+
continue;
|
|
259
283
|
}
|
|
284
|
+
const r = linkEntry(fp, e.path, false);
|
|
285
|
+
if (r.appended) appended.push(e.path);
|
|
286
|
+
if (r.repointed) repointed.push(e.path);
|
|
260
287
|
}
|
|
261
288
|
}
|
|
262
|
-
return { written, appended };
|
|
289
|
+
return { written, appended, repointed };
|
|
263
290
|
}
|
|
264
291
|
|
|
265
|
-
// For `update`:
|
|
266
|
-
//
|
|
292
|
+
// For `update`: link every entry file that exists — never creates one for an agent the project may
|
|
293
|
+
// not use.
|
|
267
294
|
function ensurePointers(projectRoot, dryRun = false) {
|
|
268
|
-
|
|
295
|
+
const out = { appended: [], repointed: [] };
|
|
296
|
+
for (const rel of ENTRY_FILES) {
|
|
269
297
|
const fp = path.join(projectRoot, rel);
|
|
270
|
-
|
|
271
|
-
|
|
298
|
+
if (!fs.existsSync(fp)) continue;
|
|
299
|
+
const r = linkEntry(fp, rel, dryRun);
|
|
300
|
+
if (r.appended) out.appended.push(rel);
|
|
301
|
+
if (r.repointed) out.repointed.push(rel);
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
272
304
|
}
|
|
273
305
|
|
|
274
306
|
// { id: runner } defaults for the given agents — used to seed config.runners at init.
|
|
@@ -287,4 +319,4 @@ function knownAgents() {
|
|
|
287
319
|
return REGISTRY.map((a) => ({ id: a.id, label: a.label, bin: a.detect.bin, dirs: a.detect.dirs || [], runner: a.runner, headless: a.headless, docsUrl: a.docsUrl }));
|
|
288
320
|
}
|
|
289
321
|
|
|
290
|
-
module.exports = { generate, ensurePointers, defaultRunners, REGISTRY, knownAgents };
|
|
322
|
+
module.exports = { generate, ensurePointers, defaultRunners, REGISTRY, knownAgents, BRAIN, LEGACY_BRAIN };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* `spectoflow brain setup` — registers the `spectoflow mcp` server (lib/mcp-server.js) in the
|
|
4
|
+
* USER-LEVEL MCP config of every coding agent installed on this machine, once per machine. User level
|
|
5
|
+
* because the second brain is personal, and because project-level MCP config is unreliable or absent
|
|
6
|
+
* for several agents (trusted-projects-only in Codex, ignored by Antigravity, missing in Goose/Kimi).
|
|
7
|
+
*
|
|
8
|
+
* Paths and entry shapes come from each agent's own docs (research pass 2026-09-16, see
|
|
9
|
+
* docs/second-brain-design.md). Same rules as lib/mcp.js: an existing entry is never touched, a file
|
|
10
|
+
* that can't be parsed is never rewritten. Claude Code is the one agent wired through its own CLI —
|
|
11
|
+
* it rewrites ~/.claude.json constantly, so editing that file directly could lose the write.
|
|
12
|
+
*/
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
const { REGISTRY } = require('./adapters');
|
|
18
|
+
const detect = require('./detect');
|
|
19
|
+
const { mergeMcpServer } = require('./mcp');
|
|
20
|
+
|
|
21
|
+
const NAME = 'spectoflow';
|
|
22
|
+
|
|
23
|
+
// `spectoflow mcp` when the `spectoflow` on PATH is this very install; otherwise node + this bin's
|
|
24
|
+
// absolute path (running from a clone, or another version on PATH). Always node + path on Windows: there
|
|
25
|
+
// `spectoflow` is an npm .cmd shim, which MCP hosts that spawn without a shell can't start.
|
|
26
|
+
function serverCommand({ binPath, env = process.env, platform = process.platform } = {}) {
|
|
27
|
+
const found = findOnPath('spectoflow', env, platform);
|
|
28
|
+
let same = false;
|
|
29
|
+
if (found && platform !== 'win32') { try { same = fs.realpathSync(found) === fs.realpathSync(binPath); } catch { same = false; } }
|
|
30
|
+
const cmd = same ? { command: 'spectoflow', args: ['mcp'] } : { command: process.execPath, args: [binPath, 'mcp'] };
|
|
31
|
+
if (env.SPECTOFLOW_HOME) cmd.env = { SPECTOFLOW_HOME: env.SPECTOFLOW_HOME };
|
|
32
|
+
return cmd;
|
|
33
|
+
}
|
|
34
|
+
function findOnPath(bin, env, platform) {
|
|
35
|
+
const exts = platform === 'win32' ? ['', ...(env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)] : [''];
|
|
36
|
+
for (const d of (env.PATH || env.Path || '').split(path.delimiter).filter(Boolean)) {
|
|
37
|
+
for (const e of exts) { const fp = path.join(d, bin + e); if (fs.existsSync(fp)) return fp; }
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const home = () => os.homedir();
|
|
43
|
+
const appData = () => process.env.APPDATA || path.join(home(), 'AppData', 'Roaming');
|
|
44
|
+
|
|
45
|
+
// id → how to wire it. `file` is resolved lazily (tests point HOME elsewhere).
|
|
46
|
+
const TARGETS = {
|
|
47
|
+
claude: { kind: 'claude-cli', file: () => path.join(process.env.CLAUDE_CONFIG_DIR || home(), '.claude.json') },
|
|
48
|
+
codex: { kind: 'toml', file: () => path.join(process.env.CODEX_HOME || path.join(home(), '.codex'), 'config.toml') },
|
|
49
|
+
cursor: { kind: 'json', file: () => path.join(home(), '.cursor', 'mcp.json') },
|
|
50
|
+
gemini: { kind: 'json', file: () => path.join(home(), '.gemini', 'settings.json') },
|
|
51
|
+
opencode: { kind: 'opencode', file: () => path.join(home(), '.config', 'opencode', 'opencode.json') },
|
|
52
|
+
kiro: { kind: 'json', file: () => path.join(home(), '.kiro', 'settings', 'mcp.json') },
|
|
53
|
+
antigravity: { kind: 'json', file: () => path.join(home(), '.gemini', 'config', 'mcp_config.json') },
|
|
54
|
+
copilot: { kind: 'json', extra: { type: 'local', tools: ['*'] }, file: () => path.join(process.env.COPILOT_HOME || path.join(home(), '.copilot'), 'mcp-config.json') },
|
|
55
|
+
'amazon-q': { kind: 'json', file: () => path.join(home(), '.aws', 'amazonq', 'mcp.json') },
|
|
56
|
+
droid: { kind: 'json', file: () => path.join(home(), '.factory', 'mcp.json') },
|
|
57
|
+
auggie: { kind: 'json', file: () => path.join(home(), '.augment', 'settings.json') },
|
|
58
|
+
goose: { kind: 'yaml-snippet', file: () => (process.platform === 'win32' ? path.join(appData(), 'Block', 'goose', 'config', 'config.yaml') : path.join(home(), '.config', 'goose', 'config.yaml')) },
|
|
59
|
+
kimi: { kind: 'json', file: () => path.join(home(), '.kimi', 'mcp.json') },
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const tomlString = (s) => JSON.stringify(String(s));
|
|
63
|
+
function tomlBlock(cmd) {
|
|
64
|
+
const lines = [`[mcp_servers.${NAME}]`, `command = ${tomlString(cmd.command)}`, `args = [${cmd.args.map(tomlString).join(', ')}]`];
|
|
65
|
+
if (cmd.env) lines.push(`env = { ${Object.entries(cmd.env).map(([k, v]) => `${k} = ${tomlString(v)}`).join(', ')} }`);
|
|
66
|
+
return lines.join('\n') + '\n';
|
|
67
|
+
}
|
|
68
|
+
function yamlBlock(cmd) {
|
|
69
|
+
const q = (s) => JSON.stringify(String(s));
|
|
70
|
+
const lines = ['extensions:', ` ${NAME}:`, ' type: stdio', ` name: ${NAME}`, ' enabled: true', ` cmd: ${q(cmd.command)}`, ` args: [${cmd.args.map(q).join(', ')}]`, ' timeout: 300'];
|
|
71
|
+
if (cmd.env) lines.push(` envs: { ${Object.entries(cmd.env).map(([k, v]) => `${k}: ${q(v)}`).join(', ')} }`);
|
|
72
|
+
return lines.join('\n') + '\n';
|
|
73
|
+
}
|
|
74
|
+
const readText = (fp) => { try { return fs.readFileSync(fp, 'utf8'); } catch { return null; } };
|
|
75
|
+
|
|
76
|
+
// Is a `spectoflow` MCP server declared in this Codex TOML, in any of the forms TOML allows:
|
|
77
|
+
// [mcp_servers.spectoflow] (bare or quoted key, trailing comment), a dotted key, or a key inside [mcp_servers].
|
|
78
|
+
function tomlHasServer(text) {
|
|
79
|
+
const key = `(["']?)${NAME}\\1`;
|
|
80
|
+
if (new RegExp(`^\\s*\\[\\s*mcp_servers\\s*\\.\\s*${key}\\s*(\\.[^\\]]*)?\\]\\s*(#.*)?$`, 'm').test(text)) return true;
|
|
81
|
+
if (new RegExp(`^\\s*mcp_servers\\s*\\.\\s*${key}\\s*[.=]`, 'm').test(text)) return true;
|
|
82
|
+
let inTable = false;
|
|
83
|
+
for (const line of text.split(/\r?\n/)) {
|
|
84
|
+
const h = line.match(/^\s*\[\s*([^\]]+?)\s*\]/);
|
|
85
|
+
if (h) { inTable = h[1].replace(/\s+/g, '') === 'mcp_servers'; continue; }
|
|
86
|
+
if (inTable && new RegExp(`^\\s*${key}\\s*[.=]`).test(line)) return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Is the spectoflow server already registered for this agent? (true/false; null = can't tell)
|
|
92
|
+
function isWired(id) {
|
|
93
|
+
const t = TARGETS[id]; if (!t) return null;
|
|
94
|
+
const fp = t.file(), text = readText(fp);
|
|
95
|
+
if (text === null) return false;
|
|
96
|
+
if (t.kind === 'toml') return tomlHasServer(text);
|
|
97
|
+
if (t.kind === 'yaml-snippet') return /^extensions:\s*$/m.test(text) && new RegExp(`^ {2}${NAME}:\\s*$`, 'm').test(text);
|
|
98
|
+
try {
|
|
99
|
+
const doc = JSON.parse(text);
|
|
100
|
+
const map = t.kind === 'opencode' ? doc.mcp : doc.mcpServers;
|
|
101
|
+
return !!(map && typeof map === 'object' && Object.prototype.hasOwnProperty.call(map, NAME));
|
|
102
|
+
} catch { return null; }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function wireOne(id, cmd, { dryRun, run }) {
|
|
106
|
+
const t = TARGETS[id], fp = t.file();
|
|
107
|
+
if (t.kind === 'claude-cli') {
|
|
108
|
+
if (isWired(id)) return { status: 'exists', file: fp };
|
|
109
|
+
// Name before --env: --env takes several values and would swallow the name otherwise.
|
|
110
|
+
const args = ['mcp', 'add', '--scope', 'user', NAME, ...Object.entries(cmd.env || {}).flatMap(([k, v]) => ['--env', `${k}=${v}`]), '--', cmd.command, ...cmd.args];
|
|
111
|
+
if (dryRun) return { status: 'added', file: fp, via: `claude ${args.join(' ')}` };
|
|
112
|
+
const r = run('claude', args);
|
|
113
|
+
const out = `${r.stderr || ''}${r.stdout || ''}`.trim();
|
|
114
|
+
if (r.status === 0) return { status: 'added', file: fp, via: 'claude mcp add' };
|
|
115
|
+
if (/already exists/i.test(out)) return { status: 'exists', file: fp };
|
|
116
|
+
return { status: 'failed', file: fp, detail: out || (r.error && r.error.message) || 'failed', manual: `claude ${args.join(' ')}` };
|
|
117
|
+
}
|
|
118
|
+
if (t.kind === 'json') return { status: mergeMcpServer(fp, NAME, { ...(t.extra || {}), command: cmd.command, args: cmd.args, ...(cmd.env ? { env: cmd.env } : {}) }, { dryRun }), file: fp };
|
|
119
|
+
if (t.kind === 'opencode') return { status: mergeMcpServer(fp, NAME, { type: 'local', command: [cmd.command, ...cmd.args], enabled: true, ...(cmd.env ? { environment: cmd.env } : {}) }, { key: 'mcp', dryRun }), file: fp };
|
|
120
|
+
if (t.kind === 'toml') {
|
|
121
|
+
const text = readText(fp);
|
|
122
|
+
if (text !== null && isWired(id)) return { status: 'exists', file: fp };
|
|
123
|
+
// Any other mention of spectoflow is a form we can't classify: appending could declare the table twice,
|
|
124
|
+
// and a duplicate key makes Codex refuse its whole config. Hand the user the block instead.
|
|
125
|
+
if (text !== null && text.includes(NAME)) return { status: 'manual', file: fp, manual: tomlBlock(cmd) };
|
|
126
|
+
// `mcp_servers = { … }` (an inline table) can't be extended by a [mcp_servers.x] header either.
|
|
127
|
+
if (text !== null && /^\s*mcp_servers\s*=/m.test(text)) return { status: 'manual', file: fp, manual: tomlBlock(cmd) };
|
|
128
|
+
if (!dryRun) {
|
|
129
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
130
|
+
fs.writeFileSync(fp, text === null ? tomlBlock(cmd) : text.replace(/\s*$/, '') + '\n\n' + tomlBlock(cmd));
|
|
131
|
+
}
|
|
132
|
+
return { status: text === null ? 'created' : 'added', file: fp };
|
|
133
|
+
}
|
|
134
|
+
// Goose: YAML can't be edited safely without a parser — hand the user the exact block instead.
|
|
135
|
+
if (isWired(id)) return { status: 'exists', file: fp };
|
|
136
|
+
return { status: 'manual', file: fp, manual: yamlBlock(cmd) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// No shell on POSIX. On Windows `claude` is a .cmd shim, which needs cmd.exe — with every argument quoted,
|
|
140
|
+
// or a path like C:\\Program Files\\nodejs\\node.exe would be split.
|
|
141
|
+
function defaultRun(bin, args) {
|
|
142
|
+
if (process.platform !== 'win32') return spawnSync(bin, args, { encoding: 'utf8', windowsHide: true });
|
|
143
|
+
const q = (a) => `"${String(a).replace(/"/g, '""')}"`;
|
|
144
|
+
return spawnSync('cmd.exe', ['/d', '/s', '/c', `"${[bin, ...args].map(q).join(' ')}"`], { encoding: 'utf8', windowsHide: true, windowsVerbatimArguments: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// → [{ id, label, status: created|added|exists|skipped|manual|failed, file, manual?, detail? }]
|
|
148
|
+
// for every agent installed on this machine. `agents` overrides detection (tests, explicit choice).
|
|
149
|
+
function setup({ binPath, dryRun = false, agents, run = defaultRun, env = process.env } = {}) {
|
|
150
|
+
const cmd = serverCommand({ binPath, env });
|
|
151
|
+
const ids = agents || REGISTRY.filter((a) => TARGETS[a.id] && detect.binOnPath(a.detect.bin, { env })).map((a) => a.id);
|
|
152
|
+
return ids.filter((id) => TARGETS[id]).map((id) => {
|
|
153
|
+
const label = (REGISTRY.find((a) => a.id === id) || {}).label || id;
|
|
154
|
+
try { return { id, label, ...wireOne(id, cmd, { dryRun, run }) }; }
|
|
155
|
+
catch (e) { return { id, label, status: 'failed', file: TARGETS[id].file(), detail: e.message }; }
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// For the dashboard page: each installed agent and whether the server is registered for it.
|
|
160
|
+
function status({ env = process.env } = {}) {
|
|
161
|
+
return REGISTRY.filter((a) => TARGETS[a.id] && detect.binOnPath(a.detect.bin, { env }))
|
|
162
|
+
.map((a) => ({ id: a.id, label: a.label, wired: isWired(a.id) === true }));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { setup, status, isWired, serverCommand, TARGETS, NAME };
|