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
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rituals/_web_activity.py` — the live-activity surface, and the first web endpoint that
|
|
3
|
+
* DELETES.
|
|
4
|
+
*
|
|
5
|
+
* It reads the per-session JSON files the `geneseed-activity` OpenCode plugin writes into
|
|
6
|
+
* `<opencode-cfg>/activity/`, prunes the dead and the stale ones (unlinking them, so the
|
|
7
|
+
* directory self-cleans), and returns what is left. Writer and reader share only that
|
|
8
|
+
* directory: no RPC, crash-isolated, and cross-tool by construction.
|
|
9
|
+
*
|
|
10
|
+
* ONLY THE GET HALF CROSSES IN P6e. `/api/activity` answers both verbs and
|
|
11
|
+
* `api_activity_toggle` is a WRITE, so the path stays in `NOT_PORTED_POST` and the
|
|
12
|
+
* partition test in `tests/test_web_server.py` keeps that honest. The consequence for the
|
|
13
|
+
* gate is that the on/off flag can be READ but never flipped, which is why its two arms are
|
|
14
|
+
* two cells rather than two requests in one.
|
|
15
|
+
*
|
|
16
|
+
* THE PLAN GUESSED `tasklist` AND THE SOURCE SAYS OTHERWISE. `_win_pid_alive` shells
|
|
17
|
+
* nothing: it goes through `ctypes` to `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` and
|
|
18
|
+
* `GetExitCodeProcess`. So there is no sixth `_ALLOWED_SPAWNS` row to argue — not because
|
|
19
|
+
* the Node twin avoids a spawn, but because the REFERENCE never had one. `process.kill(pid,
|
|
20
|
+
* 0)` is the twin here; it is a core `process` API and not `child_process`, so the ban that
|
|
21
|
+
* P4a's source check enforces is untouched. libuv's `uv_kill` opens the process for query
|
|
22
|
+
* and answers `ESRCH` when the pid is free and `EPERM` when it cannot tell, which is
|
|
23
|
+
* exactly the reference's "no such process" / "err toward alive" pair.
|
|
24
|
+
*
|
|
25
|
+
* THE DIVERGENCE THAT LEAVES, DECLARED RATHER THAN HIDDEN. The reference also reads the
|
|
26
|
+
* exit code and calls a process that has EXITED but whose handle is still held dead;
|
|
27
|
+
* `process.kill` reports it alive, and the staleness backstop is what prunes it thirty
|
|
28
|
+
* minutes later. Reproducing the exit-code read would mean a native binding, which this
|
|
29
|
+
* port will not add for a best-effort liveness probe whose own docstring says any
|
|
30
|
+
* inconclusive answer errs toward alive.
|
|
31
|
+
*
|
|
32
|
+
* `parseJson` AND NOT `JSON.parse`, WHICH IS NOT A STYLE CHOICE. These files are written by
|
|
33
|
+
* a third party and carry `cost` — a float. Python's `json.loads` types `1.0` a float and
|
|
34
|
+
* `json.dumps` writes it back `1.0`; `JSON.parse` gives a bare `1` that `bareInts` then
|
|
35
|
+
* renders `1`. `parseJson`'s wrapper is the only thing that survives the round trip, and
|
|
36
|
+
* `s-live`'s `"cost": 1.0` is the cell that says so. Its companion is `pyTruthy`: an
|
|
37
|
+
* `entry.get("cost") or 0` over a wrapper object is TRUE for a wrapped zero, where Python's
|
|
38
|
+
* `0.0 or 0` is the int `0` — so `s-older`'s `"cost": 0.0` must come back `0` and not `0.0`.
|
|
39
|
+
*/
|
|
40
|
+
import { mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
41
|
+
import path from 'node:path';
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
normcase, parseJson, pyInt, pyStripSpace, pyTruthy, readText, writeText,
|
|
45
|
+
} from '../lib/fs.mjs';
|
|
46
|
+
import { NotFound } from './api.mjs';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `_web_activity.ACTIVITY_STALE_SECONDS` — a session whose writer is gone, or that has not
|
|
50
|
+
* been touched in this long, is pruned. pid-liveness is the real signal; the staleness
|
|
51
|
+
* backstop covers a reused pid and a platform where the probe cannot tell. Generous on
|
|
52
|
+
* purpose: streaming bumps `updated_at` constantly, so only a genuinely abandoned
|
|
53
|
+
* (process-still-alive) session ages out.
|
|
54
|
+
*/
|
|
55
|
+
export const ACTIVITY_STALE_SECONDS = 1800;
|
|
56
|
+
|
|
57
|
+
const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
|
|
58
|
+
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
59
|
+
|
|
60
|
+
/** `time.time()` — unix seconds as a float. */
|
|
61
|
+
const nowSeconds = () => Date.now() / 1000;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* `_activity_dir` — `<target>/activity`. `state.target` is ALREADY the OpenCode config dir,
|
|
65
|
+
* so this must not append `opencode/` again; that double-join is the classic seam bug.
|
|
66
|
+
*/
|
|
67
|
+
const activityDir = (state) => path.join(state.target, 'activity');
|
|
68
|
+
|
|
69
|
+
/** `_activity_flag` — the runtime on/off flag, the same path the plugin resolves. */
|
|
70
|
+
const activityFlag = (state) => path.join(state.target, '.geneseed-activity');
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* `_activity_enabled` — on unless the flag file explicitly says off. An absent (or
|
|
74
|
+
* unreadable) file is the default every user who has never toggled is in.
|
|
75
|
+
*
|
|
76
|
+
* The one input this cannot mirror is an undecodable flag file: Python's
|
|
77
|
+
* `read_text(encoding="utf-8")` raises a `UnicodeDecodeError`, which is a `ValueError` and
|
|
78
|
+
* not caught, where Node substitutes U+FFFD and reads on. No cell seeds one — it would take
|
|
79
|
+
* a binary file where a four-letter word belongs — and the reference's answer there is a
|
|
80
|
+
* 500, which is not a behaviour worth reproducing.
|
|
81
|
+
*/
|
|
82
|
+
export function activityEnabled(state) {
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = readText(activityFlag(state));
|
|
86
|
+
} catch {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return !['off', '0', 'false', 'no'].includes(pyStripSpace(raw).toLowerCase());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* `int(value)` as `_pid_alive` calls it — `TypeError` and `ValueError` both mean "not a
|
|
94
|
+
* pid", so both come back null here.
|
|
95
|
+
*
|
|
96
|
+
* The value came out of `parseJson`, so a JSON number arrives inside a wrapper this module
|
|
97
|
+
* cannot import by name. It is recognised by the only thing the wrapper promises, a numeric
|
|
98
|
+
* `valueOf` — a plain object's returns itself and an array is excluded outright, which is
|
|
99
|
+
* what `int({})` and `int([])` raising looks like from here. `int(1.9)` truncates and
|
|
100
|
+
* `int(True)` is 1, both of which a hand-edited snapshot can produce.
|
|
101
|
+
*/
|
|
102
|
+
function pyIntOf(raw) {
|
|
103
|
+
if (raw === true) return 1;
|
|
104
|
+
if (raw === false) return 0;
|
|
105
|
+
if (typeof raw === 'string') return pyInt(pyStripSpace(raw));
|
|
106
|
+
const n = (raw !== null && typeof raw === 'object' && !Array.isArray(raw))
|
|
107
|
+
? raw.valueOf() : raw;
|
|
108
|
+
if (typeof n !== 'number' || !Number.isFinite(n)) return null;
|
|
109
|
+
return Math.trunc(n);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `_pid_alive` — best-effort liveness for the writer process. See this file's header. */
|
|
113
|
+
export function pidAlive(raw) {
|
|
114
|
+
const pid = pyIntOf(raw);
|
|
115
|
+
if (pid === null || pid <= 0) return false;
|
|
116
|
+
try {
|
|
117
|
+
process.kill(pid, 0);
|
|
118
|
+
return true;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
// `ProcessLookupError` → dead. Anything else (`EPERM`, and whatever a locked-down host
|
|
121
|
+
// answers) → alive, and the staleness backstop decides.
|
|
122
|
+
return !(e && e.code === 'ESRCH');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* `_normalize_entry` — one session snapshot into the stable shape the UI consumes. v1
|
|
128
|
+
* writers omit the enrichment keys, so those default to null/0 and the UI hides what is
|
|
129
|
+
* absent. The KEY ORDER is the reference's dict-literal order, which is what the byte
|
|
130
|
+
* comparison over every response gates.
|
|
131
|
+
*/
|
|
132
|
+
function normalizeEntry(entry, stem) {
|
|
133
|
+
const g = (k) => (Object.hasOwn(entry, k) ? entry[k] : null);
|
|
134
|
+
const or = (k, dflt) => (pyTruthy(g(k)) ? g(k) : dflt);
|
|
135
|
+
return {
|
|
136
|
+
session_id: or('session_id', stem),
|
|
137
|
+
agent: g('agent'),
|
|
138
|
+
title: g('title'),
|
|
139
|
+
cwd: g('cwd'),
|
|
140
|
+
status: or('status', 'idle'),
|
|
141
|
+
updated_at: or('updated_at', 0),
|
|
142
|
+
model: g('model'),
|
|
143
|
+
phase: g('phase'),
|
|
144
|
+
turn_started_at: g('turn_started_at'),
|
|
145
|
+
cost: or('cost', 0),
|
|
146
|
+
tokens: or('tokens', 0),
|
|
147
|
+
files: g('files'),
|
|
148
|
+
todos: g('todos'),
|
|
149
|
+
blocked_on: g('blocked_on'),
|
|
150
|
+
error: g('error'),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** `_read_entry` — null on a missing, garbage or non-dict file. Never throws. */
|
|
155
|
+
function readEntry(p) {
|
|
156
|
+
let entry;
|
|
157
|
+
try {
|
|
158
|
+
entry = parseJson(readText(p));
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return (entry !== null && typeof entry === 'object' && !Array.isArray(entry)) ? entry : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** `_is_live` — the writer's pid is alive AND the snapshot is not stale. */
|
|
166
|
+
function isLive(entry, now) {
|
|
167
|
+
const updated = pyTruthy(Object.hasOwn(entry, 'updated_at') ? entry.updated_at : null)
|
|
168
|
+
? entry.updated_at : 0;
|
|
169
|
+
return (now - Number(updated)) <= ACTIVITY_STALE_SECONDS && pidAlive(
|
|
170
|
+
Object.hasOwn(entry, 'pid') ? entry.pid : null);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** `Path.stem` — the name without its LAST suffix, so `s-live.detail.json` keeps the dot. */
|
|
174
|
+
const stemOf = (name) => name.slice(0, name.length - path.extname(name).length);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* `_activity_entries` — every live session, dead and stale ones pruned AND their files
|
|
178
|
+
* removed. Never throws: a missing dir is `[]`, a garbage file is skipped (and kept —
|
|
179
|
+
* only a snapshot that PARSED and failed the liveness test is unlinked), newest first.
|
|
180
|
+
*/
|
|
181
|
+
export function activityEntries(state) {
|
|
182
|
+
const d = activityDir(state);
|
|
183
|
+
if (!isDir(d)) return [];
|
|
184
|
+
const now = nowSeconds();
|
|
185
|
+
const out = [];
|
|
186
|
+
let names;
|
|
187
|
+
try {
|
|
188
|
+
names = readdirSync(d);
|
|
189
|
+
} catch {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
// `Path.glob("*.json")` is case-INSENSITIVE on Windows, as pathlib's is; `str.endswith`
|
|
193
|
+
// on the detail suffix is not, on either platform. Two different rules, deliberately.
|
|
194
|
+
for (const name of names.filter((n) => normcase(n).endsWith('.json')).sort()) {
|
|
195
|
+
if (name.endsWith('.detail.json')) continue;
|
|
196
|
+
const p = path.join(d, name);
|
|
197
|
+
const entry = readEntry(p);
|
|
198
|
+
if (entry === null) continue;
|
|
199
|
+
if (!isLive(entry, now)) {
|
|
200
|
+
try {
|
|
201
|
+
unlinkSync(p); // crashed / abandoned writer
|
|
202
|
+
unlinkSync(path.join(d, `${stemOf(name)}.detail.json`)); // its detail file too
|
|
203
|
+
} catch { /* `contextlib.suppress(OSError)` wraps BOTH unlinks, as this does */ }
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
out.push(normalizeEntry(entry, stemOf(name)));
|
|
207
|
+
}
|
|
208
|
+
// Stable in both languages, and the tie is the point: `sorted(reverse=True)` preserves the
|
|
209
|
+
// original order of equal keys, and so does a `b - a` comparator over ES2019's stable sort.
|
|
210
|
+
out.sort((a, b) => Number(b.updated_at) - Number(a.updated_at));
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** `_web_activity.api_activity` (GET). */
|
|
215
|
+
export function apiActivity(state) {
|
|
216
|
+
const enabled = activityEnabled(state);
|
|
217
|
+
return { enabled, activity: enabled ? activityEntries(state) : [] };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* `_web_activity.api_activity_detail` — one session's snapshot plus its step timeline and
|
|
222
|
+
* its UNCAPPED files/todos. 404 if the session is absent or has aged out; a missing or
|
|
223
|
+
* garbage detail file degrades to an empty timeline rather than a 500.
|
|
224
|
+
*/
|
|
225
|
+
export function apiActivityDetail(state, sid) {
|
|
226
|
+
const d = activityDir(state);
|
|
227
|
+
// Resolve by the writer's safe-name scheme, which is also what keeps the read inside the
|
|
228
|
+
// directory: every separator, `..` and drive colon becomes `_`, so there is no join to
|
|
229
|
+
// steer. The `u` flag makes an astral character ONE replacement, as Python's code-point
|
|
230
|
+
// `re.sub` does, rather than one per surrogate.
|
|
231
|
+
const stem = sid.replace(/[^A-Za-z0-9_.-]/gu, '_');
|
|
232
|
+
const snap = path.join(d, `${stem}.json`);
|
|
233
|
+
const entry = isFile(snap) ? readEntry(snap) : null;
|
|
234
|
+
if (entry === null || !isLive(entry, nowSeconds())) throw new NotFound(sid);
|
|
235
|
+
const session = normalizeEntry(entry, stem);
|
|
236
|
+
let timeline = [];
|
|
237
|
+
let full = {};
|
|
238
|
+
const det = readEntry(path.join(d, `${stem}.detail.json`));
|
|
239
|
+
if (pyTruthy(det)) {
|
|
240
|
+
const tl = Object.hasOwn(det, 'timeline') ? det.timeline : null;
|
|
241
|
+
timeline = Array.isArray(tl) ? tl : [];
|
|
242
|
+
full = det;
|
|
243
|
+
}
|
|
244
|
+
const fg = (k) => (Object.hasOwn(full, k) ? full[k] : null);
|
|
245
|
+
// The detail file carries the uncapped lists; fall back to the snapshot's capped ones.
|
|
246
|
+
// Assigning over an existing key keeps its POSITION in both languages, which is what lets
|
|
247
|
+
// the response stay byte-comparable.
|
|
248
|
+
session.files = pyTruthy(fg('files')) ? fg('files') : session.files;
|
|
249
|
+
session.todos = pyTruthy(fg('todos')) ? fg('todos') : session.todos;
|
|
250
|
+
// The compact transcript: an ordered list of {role, text, t} turns. Empty falls back to
|
|
251
|
+
// the title as the opening user turn, because OpenCode derives the title from the first
|
|
252
|
+
// prompt.
|
|
253
|
+
const conv = fg('conversation');
|
|
254
|
+
let conversation = Array.isArray(conv) ? conv : [];
|
|
255
|
+
if (conversation.length === 0 && pyTruthy(session.title)) {
|
|
256
|
+
conversation = [{ role: 'user', text: session.title }];
|
|
257
|
+
}
|
|
258
|
+
return { session, timeline, conversation };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* `_web_activity.api_activity_toggle` — flip the runtime on/off flag.
|
|
263
|
+
*
|
|
264
|
+
* The OpenCode plugin reads this file on every event, so the change takes effect without
|
|
265
|
+
* restarting anything; writing `off` also makes the plugin clear its snapshots on its next
|
|
266
|
+
* event, while the reader gates the surface immediately.
|
|
267
|
+
*
|
|
268
|
+
* `bool(body.get("enabled", True))` DEFAULTS TO ON — an empty body enables, which is the
|
|
269
|
+
* arm a port spelled `body.enabled === true` inverts. And this is the one POST in P6f whose
|
|
270
|
+
* result is sent at 200 whatever `ok` says: the 409 convention is per-route in the
|
|
271
|
+
* reference, not a rule about the shape of the body.
|
|
272
|
+
*/
|
|
273
|
+
export function apiActivityToggle(state, body) {
|
|
274
|
+
const raw = (body && Object.hasOwn(body, 'enabled')) ? body.enabled : true;
|
|
275
|
+
const enabled = pyTruthy(raw);
|
|
276
|
+
const p = activityFlag(state);
|
|
277
|
+
try {
|
|
278
|
+
mkdirSync(path.dirname(p), { recursive: true });
|
|
279
|
+
writeText(p, enabled ? 'on' : 'off');
|
|
280
|
+
} catch (e) {
|
|
281
|
+
return { ok: false, error: String(e && e.message ? e.message : e) };
|
|
282
|
+
}
|
|
283
|
+
return { ok: true, enabled };
|
|
284
|
+
}
|