geneseed 2.0.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/CHANGELOG.md +938 -0
- package/DESIGN.md +1423 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +48 -0
- package/README.md +336 -0
- package/SETUP.md +1027 -0
- package/SHIPPED.md +70 -0
- package/adapters/bob/README.md +100 -0
- package/adapters/claude-code/README.md +101 -0
- package/adapters/claude-code/settings.json +47 -0
- package/adapters/claude-code/skills/geneseed/SKILL.md +72 -0
- package/adapters/copilot/README.md +76 -0
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +434 -0
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +158 -0
- package/adapters/opencode/README.md +582 -0
- package/adapters/opencode/agents/orchestrator.md +25 -0
- package/adapters/opencode/opencode.json +44 -0
- package/adapters/opencode/plugins/geneseed-activity.js +430 -0
- package/adapters/opencode/plugins/geneseed-context.js +876 -0
- package/adapters/opencode/plugins/geneseed-guard.js +290 -0
- package/adapters/opencode/plugins/geneseed-learn.js +396 -0
- package/adapters/opencode/plugins/geneseed-notify.js +142 -0
- package/adapters/opencode/plugins/geneseed-ponytail.js +148 -0
- package/adapters/opencode/plugins/geneseed-workflow.js +183 -0
- package/adapters/opencode/workflows/_runtime.js +277 -0
- package/adapters/opencode/workflows/council.js +64 -0
- package/adapters/opencode/workflows/dispatch.js +89 -0
- package/adapters/opencode/workflows/research-plan-implement.js +43 -0
- package/adapters/opencode/workflows/review.js +57 -0
- package/bin/geneseed-cli.mjs +412 -0
- package/bin/geneseed-hook.mjs +129 -0
- package/bin/geneseed.mjs +1086 -0
- package/docs/declined.md +149 -0
- package/docs/limits.md +270 -0
- package/docs/opencode-plugin-setup.md +64 -0
- package/docs/token-footprint.md +68 -0
- package/docs/web/_groups.json +45 -0
- package/docs/web/about.md +6 -0
- package/docs/web/adapters-claude-code.md +8 -0
- package/docs/web/adapters-opencode-loads.md +8 -0
- package/docs/web/adapters-opencode-spec.md +8 -0
- package/docs/web/adapters-opencode.md +8 -0
- package/docs/web/agents.md +8 -0
- package/docs/web/authoring.md +11 -0
- package/docs/web/autostart.md +9 -0
- package/docs/web/claude-hooks.md +36 -0
- package/docs/web/cli.md +6 -0
- package/docs/web/collaboration.md +27 -0
- package/docs/web/configure-wiki.md +9 -0
- package/docs/web/design.md +7 -0
- package/docs/web/env-knobs.md +9 -0
- package/docs/web/exclusions.md +26 -0
- package/docs/web/first-session.md +28 -0
- package/docs/web/footprint.md +31 -0
- package/docs/web/foreman-mode.md +33 -0
- package/docs/web/glossary.md +6 -0
- package/docs/web/harnesses.md +28 -0
- package/docs/web/headless.md +9 -0
- package/docs/web/ingest-docs.md +9 -0
- package/docs/web/install-paths.md +9 -0
- package/docs/web/install-quick.md +35 -0
- package/docs/web/lsp-overview.md +37 -0
- package/docs/web/mcp-claude-code.md +10 -0
- package/docs/web/mcp-filesystem.md +9 -0
- package/docs/web/mcp-gitlab.md +9 -0
- package/docs/web/mcp-markitdown.md +9 -0
- package/docs/web/mcp-overview.md +34 -0
- package/docs/web/mcp-trouble.md +9 -0
- package/docs/web/mcp-verify.md +9 -0
- package/docs/web/memory.md +7 -0
- package/docs/web/migrate.md +39 -0
- package/docs/web/model.md +20 -0
- package/docs/web/notebook.md +7 -0
- package/docs/web/pipelines.md +24 -0
- package/docs/web/plugin-activity.md +23 -0
- package/docs/web/plugin-context.md +26 -0
- package/docs/web/plugin-guard.md +24 -0
- package/docs/web/plugin-learn.md +27 -0
- package/docs/web/plugin-notify.md +24 -0
- package/docs/web/plugin-ponytail.md +23 -0
- package/docs/web/plugin-setup.md +7 -0
- package/docs/web/plugin-workflow.md +24 -0
- package/docs/web/plugins.md +17 -0
- package/docs/web/project-context.md +9 -0
- package/docs/web/rules.md +8 -0
- package/docs/web/run-anywhere.md +9 -0
- package/docs/web/self-improve.md +8 -0
- package/docs/web/setup-choices.md +39 -0
- package/docs/web/skills.md +8 -0
- package/docs/web/themes-author.md +8 -0
- package/docs/web/themes.md +9 -0
- package/docs/web/token-footprint.md +7 -0
- package/docs/web/trouble.md +38 -0
- package/docs/web/upgrade.md +9 -0
- package/docs/web/verify.md +30 -0
- package/docs/web/wiki.md +7 -0
- package/docs/web/worktree.md +10 -0
- package/docs/web-ui.md +144 -0
- package/docs/wiki.md +43 -0
- package/geneseed +39 -0
- package/geneseed.cmd +34 -0
- package/harness.config.json +6 -0
- package/js/anim.mjs +326 -0
- package/js/catalog.mjs +112 -0
- package/js/checkout.mjs +88 -0
- package/js/cli-table.json +1034 -0
- package/js/cli.mjs +498 -0
- package/js/diff.mjs +384 -0
- package/js/doctor.mjs +1364 -0
- package/js/emit.mjs +1373 -0
- package/js/excludes.mjs +271 -0
- package/js/generate.mjs +379 -0
- package/js/hooks.mjs +1026 -0
- package/js/hosts.mjs +253 -0
- package/js/installs.mjs +378 -0
- package/js/inventory.mjs +175 -0
- package/js/lib/fs.mjs +913 -0
- package/js/lib/proc.mjs +28 -0
- package/js/lib/udiff.mjs +275 -0
- package/js/link.mjs +286 -0
- package/js/mcp.mjs +335 -0
- package/js/memory.mjs +108 -0
- package/js/menu.mjs +118 -0
- package/js/migrate.mjs +251 -0
- package/js/native.mjs +426 -0
- package/js/opencode.mjs +325 -0
- package/js/registry.mjs +110 -0
- package/js/render.mjs +282 -0
- package/js/settings.mjs +924 -0
- package/js/setup.mjs +492 -0
- package/js/status.mjs +281 -0
- package/js/themes.mjs +195 -0
- package/js/tui.mjs +444 -0
- package/js/uninstall.mjs +1137 -0
- package/js/update.mjs +910 -0
- package/js/web/actions.mjs +868 -0
- package/js/web/activity.mjs +284 -0
- package/js/web/api.mjs +873 -0
- package/js/web/docs.mjs +531 -0
- package/js/web/graph.mjs +0 -0
- package/js/web/jobs.mjs +480 -0
- package/js/web/server.mjs +1135 -0
- package/package.json +54 -0
- package/registry.json +474 -0
- package/src/AGENT.md.tmpl +390 -0
- package/src/agents/_template.md +76 -0
- package/src/agents/advocate.md +36 -0
- package/src/agents/architect.md +55 -0
- package/src/agents/developer.md +66 -0
- package/src/agents/docs.md +54 -0
- package/src/agents/empiricist.md +39 -0
- package/src/agents/explorer.md +54 -0
- package/src/agents/framer.md +36 -0
- package/src/agents/historian.md +51 -0
- package/src/agents/operator.md +37 -0
- package/src/agents/pragmatist.md +37 -0
- package/src/agents/reviewer.md +52 -0
- package/src/agents/security.md +57 -0
- package/src/agents/skeptic.md +48 -0
- package/src/agents/steward.md +36 -0
- package/src/agents/tester.md +54 -0
- package/src/agents/user-advocate.md +36 -0
- package/src/agents/visionary.md +36 -0
- package/src/laws/universal.md +387 -0
- package/src/memory/README.md +109 -0
- package/src/memory/gitignore +6 -0
- package/src/modes/README.md +26 -0
- package/src/modes/direct.md +2 -0
- package/src/modes/foreman.md +19 -0
- package/src/notebook/README.md +39 -0
- package/src/notebook/gitignore +7 -0
- package/src/postures/README.md +31 -0
- package/src/postures/artisan.md +14 -0
- package/src/postures/assistant.md +12 -0
- package/src/postures/expert.md +11 -0
- package/src/postures/mentor.md +11 -0
- package/src/postures/peer.md +12 -0
- package/src/skills/_template.md +47 -0
- package/src/skills/brainstorm.md +28 -0
- package/src/skills/clarify.md +28 -0
- package/src/skills/codebase-design.md +44 -0
- package/src/skills/commit.md +48 -0
- package/src/skills/council.md +34 -0
- package/src/skills/crash-course.md +28 -0
- package/src/skills/daydream/.claude-plugin/plugin.json +9 -0
- package/src/skills/daydream/SKILL.md +74 -0
- package/src/skills/daydream/VENDOR.md +25 -0
- package/src/skills/daydream/critic-prompt.md +38 -0
- package/src/skills/daydream/instructions.md +422 -0
- package/src/skills/daydream/synthesizer-prompt.md +41 -0
- package/src/skills/debug.md +37 -0
- package/src/skills/decode.md +28 -0
- package/src/skills/develop.md +29 -0
- package/src/skills/document-project.md +79 -0
- package/src/skills/domain-modeling.md +38 -0
- package/src/skills/drill.md +28 -0
- package/src/skills/feynman.md +26 -0
- package/src/skills/forge-mcp.md +48 -0
- package/src/skills/fresh-eyes.md +29 -0
- package/src/skills/frontend-design.md +58 -0
- package/src/skills/gap-detector.md +27 -0
- package/src/skills/geneseed-code-review.md +33 -0
- package/src/skills/geneseed.md +29 -0
- package/src/skills/git-archaeology.md +47 -0
- package/src/skills/git-rescue.md +68 -0
- package/src/skills/handoff.md +29 -0
- package/src/skills/herdr.md +164 -0
- package/src/skills/ingest.md +49 -0
- package/src/skills/learning-path.md +26 -0
- package/src/skills/migrate.md +45 -0
- package/src/skills/opencode-theme.md +52 -0
- package/src/skills/parallel-agents.md +28 -0
- package/src/skills/pipeline.md +52 -0
- package/src/skills/plan.md +45 -0
- package/src/skills/ponytail.md +29 -0
- package/src/skills/profile.md +58 -0
- package/src/skills/prose.md +42 -0
- package/src/skills/prototype.md +44 -0
- package/src/skills/react-view-transitions/AGENTS.md +955 -0
- package/src/skills/react-view-transitions/README.md +42 -0
- package/src/skills/react-view-transitions/SKILL.md +320 -0
- package/src/skills/react-view-transitions/VENDOR.md +16 -0
- package/src/skills/react-view-transitions/metadata.json +12 -0
- package/src/skills/react-view-transitions/references/css-recipes.md +242 -0
- package/src/skills/react-view-transitions/references/implementation.md +182 -0
- package/src/skills/react-view-transitions/references/nextjs.md +176 -0
- package/src/skills/react-view-transitions/references/patterns.md +262 -0
- package/src/skills/refactor.md +32 -0
- package/src/skills/release.md +43 -0
- package/src/skills/repo-map.md +36 -0
- package/src/skills/research.md +42 -0
- package/src/skills/review-response.md +36 -0
- package/src/skills/roast-me.md +28 -0
- package/src/skills/rule.md +100 -0
- package/src/skills/ship.md +49 -0
- package/src/skills/tdd.md +31 -0
- package/src/skills/tickets.md +44 -0
- package/src/skills/token-report/SKILL.md +76 -0
- package/src/skills/token-report/VENDOR.md +23 -0
- package/src/skills/token-report/scripts/token_report.mjs +1301 -0
- package/src/skills/wayfinder.md +49 -0
- package/src/skills/wiki.md +68 -0
- package/src/skills/workflow.md +38 -0
- package/themes/README.md +69 -0
- package/themes/_TEMPLATE.json +147 -0
- package/themes/biker.json +147 -0
- package/themes/commentator.json +147 -0
- package/themes/cyberpunk.json +147 -0
- package/themes/gamer.json +147 -0
- package/themes/imperial.json +147 -0
- package/themes/joker.json +147 -0
- package/themes/marvin.json +147 -0
- package/themes/mean.json +147 -0
- package/themes/military.json +147 -0
- package/themes/neutral.json +147 -0
- package/themes/opencode/README.md +58 -0
- package/themes/opencode/catppuccin.json +27 -0
- package/themes/opencode/dracula.json +27 -0
- package/themes/opencode/everforest.json +27 -0
- package/themes/opencode/gruvbox.json +27 -0
- package/themes/opencode/kanagawa.json +27 -0
- package/themes/opencode/neon.json +27 -0
- package/themes/opencode/nord.json +27 -0
- package/themes/opencode/rosepine.json +27 -0
- package/themes/opencode/tokyonight.json +27 -0
- package/themes/pirate.json +147 -0
- package/themes/sports.json +147 -0
- package/themes/verstappen.json +147 -0
- package/themes/wizard.json +147 -0
- package/web/dist/assets/About-DX1-VTBP.js +1 -0
- package/web/dist/assets/Activity-Dp9NaXOn.js +1 -0
- package/web/dist/assets/ActivityDetail-HpOBtrXS.js +1 -0
- package/web/dist/assets/Diff-Ct4G3Se8.js +5 -0
- package/web/dist/assets/Doctor-DGjkZFuA.js +1 -0
- package/web/dist/assets/Graph-b8o-S6kv.js +1 -0
- package/web/dist/assets/Harnesses-gr3-aHBF.js +1 -0
- package/web/dist/assets/Laws-DTwF5tTC.js +1 -0
- package/web/dist/assets/Library-BEZscyaQ.js +1 -0
- package/web/dist/assets/Markdown-k_LSLBGq.js +1 -0
- package/web/dist/assets/Profile-DydukTxP.js +1 -0
- package/web/dist/assets/Rules-CIRG4Ldt.js +1 -0
- package/web/dist/assets/Skills-Buoi1IBr.js +1 -0
- package/web/dist/assets/StatusBadge-CE_AJttl.js +1 -0
- package/web/dist/assets/Themes-CI4o3gJC.js +2 -0
- package/web/dist/assets/activity-BCW7e6Cy.js +1 -0
- package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-400-italic-Bserb8X5.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-400-normal-BG6hkEXj.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-500-italic-BGfwwhek.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-500-normal-DrDcrrxK.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-600-normal-CIXX6EOa.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-700-normal-CeQ8H3UY.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
- package/web/dist/assets/index-B1PIx7RM.js +1 -0
- package/web/dist/assets/index-BnTQUSUz.js +12 -0
- package/web/dist/assets/index-Ck2iCdaC.js +2 -0
- package/web/dist/assets/index-DUCsDv7z.css +1 -0
- package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/web/dist/assets/marked.esm-JKvyS0Pp.js +46 -0
- package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
- package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
- package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
- package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/web/dist/assets/react-DOpiTn5v.js +32 -0
- package/web/dist/assets/roman-DkYN0LX9.js +1 -0
- package/web/dist/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-600-normal-DjKNqYRj.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-400-italic-YylcN9Ay.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-400-normal-Rg4St2Dn.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-700-italic-vNvENeTh.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-700-normal-mWgeinG7.woff2 +0 -0
- package/web/dist/icon-maskable.svg +10 -0
- package/web/dist/icon.svg +8 -0
- package/web/dist/index.html +21 -0
- package/web/dist/manifest.webmanifest +14 -0
- package/web/dist/sw.js +40 -0
- package/web/src/pages/Laws.jsx +230 -0
package/js/mcp.mjs
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server presets, state and config I/O — `rituals/_harness_mcp.py`'s MCP block.
|
|
3
|
+
*
|
|
4
|
+
* WHAT THE PLAN SAID AND WHAT READING FOUND. The P6f plan scored `api_mcp` + `api_mcp_toggle`
|
|
5
|
+
* at "~90 new, already in js/: `_mcp_load`/`readJsonc` (P6c), `settings.mjs`'s atomic write".
|
|
6
|
+
* Measured against `js/`: `readJsonc` had crossed and NOTHING ELSE HAD. `js/web/api.mjs`
|
|
7
|
+
* carries a five-line `mcpLoad` that is the no-host, comment-tolerant reader P6c needed for
|
|
8
|
+
* `wiki.jsonc`, which is one arm of one of these thirteen functions. `_MCP_PRESETS`,
|
|
9
|
+
* `_mcp_servers_key`, `_mcp_preset_block`, `_mcp_apply`, `_mcp_state`, `_mcp_set_enabled`,
|
|
10
|
+
* `_mcp_load`'s host fork, `_mcp_commented`, `_mcp_save`, `_mcp_config_for`,
|
|
11
|
+
* `_mcp_install_targets`, `_mcp_known_names` and `_mcp_meta` are 168 lines of Python, all
|
|
12
|
+
* new. The endpoints on top of them are the small half.
|
|
13
|
+
*
|
|
14
|
+
* AND `settings.mjs`'s ATOMIC WRITE IS NOT THIS ONE. `atomicWriteJson` is
|
|
15
|
+
* `_build_settings._atomic_write_json`; `_mcp_save` is a different function with a different
|
|
16
|
+
* temp suffix (`.tmp`, not `.geneseed-tmp`) that also creates the parent directory. Reusing
|
|
17
|
+
* the first would be right until the moment a write fails, which is the moment the temp
|
|
18
|
+
* file's name becomes the only evidence of what happened.
|
|
19
|
+
*
|
|
20
|
+
* THE ROUND TRIP IS THE HAZARD, and it is why every read here goes through `parseJson`.
|
|
21
|
+
* `_mcp_save` rewrites the WHOLE config, not the servers map — and for a Claude global
|
|
22
|
+
* install that config is `~/.claude.json`, which holds projects and history far beyond MCP
|
|
23
|
+
* wiring. A `JSON.parse` would turn every `"temperature": 1.0` in somebody's real file into
|
|
24
|
+
* `1` on the way back out. `readJsonc` already learned this; the strict-JSON fork has to
|
|
25
|
+
* learn it separately, because it does not go through `readJsonc` at all.
|
|
26
|
+
*
|
|
27
|
+
* A PyNumber IS AN OBJECT TO `typeof`, which is why `isDict` is spelled out rather than
|
|
28
|
+
* written as `typeof v === 'object'`. Python's `isinstance(x, dict)` is false for a float;
|
|
29
|
+
* the JS twin has to be false for its wrapper.
|
|
30
|
+
*/
|
|
31
|
+
import { existsSync, mkdirSync, renameSync } from 'node:fs';
|
|
32
|
+
import os from 'node:os';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
|
|
35
|
+
import { bobConfigDir, copilotConfigDir, pyResolve } from './hosts.mjs';
|
|
36
|
+
import { installState, installTargets } from './installs.mjs';
|
|
37
|
+
import { jsonDumpsIndent, parseJson, pyLjust, pyPrint, pyPrintErr, readText, writeText }
|
|
38
|
+
from './lib/fs.mjs';
|
|
39
|
+
import { opencodeTarget, readJsonc } from './settings.mjs';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `isinstance(x, dict)` for a value that came out of `json.loads`.
|
|
43
|
+
*
|
|
44
|
+
* A JSON value is null, a boolean, a string, a NUMBER, an array or an object — and
|
|
45
|
+
* `parseJson` hands numbers back wrapped, so `typeof v === 'object'` says yes to a float.
|
|
46
|
+
* `constructor === Object` is the plain-object test, and every object `JSON.parse` builds
|
|
47
|
+
* is one.
|
|
48
|
+
*/
|
|
49
|
+
const isDict = (v) => v !== null && v !== undefined && typeof v === 'object'
|
|
50
|
+
&& v.constructor === Object;
|
|
51
|
+
|
|
52
|
+
/** `dict.get(key, default)`. */
|
|
53
|
+
const dget = (obj, key, dflt) => (isDict(obj) && Object.hasOwn(obj, key) ? obj[key] : dflt);
|
|
54
|
+
|
|
55
|
+
/** The hosts whose MCP config is strict JSON under `mcpServers`, with no `enabled` flag. */
|
|
56
|
+
const FLAGLESS = ['claude', 'bob', 'copilot'];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `_harness_mcp._MCP_PRESETS` — the ready-to-wire servers the MCP screen can toggle.
|
|
60
|
+
*
|
|
61
|
+
* The `desc` strings go on the wire verbatim (`api_mcp` returns them), so they are the one
|
|
62
|
+
* place in this file where a byte matters more than the shape. Insertion order is the
|
|
63
|
+
* listing order.
|
|
64
|
+
*/
|
|
65
|
+
export const MCP_PRESETS = {
|
|
66
|
+
markitdown: {
|
|
67
|
+
label: 'MarkItDown',
|
|
68
|
+
desc: 'PDF / Office / HTML -> Markdown for the ingest skill. Runs via `uvx` '
|
|
69
|
+
+ '(needs uv; zero-install, fetches on first call). No uv? Switch the command '
|
|
70
|
+
+ 'to ["markitdown-mcp"] after `pipx install markitdown-mcp`. Exposes one '
|
|
71
|
+
+ 'tool: convert_to_markdown(uri).',
|
|
72
|
+
block: { type: 'local', command: ['uvx', 'markitdown-mcp'], enabled: true },
|
|
73
|
+
},
|
|
74
|
+
gitlab: {
|
|
75
|
+
label: 'GitLab',
|
|
76
|
+
desc: 'GitLab repo / MR / issue / CI tools via @zereight/mcp-gitlab (npx, no '
|
|
77
|
+
+ 'install). Edit GITLAB_PERSONAL_ACCESS_TOKEN (scopes: api, read_repository) '
|
|
78
|
+
+ 'and GITLAB_API_URL before use. Add a second entry (gitlab-2) for another '
|
|
79
|
+
+ 'instance.',
|
|
80
|
+
block: {
|
|
81
|
+
type: 'local',
|
|
82
|
+
command: ['npx', '-y', '@zereight/mcp-gitlab'],
|
|
83
|
+
environment: { GITLAB_PERSONAL_ACCESS_TOKEN: '',
|
|
84
|
+
GITLAB_API_URL: 'https://gitlab.com/api/v4' },
|
|
85
|
+
enabled: true,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
'gitlab-2': {
|
|
89
|
+
label: 'GitLab (2nd instance)',
|
|
90
|
+
desc: 'A second GitLab instance (e.g. a self-hosted server) via the same '
|
|
91
|
+
+ '@zereight/mcp-gitlab command. Point GITLAB_API_URL at the other instance '
|
|
92
|
+
+ "and give it that instance's own token.",
|
|
93
|
+
block: {
|
|
94
|
+
type: 'local',
|
|
95
|
+
command: ['npx', '-y', '@zereight/mcp-gitlab'],
|
|
96
|
+
environment: { GITLAB_PERSONAL_ACCESS_TOKEN: '',
|
|
97
|
+
GITLAB_API_URL: 'https://gitlab.example.com/api/v4' },
|
|
98
|
+
enabled: true,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
filesystem: {
|
|
102
|
+
label: 'Filesystem',
|
|
103
|
+
desc: 'Read/write files under explicitly allowed directories via '
|
|
104
|
+
+ '@modelcontextprotocol/server-filesystem (npx, no install). Replace the '
|
|
105
|
+
+ 'trailing path arg(s) with only the dir(s) the agent may touch '
|
|
106
|
+
+ '(least-privilege).',
|
|
107
|
+
block: {
|
|
108
|
+
type: 'local',
|
|
109
|
+
command: ['npx', '-y', '@modelcontextprotocol/server-filesystem',
|
|
110
|
+
'/path/to/allowed/dir'],
|
|
111
|
+
enabled: true,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** `_mcp_servers_key` — OpenCode nests servers under `mcp`, the other three `mcpServers`. */
|
|
117
|
+
const serversKey = (host) => (FLAGLESS.includes(host) ? 'mcpServers' : 'mcp');
|
|
118
|
+
|
|
119
|
+
/** `(config.get(key) or {})` — the servers map, or an empty one. */
|
|
120
|
+
const serversOf = (config, host) => {
|
|
121
|
+
const s = dget(config, serversKey(host), null);
|
|
122
|
+
return isDict(s) ? s : {};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `_mcp_preset_block` — the block to WRITE for a preset, shaped for `host`.
|
|
127
|
+
*
|
|
128
|
+
* OpenCode takes it verbatim. The flagless three split the command list into `command`
|
|
129
|
+
* (head) + `args` (tail) and rename `environment` to `env`; Copilot additionally requires
|
|
130
|
+
* `type` and a `tools` allowlist.
|
|
131
|
+
*/
|
|
132
|
+
export function mcpPresetBlock(name, host = 'opencode') {
|
|
133
|
+
const block = { ...MCP_PRESETS[name].block };
|
|
134
|
+
if (!FLAGLESS.includes(host)) return block;
|
|
135
|
+
const cmd = [...(block.command || [])];
|
|
136
|
+
const out = { command: cmd.length ? cmd[0] : '', args: cmd.slice(1) };
|
|
137
|
+
const env = block.environment;
|
|
138
|
+
if (env) out.env = { ...env };
|
|
139
|
+
if (host === 'copilot') {
|
|
140
|
+
out.type = 'local';
|
|
141
|
+
out.tools = ['*'];
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* `_mcp_apply` — a COPY of `config` with server `name` set to `block`, or removed when
|
|
148
|
+
* `block` is null. Never touches another key; drops an emptied servers map.
|
|
149
|
+
*
|
|
150
|
+
* The key ORDER is behaviour, not cosmetics: `setdefault` appends `$schema` at the end of a
|
|
151
|
+
* fresh dict and leaves an existing one where it is, and `_mcp_save` writes the result out
|
|
152
|
+
* as the file the user then reads. `{...config}` plus a guarded assignment is the same rule.
|
|
153
|
+
*/
|
|
154
|
+
export function mcpApply(config, name, block, host = 'opencode') {
|
|
155
|
+
const cfg = { ...config };
|
|
156
|
+
const key = serversKey(host);
|
|
157
|
+
if (host === 'opencode' && !Object.hasOwn(cfg, '$schema')) {
|
|
158
|
+
cfg.$schema = 'https://opencode.ai/config.json';
|
|
159
|
+
}
|
|
160
|
+
const servers = { ...serversOf(cfg, host) };
|
|
161
|
+
if (block === null) delete servers[name];
|
|
162
|
+
else servers[name] = block;
|
|
163
|
+
if (Object.keys(servers).length) cfg[key] = servers;
|
|
164
|
+
else delete cfg[key];
|
|
165
|
+
return cfg;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* `_mcp_state` — 'enabled' | 'disabled' | 'absent'.
|
|
170
|
+
*
|
|
171
|
+
* OpenCode: a present server with no explicit `enabled` counts as enabled (its default).
|
|
172
|
+
* The flagless three have no such key, so present IS enabled and 'disabled' never occurs —
|
|
173
|
+
* toggling one off removes its entry instead.
|
|
174
|
+
*/
|
|
175
|
+
export function mcpState(config, name, host = 'opencode') {
|
|
176
|
+
const server = serversOf(config, host)[name];
|
|
177
|
+
if (!isDict(server)) return 'absent';
|
|
178
|
+
if (FLAGLESS.includes(host)) return 'enabled';
|
|
179
|
+
return dget(server, 'enabled', true) ? 'enabled' : 'disabled';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** `_mcp_set_enabled` — flip a present server's enabled-ness. No-op when it is absent. */
|
|
183
|
+
export function mcpSetEnabled(config, name, enabled, host = 'opencode') {
|
|
184
|
+
const server = serversOf(config, host)[name];
|
|
185
|
+
if (!isDict(server)) return config;
|
|
186
|
+
if (FLAGLESS.includes(host)) {
|
|
187
|
+
return enabled ? config : mcpApply(config, name, null, host);
|
|
188
|
+
}
|
|
189
|
+
return mcpApply(config, name, { ...server, enabled }, host);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* `_mcp_load` — a config as a dict; `{}` when missing, unreadable, unparseable or not an
|
|
194
|
+
* object.
|
|
195
|
+
*
|
|
196
|
+
* The host fork is the whole function. OpenCode is read comment-tolerantly because a
|
|
197
|
+
* hand-maintained `opencode.jsonc` carries `//`; the other three are read STRICTLY, because
|
|
198
|
+
* the comment stripper's trailing-comma pass is not string-aware and would silently drop a
|
|
199
|
+
* comma from any string value containing `,]` — in a file that holds projects and history.
|
|
200
|
+
*/
|
|
201
|
+
export function mcpLoad(p, host = 'opencode') {
|
|
202
|
+
if (!existsSync(p)) return {};
|
|
203
|
+
let text;
|
|
204
|
+
try {
|
|
205
|
+
text = readText(p);
|
|
206
|
+
} catch {
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
if (FLAGLESS.includes(host)) {
|
|
210
|
+
let data;
|
|
211
|
+
try {
|
|
212
|
+
data = parseJson(text);
|
|
213
|
+
} catch {
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
return isDict(data) ? data : {};
|
|
217
|
+
}
|
|
218
|
+
const [data] = readJsonc(text);
|
|
219
|
+
return isDict(data) ? data : {};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** `_mcp_commented` — an existing `.jsonc` that carries comments, which must not be rewritten. */
|
|
223
|
+
export function mcpCommented(p) {
|
|
224
|
+
if (path.extname(p) !== '.jsonc' || !existsSync(p)) return false;
|
|
225
|
+
try {
|
|
226
|
+
const [, had] = readJsonc(readText(p));
|
|
227
|
+
return had;
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* `_mcp_save` — pretty JSON, written ATOMICALLY through a sibling temp file.
|
|
235
|
+
*
|
|
236
|
+
* Not `atomicWriteJson`: that is `_build_settings._atomic_write_json`, whose temp file is
|
|
237
|
+
* named `.geneseed-tmp` and which does not create the parent directory. Two Python
|
|
238
|
+
* functions, two twins.
|
|
239
|
+
*/
|
|
240
|
+
export function mcpSave(p, config) {
|
|
241
|
+
mkdirSync(path.dirname(p), { recursive: true });
|
|
242
|
+
const tmp = `${p}.tmp`;
|
|
243
|
+
writeText(tmp, `${jsonDumpsIndent(config)}\n`);
|
|
244
|
+
renameSync(tmp, p);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* `_mcp_config_for` — the file an install writes its MCP servers into, or null for a host
|
|
249
|
+
* that carries no MCP wiring at that scope.
|
|
250
|
+
*/
|
|
251
|
+
export function mcpConfigFor(host, scope, root) {
|
|
252
|
+
if (host === 'opencode') return opencodeTarget(path.join(root, 'opencode.json'));
|
|
253
|
+
if (host === 'claude') {
|
|
254
|
+
return scope === 'project' ? path.join(root, '.mcp.json')
|
|
255
|
+
: pyResolve(path.join(os.homedir(), '.claude.json'));
|
|
256
|
+
}
|
|
257
|
+
if (host === 'bob') {
|
|
258
|
+
return scope === 'project' ? path.join(root, '.bob', 'settings.json')
|
|
259
|
+
: path.join(bobConfigDir(), 'settings.json');
|
|
260
|
+
}
|
|
261
|
+
if (host === 'copilot') {
|
|
262
|
+
// The Copilot CLI reads MCP servers from ~/.copilot/mcp-config.json only — no per-repo
|
|
263
|
+
// file is documented, so a project install carries no MCP wiring.
|
|
264
|
+
return scope === 'global' ? path.join(copilotConfigDir(), 'mcp-config.json') : null;
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** `_mcp_install_targets` — (label, configFile, host, scope, installRoot) per detected install. */
|
|
270
|
+
export function mcpInstallTargets() {
|
|
271
|
+
const out = [];
|
|
272
|
+
for (const [host, scope, root] of installTargets()) {
|
|
273
|
+
const cfg = mcpConfigFor(host, scope, root);
|
|
274
|
+
if (cfg !== null) out.push([`${scope} config`, cfg, host, scope, root]);
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** `_mcp_known_names` — the presets first, then any server already in THIS config. */
|
|
280
|
+
export function mcpKnownNames(config, host = 'opencode') {
|
|
281
|
+
const names = Object.keys(MCP_PRESETS);
|
|
282
|
+
const present = isDict(config) ? Object.keys(serversOf(config, host)) : [];
|
|
283
|
+
return names.concat(present.filter((n) => !Object.hasOwn(MCP_PRESETS, n)));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** `_mcp_meta` — (label, description); the bare name and a generic note for an unknown one. */
|
|
287
|
+
export function mcpMeta(name) {
|
|
288
|
+
const p = MCP_PRESETS[name];
|
|
289
|
+
if (p) return [p.label, p.desc];
|
|
290
|
+
return [name, 'User-defined MCP server (not a Harness preset). It lives in this '
|
|
291
|
+
+ "config already — 'e' enables/disables it, Enter removes it."];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export { isDict };
|
|
295
|
+
|
|
296
|
+
// ---- the verb ----------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* `geneseed mcp` — the CLI FACE this library never had.
|
|
300
|
+
*
|
|
301
|
+
* Everything above crossed with the web console rather than with a subcommand: the reference
|
|
302
|
+
* kept it as a preset/state library whose only command was the uninstall path, and the one
|
|
303
|
+
* way to see which servers an install had wired was to start a server and look. That is the
|
|
304
|
+
* gap this verb closes, and it closes it by CALLING the library — the same `mcpInstallTargets`
|
|
305
|
+
* / `mcpLoad` / `mcpKnownNames` / `mcpState` / `mcpMeta` sequence the console's read endpoint
|
|
306
|
+
* runs, rendered as text instead of as JSON.
|
|
307
|
+
*
|
|
308
|
+
* ponytail: READ-ONLY, on purpose, and this is the ceiling. Wiring a server from here needs
|
|
309
|
+
* the console's toggle orchestration, whose load-bearing part is not the toggle at all — it
|
|
310
|
+
* is the refusal to rewrite a config it could not parse strictly, because for a global
|
|
311
|
+
* Claude install that file holds projects and history and a rewrite from an empty object
|
|
312
|
+
* would erase them. A second copy of that guard is a second place for it to be wrong. When a
|
|
313
|
+
* terminal toggle is wanted, move the console's non-HTTP core down into this module and have
|
|
314
|
+
* both faces call it; do not grow a parallel one here.
|
|
315
|
+
*/
|
|
316
|
+
export function cmdMcp() {
|
|
317
|
+
const targets = mcpInstallTargets()
|
|
318
|
+
.filter(([, , host, scope, root]) => installState(root, host, scope) === 'active');
|
|
319
|
+
if (!targets.length) {
|
|
320
|
+
pyPrintErr('[mcp] no active Geneseed install found. Run `geneseed setup` first.\n');
|
|
321
|
+
return 1;
|
|
322
|
+
}
|
|
323
|
+
for (const [label, p, host] of targets) {
|
|
324
|
+
const cfg = mcpLoad(p, host);
|
|
325
|
+
pyPrint(`[mcp] ${host} ${label}: ${p}${existsSync(p) ? '' : ' (not written yet)'}\n`);
|
|
326
|
+
if (mcpCommented(p)) {
|
|
327
|
+
pyPrint(' this config carries comments — edit it by hand so they survive.\n');
|
|
328
|
+
}
|
|
329
|
+
for (const name of mcpKnownNames(cfg, host)) {
|
|
330
|
+
const [lbl] = mcpMeta(name);
|
|
331
|
+
pyPrint(` ${pyLjust(mcpState(cfg, name, host), 8)} ${pyLjust(name, 12)} ${lbl}\n`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
package/js/memory.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `geneseed memory` — the memory store from a terminal.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS VERB IS NEW RATHER THAN PORTED. The reference had no `memory` subcommand: a fact
|
|
5
|
+
* could be written by the `learn` hook and deleted from the web console, and there was no
|
|
6
|
+
* third way. So the semantics here are the DELETE ENDPOINT's, deliberately and field for
|
|
7
|
+
* field — the same reserved names, the same "a bare slug, never a path" rule, the same
|
|
8
|
+
* index line dropped from MEMORY.md after the file goes — because two ways to remove a fact
|
|
9
|
+
* that disagree about what a fact is would be worse than one way.
|
|
10
|
+
*
|
|
11
|
+
* WHERE IT PARTS COMPANY WITH THE ENDPOINT, AND WHY. Two places, both forced by the
|
|
12
|
+
* difference between a browser talking to a running server and a person typing in a shell:
|
|
13
|
+
*
|
|
14
|
+
* * THE STORE IS RESOLVED, NOT CONFIGURED. The endpoint is always `<target>/memory`,
|
|
15
|
+
* because a server has a target. A CLI has a cwd, so this takes the resolver every other
|
|
16
|
+
* terminal-side reader of the store already uses — `$GENESEED_MEMORY`, then a store
|
|
17
|
+
* beside the cwd, then the global config dir — with `--memory` as the explicit override.
|
|
18
|
+
* `status` prints the answer of that same resolver, so the two agree about which store
|
|
19
|
+
* is "the" store.
|
|
20
|
+
* * THE RESERVED-NAME CHECK IS CASE-INSENSITIVE, WHICH THE ENDPOINT'S IS NOT. It compares
|
|
21
|
+
* `MEMORY` and `README` exactly. On a case-insensitive filesystem — which is every
|
|
22
|
+
* Windows install and the default macOS one — `rm memory` then resolves to the index
|
|
23
|
+
* file, passes the guard, and deletes the thing the agent reads at session start. This
|
|
24
|
+
* is a NARROWING of what the endpoint accepts, on a destructive path, and it is the one
|
|
25
|
+
* divergence in this file that is not about resolution.
|
|
26
|
+
*
|
|
27
|
+
* `memoryDropIndex` is the hook module's, not a copy: writing the fact and unwriting its
|
|
28
|
+
* index line are two halves of one format, and the half that writes lives there.
|
|
29
|
+
*/
|
|
30
|
+
import { readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { memoryDropIndex } from './hooks.mjs';
|
|
34
|
+
import { resolveMemoryDir } from './hosts.mjs';
|
|
35
|
+
import { pyPrint, pyPrintErr } from './lib/fs.mjs';
|
|
36
|
+
|
|
37
|
+
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The two names the store owns rather than holds: the index the agent reads at session
|
|
41
|
+
* start, and the file that documents the store. `api_memory_delete` refuses both.
|
|
42
|
+
*/
|
|
43
|
+
const RESERVED = ['memory', 'readme'];
|
|
44
|
+
|
|
45
|
+
/** Whether `name` is a fact this verb may act on — the endpoint's rule, plus the case fold. */
|
|
46
|
+
export function isFactName(name) {
|
|
47
|
+
return Boolean(name) && !name.includes('/') && !name.includes('\\')
|
|
48
|
+
&& !RESERVED.includes(name.toLowerCase());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The removable facts in `dir`, as bare slugs, sorted.
|
|
53
|
+
*
|
|
54
|
+
* DERIVED FROM `isFactName` RATHER THAN FROM ITS OWN FILTER, so a listing can never offer a
|
|
55
|
+
* name the removal then refuses, and can never hide one it would accept.
|
|
56
|
+
*/
|
|
57
|
+
export function memoryFacts(dir) {
|
|
58
|
+
let names;
|
|
59
|
+
try { names = readdirSync(dir); } catch { return []; }
|
|
60
|
+
return names.filter((n) => n.endsWith('.md') && isFile(path.join(dir, n)))
|
|
61
|
+
.map((n) => n.slice(0, -3)).filter(isFactName).sort();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `api_memory_delete`, minus the HTTP: the file, then its line in the index. */
|
|
65
|
+
export function memoryDelete(dir, name) {
|
|
66
|
+
if (!isFactName(name)) return false;
|
|
67
|
+
const p = path.join(dir, `${name}.md`);
|
|
68
|
+
if (!isFile(p)) return false;
|
|
69
|
+
unlinkSync(p);
|
|
70
|
+
memoryDropIndex(dir, name);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The verb. */
|
|
75
|
+
export function cmdMemory(args) {
|
|
76
|
+
const dir = resolveMemoryDir(args.memory);
|
|
77
|
+
if (dir === null) {
|
|
78
|
+
// The resolver answers null for "no store anywhere on the chain", which is also what an
|
|
79
|
+
// explicit `--memory` that is not a directory gives — so the message names both.
|
|
80
|
+
pyPrintErr('[memory] no memory store found. Pass --memory <dir>, set $GENESEED_MEMORY, '
|
|
81
|
+
+ 'or run this from a directory with a memory/ store.\n');
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
if (args.action === 'list') {
|
|
85
|
+
const facts = memoryFacts(dir);
|
|
86
|
+
if (!facts.length) {
|
|
87
|
+
pyPrint(`[memory] ${dir}: no facts.\n`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
pyPrint(`[memory] ${dir}\n`);
|
|
91
|
+
for (const name of facts) pyPrint(` ${name}\n`);
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
if (!args.name) {
|
|
95
|
+
pyPrintErr('[memory] rm needs the name of a fact.\n');
|
|
96
|
+
return 2;
|
|
97
|
+
}
|
|
98
|
+
if (!memoryDelete(dir, args.name)) {
|
|
99
|
+
// ONE MESSAGE FOR BOTH REFUSALS, which is the endpoint's shape too — it raises the same
|
|
100
|
+
// `NotFound(name)` for a reserved name, a name with a separator and a fact that is not
|
|
101
|
+
// there. Telling a caller that `MEMORY` exists but is protected is a distinction this
|
|
102
|
+
// verb has no reason to draw.
|
|
103
|
+
pyPrintErr(`[memory] no such fact '${args.name}' in ${dir}.\n`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
pyPrint(`[memory] deleted ${args.name}.\n`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
package/js/menu.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `menu` and `home` — `rituals/_harness_menu.py`'s two dispatchers.
|
|
3
|
+
*
|
|
4
|
+
* WHAT CROSSES HERE AND WHAT DOES NOT, said plainly, because this is the one module in the
|
|
5
|
+
* port whose Python twin is 3,398 lines and whose Node twin is ninety.
|
|
6
|
+
*
|
|
7
|
+
* `cmd_menu` is a FORK, not a menu. Off a TTY it prints five lines of command list and
|
|
8
|
+
* returns 0; on a TTY it hands control to `curses.wrapper(_main_menu)` — the full-screen
|
|
9
|
+
* panel, which is P7c's and is not here. The fork's off-TTY arm is what every cell of this
|
|
10
|
+
* port can reach, and it is ported byte for byte.
|
|
11
|
+
*
|
|
12
|
+
* ON A TTY, THIS ENTRY FALLS BACK RATHER THAN INVENTING A SECOND MENU. The reference already
|
|
13
|
+
* has that behaviour: `cmd_menu` wraps the panel in `except Exception`, writes `[menu] TUI
|
|
14
|
+
* unavailable (<reason>)` to stderr and prints the same command list. Node's reason is
|
|
15
|
+
* permanent rather than accidental, and it is stated as such.
|
|
16
|
+
*
|
|
17
|
+
* ⚠ THE REASON THIS HEADER ORIGINALLY GAVE WAS WRONG, and P7b measured it: it said the
|
|
18
|
+
* reference reaches that arm often because "`import curses` fails on stock Windows Python".
|
|
19
|
+
* A bare `import curses` does fail here, but `rituals/_harness_core.py` installs
|
|
20
|
+
* `rituals/_winterm.py` under the name `curses` when it does — so inside the harness the
|
|
21
|
+
* import succeeds, the panel opens, and it is the PORT that has no window. The fallback is
|
|
22
|
+
* still the right shape; only its explanation moved. The alternative that was considered and
|
|
23
|
+
* refused: building a numbered line-menu out of `js/setup.mjs`'s `askChoice`. That is not a
|
|
24
|
+
* port of `_main_menu` — it is a THIRD user interface, differing from the reference on every
|
|
25
|
+
* screen, unreachable from every cell (no cell has a TTY), and it would have to be deleted
|
|
26
|
+
* the day P7c lands the real panel. The fallback is the reference's own contract for "no
|
|
27
|
+
* full-screen menu available here"; a new UI would not be.
|
|
28
|
+
*
|
|
29
|
+
* `cmd_home` is the default for a bare `geneseed`, and it crosses WHOLE. Its two arms are
|
|
30
|
+
* `_web_first_ok()` — the web console, which is P6h's `startDaemon` and is fully ported —
|
|
31
|
+
* and `cmd_menu` above. Nothing in it was deferred.
|
|
32
|
+
*
|
|
33
|
+
* `_web_first_ok`'s FIVE REFUSALS ARE THE PAYLOAD, and only two of them are reachable from a
|
|
34
|
+
* cell: a cell's stdin is a pipe, so `isTTY` is false and everything past it is dead. The
|
|
35
|
+
* other three (SSH, the Linux display server, the browser) are gated by the corpus in
|
|
36
|
+
* `tests/test_pure_function_parity.py`, which drives this function with `isTTY` FAKED — the
|
|
37
|
+
* one input no cell can vary. See that file's P7a section for what it reaches and what it
|
|
38
|
+
* declares.
|
|
39
|
+
*/
|
|
40
|
+
import { pyPrint, pyPrintErr, pyWhich } from './lib/fs.mjs';
|
|
41
|
+
import { startDaemon } from './web/server.mjs';
|
|
42
|
+
|
|
43
|
+
/** `_menu_help` — the off-TTY arm, and the fallback both TTY arms end in. */
|
|
44
|
+
export function menuHelp() {
|
|
45
|
+
pyPrint('Geneseed — no interactive menu here. Get started with: geneseed setup\n');
|
|
46
|
+
pyPrint('Other commands: bootstrap · update · build · doctor · diff · tui · web\n');
|
|
47
|
+
pyPrint('On a VT-capable terminal, a bare `./geneseed` opens the interactive menu of these.\n');
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `webbrowser.get()`, which raises `webbrowser.Error` when nothing on the machine can open
|
|
53
|
+
* a URL — and has no Node equivalent, because there is no stdlib registry of browsers.
|
|
54
|
+
*
|
|
55
|
+
* The twin answers the question this program actually needs: can `openUrl` (js/web/server.mjs)
|
|
56
|
+
* do its job? That makes the answer PER PLATFORM and identical to what `openUrl` will run —
|
|
57
|
+
* `cmd /c start` on Windows and `open` on macOS are OS builtins that are always there, and
|
|
58
|
+
* everywhere else it is `xdg-open`, which is not.
|
|
59
|
+
*
|
|
60
|
+
* DECLARED DIVERGENCE: on Linux the reference consults `$BROWSER` and a list of a dozen
|
|
61
|
+
* browser binaries before giving up, so a box with `firefox` on PATH and no `xdg-open`
|
|
62
|
+
* answers True there and false here. It is unreachable from this port's machine (the
|
|
63
|
+
* previous branch already returned false without a display server, and Windows never gets
|
|
64
|
+
* here), and closing it would mean reproducing `webbrowser`'s registry — a table of browser
|
|
65
|
+
* names that would go stale silently, which is the shape this port refuses.
|
|
66
|
+
*/
|
|
67
|
+
function browserAvailable() {
|
|
68
|
+
if (process.platform === 'win32' || process.platform === 'darwin') return true;
|
|
69
|
+
return pyWhich('xdg-open') !== null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* `_web_first_ok` — whether a bare `geneseed` opens the web UI rather than the TUI menu.
|
|
74
|
+
* Deliberately conservative in the reference and here: any doubt falls back to the menu, so
|
|
75
|
+
* a server box is never dropped into a browser launch that cannot open.
|
|
76
|
+
*/
|
|
77
|
+
export function webFirstOk() {
|
|
78
|
+
if (process.env.GENESEED_NO_WEB) return false;
|
|
79
|
+
// `process.stdin.isTTY` is `true` or UNDEFINED — never `false` — which is why this is a
|
|
80
|
+
// truthiness test and not a comparison.
|
|
81
|
+
if (!process.stdin.isTTY) return false;
|
|
82
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_TTY) return false;
|
|
83
|
+
// On Linux a browser needs a display server; macOS/Windows always have a GUI.
|
|
84
|
+
if (process.platform === 'linux'
|
|
85
|
+
&& !(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)) return false;
|
|
86
|
+
return browserAvailable();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `cmd_menu`. See this module's header for why the TTY arm falls back. */
|
|
90
|
+
export function cmdMenu() {
|
|
91
|
+
if (!process.stdin.isTTY) return menuHelp();
|
|
92
|
+
// The reference's own `except Exception` arm, with a reason that is permanent here rather
|
|
93
|
+
// than machine-dependent. stderr, and CRLF on Windows, because `sys.stderr.write` does.
|
|
94
|
+
//
|
|
95
|
+
// P7b CORRECTED THE REASON RATHER THAN THE ARM. This module's header said the fallback was
|
|
96
|
+
// reached because "`import curses` fails on stock Windows Python", and P7b measured that
|
|
97
|
+
// to be false where it matters: `rituals/_harness_core.py` installs `rituals/_winterm.py`
|
|
98
|
+
// as `sys.modules["curses"]` when the stdlib module is missing, so the reference opens its
|
|
99
|
+
// panel on this machine and `tests/test_tui_boundary.py` drives it doing so. What is
|
|
100
|
+
// missing is on THIS side — there is no window implementation in the port — and that is
|
|
101
|
+
// what the message now says.
|
|
102
|
+
// NOTHING TO POINT AT ANY MORE. The message used to end by sending the reader at the
|
|
103
|
+
// Python panel, and P2 took the pointer out rather than re-aiming it: the panel it named
|
|
104
|
+
// is the Python this migration deletes, and a refusal that sends a user to a file which
|
|
105
|
+
// will not be there is worse than one that simply says the screen does not exist.
|
|
106
|
+
pyPrintErr('[menu] TUI unavailable (this entry has no full-screen menu).\n');
|
|
107
|
+
return menuHelp();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** `cmd_home` — the default for a bare `geneseed`. */
|
|
111
|
+
export async function cmdHome(args) {
|
|
112
|
+
if (webFirstOk()) {
|
|
113
|
+
pyPrint('[geneseed] opening the web console — `geneseed menu` for the terminal UI, '
|
|
114
|
+
+ 'GENESEED_NO_WEB=1 to disable.\n');
|
|
115
|
+
return startDaemon(null, 4747, true);
|
|
116
|
+
}
|
|
117
|
+
return cmdMenu(args);
|
|
118
|
+
}
|