@staix/agent-hub 0.12.23 → 0.12.25
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 +109 -1
- package/README.md +8 -3
- package/docs/addons/claude-mem.md +24 -0
- package/docs/addons/codex-plugin.md +8 -0
- package/docs/addons/dot-admit.md +8 -0
- package/docs/addons/open-code-review.md +8 -0
- package/docs/addons/ponytail.md +23 -0
- package/docs/addons/ripwire.md +22 -0
- package/docs/agent-notes/README.md +5 -0
- package/docs/agent-notes/adapters.md +10 -1
- package/docs/agent-notes/bus.md +3 -1
- package/docs/agent-notes/daemon.md +7 -1
- package/docs/agent-notes/local-worker.md +4 -2
- package/docs/agent-notes/tasks.md +8 -0
- package/docs/agent-notes/tests.md +5 -1
- package/docs/bench.md +28 -3
- package/docs/cooperbench.md +4 -0
- package/docs/data-policy.md +36 -0
- package/docs/events.md +3 -0
- package/docs/gsd-collaboration-analysis.md +48 -0
- package/docs/memory.md +231 -0
- package/docs/operations.md +666 -19
- package/docs/plans/2026-10-11-issues-109-sequential.md +81 -0
- package/docs/plans/2026-10-11-routing-evaluation-proposal.md +30 -0
- package/docs/profiles.md +57 -0
- package/docs/quickstart.md +2 -2
- package/docs/research.md +31 -1
- package/docs/roles.md +85 -0
- package/docs/sdlc.md +170 -0
- package/docs/security.md +111 -4
- package/docs/smoke.md +288 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +633 -7
- package/docs/specs/2026-10-12-backlog-implementation-plan.md +186 -0
- package/docs/specs/2026-10-12-office-view-design.md +212 -0
- package/docs/text-projection.md +19 -0
- package/docs/verification/2026-10-11-acp-watch-t0.md +33 -0
- package/docs/verification/2026-10-11-agent-config-inventory.md +20 -0
- package/docs/verification/2026-10-11-jcode-peer-contract.md +27 -0
- package/docs/verification/2026-10-13-0.12.24.md +94 -0
- package/docs/verification/2026-10-13-board-phone.md +31 -0
- package/docs/verification/2026-10-13-fsmonitor-fixture-attribution.md +19 -0
- package/docs/verification/2026-10-14-0.12.24-audit.md +123 -0
- package/docs/verification/2026-10-14-0.12.25.md +27 -0
- package/docs/verified.json +22 -13
- package/package.json +2 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/ROLE-NOTICE.md +26 -0
- package/plugins/agent-hub/server.js +1175 -339
- package/scripts/check-docs.mjs +129 -0
- package/src/adapters/acp.ts +112 -13
- package/src/adapters/claude-channel.ts +48 -11
- package/src/adapters/codex-appserver.ts +11 -0
- package/src/adapters/jcode.ts +110 -0
- package/src/adapters/local-worker.ts +10 -7
- package/src/adapters/pi.ts +171 -23
- package/src/cli/attention.ts +60 -0
- package/src/cli/bench.ts +24 -5
- package/src/cli/board-read.ts +12 -0
- package/src/cli/console-state.ts +103 -17
- package/src/cli/console.ts +54 -8
- package/src/cli/facts-hook.ts +12 -4
- package/src/cli/help.ts +35 -9
- package/src/cli/identity.ts +18 -7
- package/src/cli/init.ts +48 -10
- package/src/cli/jcode-mcp.ts +58 -0
- package/src/cli/kimi-mcp.ts +53 -0
- package/src/cli/launch.ts +16 -1
- package/src/cli/main.ts +392 -57
- package/src/cli/memory.ts +193 -0
- package/src/cli/output.ts +45 -4
- package/src/cli/pet-config.ts +1 -0
- package/src/cli/pet.ts +83 -0
- package/src/cli/preview.ts +38 -5
- package/src/cli/status-lines.ts +1 -1
- package/src/cli/tail-render.ts +1 -0
- package/src/cli/upgrade-runtime.ts +32 -3
- package/src/cli/watch.ts +31 -0
- package/src/hub/activity.ts +53 -0
- package/src/hub/ask.ts +10 -5
- package/src/hub/bench.ts +10 -3
- package/src/hub/board.ts +222 -16
- package/src/hub/bus.ts +48 -9
- package/src/hub/citations.ts +7 -0
- package/src/hub/conductor.ts +12 -7
- package/src/hub/config-trust.ts +4 -2
- package/src/hub/crash.ts +9 -0
- package/src/hub/daemon.ts +914 -120
- package/src/hub/delivery-journal.ts +5 -0
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +12 -2
- package/src/hub/facts.ts +26 -7
- package/src/hub/git.ts +8 -2
- package/src/hub/github-import.ts +28 -0
- package/src/hub/hub-tools.ts +49 -10
- package/src/hub/inference.ts +1 -1
- package/src/hub/lifecycle-schema.ts +17 -0
- package/src/hub/listen.ts +15 -0
- package/src/hub/manager.ts +12 -5
- package/src/hub/path-policy.ts +115 -0
- package/src/hub/peer-catalog.ts +28 -0
- package/src/hub/permission-mode.ts +19 -5
- package/src/hub/planning.ts +70 -0
- package/src/hub/profiles.ts +74 -0
- package/src/hub/projected-diff.ts +73 -0
- package/src/hub/projection-exec.ts +24 -0
- package/src/hub/research.ts +144 -21
- package/src/hub/roles.ts +123 -0
- package/src/hub/roster.ts +23 -0
- package/src/hub/routing.ts +18 -5
- package/src/hub/sdlc-queue.ts +112 -0
- package/src/hub/sdlc-release.ts +125 -0
- package/src/hub/sdlc.ts +250 -0
- package/src/hub/settings.ts +34 -15
- package/src/hub/shipped-roles.ts +25 -0
- package/src/hub/snapshots.ts +89 -15
- package/src/hub/start-mode.ts +7 -5
- package/src/hub/supervision.ts +1 -0
- package/src/hub/task-sweep.ts +1 -1
- package/src/hub/tasks.ts +905 -89
- package/src/hub/text-projection.ts +151 -0
- package/src/hub/tooling.ts +170 -0
- package/src/hub/ui.ts +16 -2
- package/src/hub/workflow.ts +71 -0
- package/src/local/sandbox.ts +6 -4
- package/src/local/tools.ts +11 -6
- package/src/memory/brief.ts +28 -14
- package/src/memory/import.ts +55 -0
- package/src/memory/recall.ts +84 -0
- package/src/memory/store.ts +757 -0
- package/src/memory/sync.ts +145 -0
- package/src/pet/NOTICE.md +10 -0
- package/src/pet/characters.ts +1438 -0
- package/src/pet/config.ts +31 -0
- package/src/pet/frame.ts +17 -0
- package/src/pet/pixels.ts +28 -0
- package/src/pi/extension.ts +40 -5
- package/src/role-text.d.ts +1 -0
- package/src/ui/index.html +843 -46
- package/src/ui/needs.ts +58 -0
- package/src/ui/office-model.ts +440 -0
- package/src/ui/peer-palette.ts +16 -0
- package/src/ui/task-board.ts +49 -0
- package/src/ui/task-progress.ts +5 -1
- package/templates/AGENTS.block.md +3 -2
- package/templates/config.json +5 -1
- package/templates/profiles/dev.json +22 -0
- package/templates/profiles/general.json +26 -0
- package/templates/profiles/research.json +57 -0
- package/templates/profiles/writing.json +28 -0
- package/templates/roles/NOTICE.md +26 -0
- package/templates/roles/dev/backend-engineer.md +26 -0
- package/templates/roles/dev/code-reviewer.md +26 -0
- package/templates/roles/dev/frontend-engineer.md +26 -0
- package/templates/roles/dev/security-reviewer.md +26 -0
- package/templates/roles/dev/software-architect.md +26 -0
- package/templates/roles/dev/test-engineer.md +26 -0
- package/templates/roles/research/analyst.md +26 -0
- package/templates/roles/research/fact-checker.md +26 -0
- package/templates/roles/research/research-lead.md +26 -0
- package/templates/roles/research/researcher.md +26 -0
- package/templates/roles/research/statistician.md +26 -0
package/src/cli/help.ts
CHANGED
|
@@ -10,13 +10,17 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
10
10
|
["ahub projects [--json] [--color=auto|always|never]", "list registered projects and live status"],
|
|
11
11
|
["ahub projects remove <id>", "forget a stopped registration (keeps project files)"],
|
|
12
12
|
["ahub setup [--yes]", "install or update the Claude Code channel plugin from this package, then run doctor"],
|
|
13
|
-
["ahub init [--dry-run --json]", "preview or write .agenthub/config.json and the AGENTS.md marker block (drops a legacy CLAUDE.md block)"],
|
|
13
|
+
["ahub init [--profile dev|research|writing|general] [--shadow] [--dry-run --json]", "preview or write .agenthub/config.json and the AGENTS.md marker block (drops a legacy CLAUDE.md block)"],
|
|
14
14
|
["ahub help [command]", "this help, or one command's entries; also --help and -h"],
|
|
15
15
|
["ahub version | --version", "print the installed version"],
|
|
16
16
|
]],
|
|
17
17
|
["Daemon and runtime", [
|
|
18
18
|
["ahub up [--unattended] [--no-console]", "start the daemon; interactive terminals enter console"],
|
|
19
|
-
["ahub console [--panels] [--color=auto|always|never]", "enter the human console
|
|
19
|
+
["ahub console [--panels] [--color=auto|always|never]", "enter the human console; P cycles built-in pet characters/off; exiting leaves the daemon running"],
|
|
20
|
+
["ahub console --no-pet", "turn off attention for this session; combines with --panels and --color"],
|
|
21
|
+
["ahub pet [--short]", "read attention without connecting; --short prints ahub!N only when action is needed"],
|
|
22
|
+
["ahub sdlc status [task] | explain <task> | export <task> | record <task> <receipt-json>", "read head-bound lifecycle gates and the next actor/action (person only)"],
|
|
23
|
+
["ahub sdlc queue <owner/repo> | release <owner/repo> [live-issue...]", "read-only merge and release evidence (person only)"],
|
|
20
24
|
["ahub status [--json] [--full] [--color=auto|always|never]", "the hub, its peers, model backends and task counts"],
|
|
21
25
|
["ahub status --all [--json] [--color=auto|always|never]", "show every registered project"],
|
|
22
26
|
["ahub logs [-f]", "the last 100 lines of hub.log; -f shows the last 10 and follows it"],
|
|
@@ -35,30 +39,44 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
35
39
|
["Agents", [
|
|
36
40
|
["ahub claude [--print-command] [--unattended] [args...]", "launch Claude Code with the hub channel"],
|
|
37
41
|
["ahub codex [--print-command] [--unattended] [args...]", "start the Codex adapter and attach the TUI"],
|
|
38
|
-
["ahub kimi [--print-command] [--model <alias>]", "
|
|
42
|
+
["ahub kimi [--print-command] [--mode tui|headless] [--model <alias>]", "default: Kimi's own TUI, pull-only via hub_inbox; no push, steer, watchdog, console approvals or automatic quota handoff. Select headless for ACP"],
|
|
43
|
+
["ahub jcode [--mode headless] [--model <alias>] [--provider-profile <name>] [--session-id <id>] [--print-command]", "optional owned Jcode daemon/ACP; native policy unmanaged, no hub sandbox"],
|
|
44
|
+
["ahub qwen [--mode headless] [--model <alias>] [--session-id <id>] [--print-command]", "start optional Qwen Code 0.25.0 ACP (roster.qwen, default off)"],
|
|
39
45
|
["ahub pi [--print-command] [--mode headless|tui | --headless] [--backend auto|dgx|mlx] [--session-id <id>] [--session-file <path>]", "start Pi: its TUI in this terminal unless --headless or the start mode setting (peers.pi.start_mode) says headless; without a terminal the hub opens one or says how"],
|
|
40
46
|
["ahub local [--route <id> | --model <id>]", "start the hub-native worker on the self-hosted models (routing.toml)"],
|
|
47
|
+
["ahub role list [--group <group>] | show <id>", "read bounded catalog guidance with layer and content hash"],
|
|
48
|
+
["ahub role assign <peer> <id> | clear <peer> [<id>]", "person only: add or clear catalog guidance, applied at the next peer start"],
|
|
41
49
|
["ahub stop <peer>", "human only: stop a hub-owned headless peer; end TUI agents in their terminals"],
|
|
42
50
|
["ahub models status [--json] [--color=auto|always|never]", "inspect local model state as labelled fields"],
|
|
43
51
|
["ahub models setup|start|stop", "prepare or inspect local Ollama MLX (legacy stop is explicit)"],
|
|
44
52
|
]],
|
|
45
53
|
["Messages and approvals", [
|
|
46
54
|
["ahub say [@peer ...] <text>", "send as the console user (no @peer = broadcast); delivered at once, start the text with [STATUS] to let it batch or [FYI] for the record only"],
|
|
55
|
+
["ahub watch kimi|qwen [--no-thoughts] [--color=auto|always|never]", "person-only transient activity from a headless ACP peer; q quits, approvals stay in console"],
|
|
47
56
|
["ahub tail", "live stream of messages, states and permission requests"],
|
|
48
57
|
["ahub permission [<peer> [ask|ask-when-needed|never-ask]] [--yes]", "human only: inspect or set a running peer mode; never-ask requires --yes; config never-ask waits for console y"],
|
|
49
58
|
["ahub permit <id> <option>", "answer a permission request shown by tail (\"deny\" cancels)"],
|
|
59
|
+
["ahub roster [--json] [--color=auto|always|never]", "human only: every known peer, participation, start mode and retained work"],
|
|
60
|
+
["ahub roster add <peer> | remove <peer> [--yes] [--now]", "human only: change live participation through settings; removal previews retained messages and task handoff"],
|
|
50
61
|
["ahub settings [list] [--json] | get <key>", "human only: the settings the dashboard can change, each with its value, the file it comes from and when it applies"],
|
|
51
|
-
["ahub settings set <key> <value|inherit> [--yes] [--preview] | undo", "human only: change one, written to .agenthub/config.local.json or routing.local.toml (never a tracked file); --preview shows the effect and saves nothing; undo puts back the last write"],
|
|
62
|
+
["ahub settings set <key> <value|inherit> [--yes] [--preview] [--now] | undo", "human only: change one, written to .agenthub/config.local.json or routing.local.toml (never a tracked file); --preview shows the effect and saves nothing; undo puts back the last write"],
|
|
52
63
|
["ahub pause|resume <peer>", "hold a peer's deliveries in its queue / release them"],
|
|
53
64
|
["ahub queue list [--peer <id>] [--json] [--color=auto|always|never]", "inspect durable deliveries"],
|
|
54
65
|
["ahub queue show <delivery-id> [--json] [--color=auto|always|never]", "inspect one delivery and revision"],
|
|
55
66
|
["ahub queue resolve <delivery-id> --action completed|retry|discard --reason <text>", "release a delivery held as needs_review: mark it completed, retry it or discard it"],
|
|
56
67
|
]],
|
|
57
68
|
["Tasks and review", [
|
|
58
|
-
["ahub board
|
|
59
|
-
["ahub
|
|
69
|
+
["ahub board import github --label <label> [--milestone <milestone>] [--repo <owner/repo>]", "person only: read bounded open issues into deduplicated Backlog; never writes GitHub"],
|
|
70
|
+
["ahub board [state | --ready] [--columns] [--release <name> | --release-id <id>] [--json] [--color=auto|always|never]", "the task board; --ready: proposed tasks with nothing left to wait for"],
|
|
71
|
+
["ahub task propose [--class <c> | <class>] <title...> [--owner <peer>] [--path <p>]... [--after <id>]... [--urgent] [--backlog] [--detail <text>]", "put a task on the board; a first word that names a class is taken as the class; while a task named by --after is not approved yet, --owner reserves the task for that peer, offered to it first once it is ready; otherwise --owner assigns it now"],
|
|
72
|
+
["ahub backlog list | add <class> <title...> | plan <id> <revision> <fields-json> <reason...> | pin <id> <revision> true|false <reason...> | undo <id> <revision>", "person only: stage and plan work, preserve pins/frozen scope and refuse stale edits"],
|
|
73
|
+
["ahub release new <name> <goal> <order> | list | show <name> | plan <name> <revision> <dates-json> | activate|freeze|ship|drop <name> <revision>", "person only: planned UTC millisecond dates and phased scope; ship records a time after approval, never tags or publishes"],
|
|
74
|
+
["ahub backlog split <id> <revision> <parts-json> <reason...>", "person only: stage2..8 child prerequisites; original task retains integration, undo refuses used or changed children"],
|
|
75
|
+
["ahub backlog pull-ready <id> <revision> <reason...>", "person only: reserve an eligible WIP slot before routing; pending unowned work can be undone"],
|
|
76
|
+
["ahub backlog pull|return <id>", "person only: pull backlog into To Do or return ownerless proposed work; --backlog on task propose stages unoffered work"],
|
|
60
77
|
["ahub task show|escalate <id>", "full task with history (PII text included) / hand it to the next peer in escalate_to"],
|
|
61
|
-
["ahub task
|
|
78
|
+
["ahub task role <id> <role|none>", "person only: change bounded catalog guidance on an open task; routing and authority are unchanged"],
|
|
79
|
+
["ahub task assign <id> <peer> [--role owner|reviewer]", "name the owner (default) or reviewer; a task that still waits reserves its owner"],
|
|
62
80
|
["ahub task label <id> ok|regressed|reverted|incomplete|wrong|abandoned", "your later verdict on an approved task, kept in the research records (research must be on)"],
|
|
63
81
|
["ahub review <id> approved|changes_requested [note...] [--unmet <item>]...", "give a review verdict on a task"],
|
|
64
82
|
["ahub route explain <id>", "why a task went where it went, its reserved owner included"],
|
|
@@ -79,21 +97,29 @@ const SECTIONS: [string, [usage: string, description: string][]][] = [
|
|
|
79
97
|
["ahub report [--since 7d|<iso>] [--by task] [--json] [--color=auto|always|never]", "turns, tokens, messages, overlaps and task events per period"],
|
|
80
98
|
["ahub export [--since 7d|<iso>]", "structured events (events.jsonl) as JSON lines; never message bodies"],
|
|
81
99
|
["ahub research [--since 30d] [--all] [--json]", "success, first-pass, rework and check-failure rates, tokens and wall time per approved task, from the opt-in research records"],
|
|
100
|
+
["ahub research addons [--since 30d] [--all] [--json]", "approved tasks with each add-on (claude-mem, ponytail, ripwire, ...) on against off, with intervals; association, not cause"],
|
|
82
101
|
["ahub research export [--format jsonl|csv] [--since 30d] [--all]", "the research records for outside analysis: ids, counts and tokens, never text"],
|
|
83
102
|
["ahub research backfill", "build this project's research records once from events.jsonl"],
|
|
84
|
-
["ahub bench run <suite.json> --arm <label> [--repeat N] [--tasks a,b]", "run a benchmark suite against the peers attached here, in a project kept for benchmarks (it resets the work tree)"],
|
|
103
|
+
["ahub bench run <suite.json> --arm <label> [--repeat N] [--tasks a,b] [--addon <name>=on|off]... [--memory off|claude-mem|native|both]", "run a benchmark suite against the peers attached here, in a project kept for benchmarks (it resets the work tree); --addon and --memory declare the arm's add-on and memory state and refuse a setup that differs"],
|
|
85
104
|
["ahub bench list|status", "benchmark runs on this machine / the one running now"],
|
|
86
105
|
["ahub bench report <run> [--json]", "pass rate, first pass, rework, tokens and wall time per task and overall"],
|
|
87
106
|
["ahub bench compare <run|arm>... [--suite <name>] [--mixed] [--json]", "compare arms of one suite with 95% bootstrap intervals; inconclusive below 5 counted attempts per arm"],
|
|
88
107
|
["ahub bench export [--format jsonl|csv]", "benchmark records for outside analysis: ids, outcomes and counts, never suite text"],
|
|
89
108
|
["ahub check-path <file> [--peer <id>]", "other owners' open tasks that claim or changed a file"],
|
|
109
|
+
["ahub memory [list] [--tier short|long] [--kind <kind>] [--peer <p>] [--machine <id>] [--since <iso>] [--limit N] [--json]", "the hub's own memory for this project: decisions, contracts, fails and findings, short- and long-term (docs/memory.md)"],
|
|
110
|
+
["ahub memory search <words> | show <id> | graph <id|node> [--hops 1|2] [--json]", "find entries, read one with its provenance, or list its graph neighbourhood"],
|
|
111
|
+
["ahub memory promote|demote|forget <id> | supersede <old> <new> | rebuild", "a person's changes to memory: move an entry between tiers, forget it, mark it superseded, or rebuild the index from the logs"],
|
|
112
|
+
["ahub memory sync [--remote <private git url>] | machine [--label <name>]", "sync hub memory with your other machines through a private git remote (a person's), or show or name this machine"],
|
|
113
|
+
["ahub memory import claude-mem [--db <path>] [--limit N] | suggestions | confirm|reject <id>", "copy the hub's notes from claude-mem into hub memory (a person's); review the model's suggested edges"],
|
|
114
|
+
["ahub goal [list] [--all] | show <id> [--json]", "long-term tasks: state, which machine holds each and until when, linked board tasks"],
|
|
115
|
+
["ahub goal new <title> [--milestone <text>]... [--depends <id>]... [--shared] | link <id> <task> | state <id> <state> | done <id> | take <id> [--force]", "a person's changes to long-term tasks; take --force voids other machines' claims"],
|
|
90
116
|
]],
|
|
91
117
|
["Hooks", [
|
|
92
118
|
["ahub check-path --hook", "check-path as a Claude Code PreToolUse hook (templates/claude-hooks.json); never blocks"],
|
|
93
119
|
["ahub facts --hook", "turn-free facts as a Claude Code PreToolUse, PostToolUse and Stop hook (issue #108); never blocks"],
|
|
94
120
|
]],
|
|
95
121
|
["Dashboard", [
|
|
96
|
-
["ahub ui [--no-open]", "open the local dashboard (or print its one-time link)"],
|
|
122
|
+
["ahub ui [--no-open]", "open the local dashboard with configured attention character (or print its one-time link)"],
|
|
97
123
|
["ahub ui --settings [--no-open]", "human only: open it with a settings session, which for 15 minutes may change permission modes, routing and start settings"],
|
|
98
124
|
["ahub ui --all [--no-open]", "open the unified project dashboard"],
|
|
99
125
|
["ahub ui --all --stop", "stop only the dashboard manager"],
|
package/src/cli/identity.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isSupportedPeer } from "../hub/peer-catalog.ts";
|
|
2
|
+
|
|
1
3
|
export type CliIdentity =
|
|
2
4
|
| { role: "console" }
|
|
3
5
|
| { role: "tools"; peer: string }
|
|
@@ -26,8 +28,9 @@ export type PeerCommandAccess = "allowed" | "conductor" | "console";
|
|
|
26
28
|
|
|
27
29
|
/** A closed list: unknown commands cannot acquire console authority through an agent shell. */
|
|
28
30
|
export function classifyPeerCommand(command: string, args: readonly string[]): PeerCommandAccess {
|
|
29
|
-
if (command === "permission" || command === "stop") return "console";
|
|
31
|
+
if (command === "permission" || command === "stop" || command === "watch") return "console";
|
|
30
32
|
if (args.some((arg) => arg === "--as-user" || arg.startsWith("--as-user="))) return "console";
|
|
33
|
+
if (command === "role") return ["list", "show"].includes(args[0] ?? "") ? "allowed" : "console";
|
|
31
34
|
if (command === "task") {
|
|
32
35
|
if (["propose", "show"].includes(args[0] ?? "")) return "allowed";
|
|
33
36
|
if (["assign", "escalate"].includes(args[0] ?? "")) return "conductor";
|
|
@@ -36,21 +39,29 @@ export function classifyPeerCommand(command: string, args: readonly string[]): P
|
|
|
36
39
|
if (command === "route") return args[0] === "explain" ? "allowed" : "console";
|
|
37
40
|
// #247: reading the research measures and records is harmless (ids and counts only); writing them is a person's.
|
|
38
41
|
if (command === "research") return args[0] === "backfill" ? "console" : "allowed";
|
|
42
|
+
// #258: reading hub memory is harmless; promote, demote, forget, supersede and rebuild are a person's.
|
|
43
|
+
if (command === "memory") { const sub = args.find((arg) => arg !== "--json"); return sub === undefined || sub.startsWith("--") || ["list", "search", "show", "suggestions"].includes(sub) || (sub === "machine" && !args.includes("--label")) ? "allowed" : "console"; } // graph labels are unscreened note text: a person's
|
|
44
|
+
if (command === "goal") { const sub = args.find((arg) => arg !== "--json"); return sub === undefined || sub.startsWith("--") || ["list", "show"].includes(sub) ? "allowed" : "console"; }
|
|
39
45
|
// #251: a run resets the bench project's tree and proposes work; reading results is harmless.
|
|
40
46
|
// The subcommand is the first argument that is not --json, which the command accepts anywhere.
|
|
41
47
|
if (command === "bench") return args.find((arg) => arg !== "--json") === "run" ? "console" : "allowed";
|
|
42
48
|
if (command === "budget") return !args.length || args.every((arg) => arg === "--json" || arg === "--full" || arg.startsWith("--color=")) ? "allowed" : "console";
|
|
43
|
-
if (
|
|
44
|
-
if (
|
|
49
|
+
if (command === "jcode") return "console"; // the person launcher alone writes its project MCP entry
|
|
50
|
+
if (isSupportedPeer(command) || ["pause", "resume"].includes(command)) return "conductor";
|
|
51
|
+
if (command === "board" && args[0] === "import") return "console";
|
|
52
|
+
if (["help", "version", "--version", "say", "status", "pet", "board", "report", "turns", "review", "remember", "facts", "check-path"].includes(command)) return "allowed";
|
|
45
53
|
return "console";
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
/** No argument text is included: notices and refusal logs must not copy task text or credentials. */
|
|
49
57
|
export function cliCommandLabel(command: string, args: readonly string[]): string {
|
|
50
|
-
if (
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
if (command === "backlog") return ["list", "add", "pull", "pull-ready", "split", "return", "plan", "pin", "undo"].includes(args[0] ?? "") ? `backlog ${args[0]}` : "backlog";
|
|
59
|
+
if (command === "board" && args[0] === "import") return "board import";
|
|
60
|
+
if (command === "release") return ["new", "list", "show", "plan", "activate", "freeze", "ship", "drop"].includes(args[0] ?? "") ? `release ${args[0]}` : "release";
|
|
61
|
+
if (!["upgrade", "restart", "recovery", "recovery-run", "help", "--help", "--version", "version", "projects", "ui", "manager", "setup", "init", "daemon", "up", "console", "claude", "codex", "kimi", "qwen", "jcode", "pi", "local", "stop", "models", "say", "queue", "tail", "watch", "budget", "board", "task", "review", "remember", "ask", "route", "pause", "resume", "permit", "permission", "settings", "roster", "status", "pet", "logs", "export", "report", "research", "memory", "goal", "bench", "sdlc", "role", "facts", "check-path", "turns", "undo", "kill", "reset", "doctor"].includes(command)) return "unknown";
|
|
62
|
+
if (["task", "queue", "budget", "route", "recovery", "models", "projects", "research", "memory", "goal", "bench"].includes(command)) {
|
|
63
|
+
const sub = command === "bench" || command === "memory" || command === "goal" ? args.find((arg) => arg !== "--json") : args[0];
|
|
64
|
+
if (sub && ["propose", "show", "assign", "escalate", "label", "list", "resolve", "resume", "set", "execution", "explain", "status", "abort", "dispose", "setup", "start", "stop", "remove", "export", "backfill", "addons", "run", "report", "compare", "search", "graph", "promote", "demote", "forget", "supersede", "rebuild", "sync", "machine", "import", "suggestions", "confirm", "reject", "new", "link", "state", "done", "take"].includes(sub)) return `${command} ${sub}`;
|
|
54
65
|
}
|
|
55
66
|
return (/^[a-z][a-z0-9-]*$/.test(command) && !/\s/.test(command)) || ["--help", "--version"].includes(command) ? command : "unknown";
|
|
56
67
|
}
|
package/src/cli/init.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { initShadow, repoOf } from "../hub/snapshots.ts";
|
|
2
|
+
import { profileName, projectProfile, resolveProfile, mergeProfileOverrides, type ProfileName } from "../hub/profiles.ts";
|
|
3
|
+
import { JCODE_MCP_FILE } from "./jcode-mcp.ts";
|
|
1
4
|
import { existsSync, lstatSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
5
|
import { join } from "node:path";
|
|
6
|
+
import { hubGitSync } from "../hub/git.ts";
|
|
7
|
+
import { KIMI_MCP_FILE } from "./kimi-mcp.ts";
|
|
3
8
|
|
|
4
9
|
const TEMPLATES = join(import.meta.dir, "..", "..", "templates");
|
|
5
10
|
const BEGIN = "<!-- AGENT_HUB:BEGIN (managed by `ahub init`, edits inside are overwritten) -->";
|
|
@@ -30,21 +35,51 @@ export interface InitChange {
|
|
|
30
35
|
managedBlock?: "insert" | "replace" | "remove";
|
|
31
36
|
}
|
|
32
37
|
/** Content is internal to apply, never included in a public preview. */
|
|
33
|
-
interface PlannedWrite extends InitChange { next?: string; }
|
|
34
|
-
function changes(cwd: string): PlannedWrite[] {
|
|
38
|
+
interface PlannedWrite extends InitChange { next?: string; shadow?: true; }
|
|
39
|
+
function changes(cwd: string, requested?: ProfileName, shadow = false): PlannedWrite[] {
|
|
40
|
+
const repository = shadow ? repoOf(cwd) : undefined;
|
|
41
|
+
if (shadow && repository && !repository.shadow) throw new Error("project already has a Git work tree; shadow mode is for non-git folders");
|
|
42
|
+
if (shadow) {
|
|
43
|
+
const parent = lstatSync(join(cwd, ".agenthub"), { throwIfNoEntry: false });
|
|
44
|
+
if (parent && (!parent.isDirectory() || parent.isSymbolicLink())) throw new Error(".agenthub must be a project directory, not a link");
|
|
45
|
+
const own = lstatSync(join(cwd, ".agenthub/shadow.git"), { throwIfNoEntry: false });
|
|
46
|
+
if (own && !repository?.shadow) throw new Error("existing shadow metadata is invalid; nothing was replaced");
|
|
47
|
+
const ignore = lstatSync(join(cwd, ".agenthub/ignore"), { throwIfNoEntry: false });
|
|
48
|
+
if (ignore && (!ignore.isFile() || ignore.isSymbolicLink() || ignore.nlink !== 1)) throw new Error("shadow ignore must be a regular project file, not a link");
|
|
49
|
+
}
|
|
50
|
+
const configPath = join(cwd, ".agenthub", "config.json");
|
|
51
|
+
const existingProfile = existsSync(configPath) ? profileName(JSON.parse(readFileSync(configPath, "utf8")).profile) : undefined;
|
|
52
|
+
let overrides: unknown, activeProfile = existingProfile;
|
|
53
|
+
for (const name of ["config.json", "config.local.json"]) {
|
|
54
|
+
const path = join(cwd, ".agenthub", name);
|
|
55
|
+
if (existsSync(path)) {
|
|
56
|
+
const layer = JSON.parse(readFileSync(path, "utf8"));
|
|
57
|
+
if (layer.profile !== undefined) activeProfile = profileName(layer.profile);
|
|
58
|
+
resolveProfile(activeProfile, layer.profile_overrides);
|
|
59
|
+
overrides = mergeProfileOverrides(overrides, layer.profile_overrides);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (requested !== undefined && activeProfile !== undefined && requested !== activeProfile) throw new Error("init profile differs from existing config; edit the project profile explicitly before refreshing instructions");
|
|
63
|
+
const profile = resolveProfile(requested ?? activeProfile, overrides);
|
|
35
64
|
const changed: PlannedWrite[] = [];
|
|
65
|
+
if (shadow && !repository?.shadow) changed.push({ action: "create", path: join(cwd, ".agenthub/shadow.git"), reason: "create opt-in shadow Git metadata without .git", shadow: true });
|
|
36
66
|
const write = (path: string, next: string, reason: string, managedBlock?: InitChange["managedBlock"]) => {
|
|
37
67
|
const exists = existsSync(path);
|
|
38
68
|
if (exists && readFileSync(path, "utf8") === next) return;
|
|
39
69
|
changed.push({ action: exists ? "modify" : "create", path, next, reason, ...(managedBlock ? { managedBlock } : {}) });
|
|
40
70
|
};
|
|
71
|
+
if (shadow && !existsSync(join(cwd, ".agenthub/ignore"))) write(join(cwd, ".agenthub/ignore"), "# Paths excluded from shadow snapshots and facts\ndata/raw/\n", "create editable large/raw data ignore policy");
|
|
41
72
|
for (const name of ["config.json", "routing.toml"]) {
|
|
42
73
|
const path = join(cwd, ".agenthub", name);
|
|
43
|
-
if (!existsSync(path))
|
|
74
|
+
if (!existsSync(path)) {
|
|
75
|
+
const template = readFileSync(join(TEMPLATES, name), "utf8");
|
|
76
|
+
const next = name === "config.json" && profile.name !== "dev" ? JSON.stringify({ ...JSON.parse(template), profile: profile.name }, null, 2) + "\n" : template;
|
|
77
|
+
write(path, next, "create missing project defaults");
|
|
78
|
+
}
|
|
44
79
|
}
|
|
45
80
|
const agents = join(cwd, "AGENTS.md");
|
|
46
81
|
const existing = existsSync(agents) ? readFileSync(agents, "utf8") : "";
|
|
47
|
-
write(agents, upsertBlock(existing, readFileSync(join(TEMPLATES, "AGENTS.block.md"), "utf8")), "refresh hub instructions; preserve surrounding text", existing.includes("<!-- AGENT_HUB:BEGIN") ? "replace" : "insert");
|
|
82
|
+
write(agents, upsertBlock(existing, overrides && Object.hasOwn(overrides as object, "agents") ? profile.agents : profile.agents || readFileSync(join(TEMPLATES, "AGENTS.block.md"), "utf8")), "refresh hub instructions; preserve surrounding text", existing.includes("<!-- AGENT_HUB:BEGIN") ? "replace" : "insert");
|
|
48
83
|
const claude = join(cwd, "CLAUDE.md");
|
|
49
84
|
const own = lstatSync(claude, { throwIfNoEntry: false });
|
|
50
85
|
// The write preserves an existing inode, including AGENTS.md -> CLAUDE.md and hard links.
|
|
@@ -57,22 +92,25 @@ function changes(cwd: string): PlannedWrite[] {
|
|
|
57
92
|
}
|
|
58
93
|
const ignore = join(cwd, ".gitignore");
|
|
59
94
|
let lines = existsSync(ignore) ? readFileSync(ignore, "utf8") : "";
|
|
60
|
-
for (const entry of [".agenthub/state/", ".agenthub/config.local.json", ".agenthub/routing.local.toml"]) {
|
|
95
|
+
for (const entry of [".agenthub/state/", ".agenthub/config.local.json", ".agenthub/routing.local.toml", ...(shadow ? [".agenthub/shadow.git/"] : [])]) {
|
|
61
96
|
if (!lines.split("\n").includes(entry)) lines = `${lines}${lines && !lines.endsWith("\n") ? "\n" : ""}${entry}\n`;
|
|
62
97
|
}
|
|
98
|
+
if (existsSync(join(cwd, KIMI_MCP_FILE)) && hubGitSync(["ls-files", "--error-unmatch", "--", KIMI_MCP_FILE], { cwd, encoding: "utf8", timeout: 3000 }).status !== 0 && !lines.split("\n").includes(KIMI_MCP_FILE)) lines = `${lines}${lines && !lines.endsWith("\n") ? "\n" : ""}${KIMI_MCP_FILE}\n`;
|
|
99
|
+
if (existsSync(join(cwd, JCODE_MCP_FILE)) && hubGitSync(["ls-files", "--error-unmatch", "--", JCODE_MCP_FILE], { cwd, encoding: "utf8", timeout: 3000 }).status !== 0 && !lines.split("\n").includes(JCODE_MCP_FILE)) lines = `${lines}${lines && !lines.endsWith("\n") ? "\n" : ""}${JCODE_MCP_FILE}\n`;
|
|
63
100
|
write(ignore, lines, "ignore runtime state and machine-local configuration");
|
|
64
101
|
return changed;
|
|
65
102
|
}
|
|
66
103
|
/** Read-only metadata, deliberately omitting project/user file contents. */
|
|
67
|
-
export function planInit(cwd: string): InitChange[] {
|
|
68
|
-
return changes(cwd).map(({ next, ...change }) => change);
|
|
104
|
+
export function planInit(cwd: string, profile?: ProfileName, shadow = false): InitChange[] {
|
|
105
|
+
return changes(cwd, profile, shadow).map(({ next, shadow: _shadow, ...change }) => change);
|
|
69
106
|
}
|
|
70
107
|
/** Returns the paths it changed, applying the same plan as the preview. */
|
|
71
|
-
export function init(cwd: string): string[] {
|
|
72
|
-
const plan = changes(cwd);
|
|
108
|
+
export function init(cwd: string, profile?: ProfileName, shadow = false): string[] {
|
|
109
|
+
const plan = changes(cwd, profile, shadow);
|
|
73
110
|
mkdirSync(join(cwd, ".agenthub"), { recursive: true });
|
|
74
111
|
for (const change of plan) {
|
|
75
|
-
if (change.
|
|
112
|
+
if (change.shadow) initShadow(cwd);
|
|
113
|
+
else if (change.action === "delete") unlinkSync(change.path);
|
|
76
114
|
else writeFileSync(change.path, change.next!);
|
|
77
115
|
}
|
|
78
116
|
return plan.map(change => change.path);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { hubGitSync } from "../hub/git.ts";
|
|
5
|
+
|
|
6
|
+
export const JCODE_MCP_FILE = ".jcode/mcp.json", JCODE_MCP_NAME = "agent-hub-jcode";
|
|
7
|
+
type Document = Record<string, unknown>;
|
|
8
|
+
const object = (value: unknown): value is Document => !!value && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
|
|
10
|
+
function document(file: string): Document {
|
|
11
|
+
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
12
|
+
if (!stat) return {};
|
|
13
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 256 * 1024) throw new Error("Jcode MCP configuration must be a bounded regular file, not a link");
|
|
14
|
+
let value: unknown; try { value = JSON.parse(readFileSync(file, "utf8")); } catch { throw new Error("Jcode MCP configuration is invalid JSON"); }
|
|
15
|
+
if (!object(value)) throw new Error("Jcode MCP configuration must be an object");
|
|
16
|
+
if (value.servers !== undefined && value.mcpServers !== undefined) throw new Error("Jcode MCP configuration has ambiguous server maps");
|
|
17
|
+
for (const key of ["servers", "mcpServers"]) if (value[key] !== undefined && !object(value[key])) throw new Error("Jcode MCP server map must be an object");
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
const map = (doc: Document): Document => (doc.mcpServers ?? doc.servers ?? {}) as Document;
|
|
21
|
+
const owned = (entry: unknown, cwd: string): entry is Document => object(entry) && object(entry.env) && entry.env.AGENTHUB_MODE === "tools" && entry.env.AGENTHUB_PEER_ID === "jcode" && entry.env.AGENTHUB_PROJECT_DIR === cwd;
|
|
22
|
+
|
|
23
|
+
export function jcodeMcpEntry(cwd: string, stateDir: string, bundle: string) {
|
|
24
|
+
return { command: "bun", args: ["run", bundle], shared: false, env: { AGENTHUB_MODE: "tools", AGENTHUB_PEER_ID: "jcode", AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } };
|
|
25
|
+
}
|
|
26
|
+
/** The native loader reads later project files last. Never shadow a foreign definition. */
|
|
27
|
+
function inspect(cwd: string): Document {
|
|
28
|
+
const directory = lstatSync(join(cwd, ".jcode"), { throwIfNoEntry: false });
|
|
29
|
+
if (directory && (!directory.isDirectory() || directory.isSymbolicLink())) throw new Error(".jcode must be a project directory, not a link");
|
|
30
|
+
for (const relative of [".mcp.json", ".claude/mcp.json"]) {
|
|
31
|
+
const parent = relative.startsWith(".claude/") ? lstatSync(join(cwd, ".claude"), { throwIfNoEntry: false }) : undefined;
|
|
32
|
+
if (parent?.isSymbolicLink()) throw new Error("Jcode MCP override layer is a link; inspect it before launching");
|
|
33
|
+
if (Object.hasOwn(map(document(join(cwd, relative))), JCODE_MCP_NAME)) throw new Error("a later project MCP layer overrides agent-hub-jcode; resolve it in your editor");
|
|
34
|
+
}
|
|
35
|
+
return document(join(cwd, JCODE_MCP_FILE));
|
|
36
|
+
}
|
|
37
|
+
export function jcodeMcpReady(cwd: string, stateDir: string, bundle: string): boolean {
|
|
38
|
+
const entry = map(inspect(cwd))[JCODE_MCP_NAME];
|
|
39
|
+
return JSON.stringify(entry) === JSON.stringify(jcodeMcpEntry(cwd, stateDir, bundle));
|
|
40
|
+
}
|
|
41
|
+
/** Only the console launcher calls this; daemon starts use jcodeMcpReady read-only. */
|
|
42
|
+
export function configureJcodeMcp(cwd: string, stateDir: string, bundle: string, remove = false): { changed: boolean; tracked: boolean } {
|
|
43
|
+
const current = inspect(cwd), servers = { ...map(current) }, existing = servers[JCODE_MCP_NAME];
|
|
44
|
+
if (existing !== undefined && !owned(existing, cwd)) throw new Error("agent-hub-jcode is a foreign MCP entry; it was not changed");
|
|
45
|
+
if (remove) delete servers[JCODE_MCP_NAME]; else servers[JCODE_MCP_NAME] = jcodeMcpEntry(cwd, stateDir, bundle);
|
|
46
|
+
const key = current.servers !== undefined ? "servers" : "mcpServers";
|
|
47
|
+
const next = { ...current, ...(current[key] !== undefined || !remove ? { [key]: servers } : {}) };
|
|
48
|
+
const tracked = hubGitSync(["ls-files", "--error-unmatch", "--", JCODE_MCP_FILE], { cwd, encoding: "utf8", timeout: 3000 }).status === 0;
|
|
49
|
+
if (JSON.stringify(next) === JSON.stringify(current)) return { changed: false, tracked };
|
|
50
|
+
mkdirSync(join(cwd, ".jcode"), { recursive: true });
|
|
51
|
+
const file = join(cwd, JCODE_MCP_FILE), temporary = `${file}.${randomUUID()}.tmp`;
|
|
52
|
+
try {
|
|
53
|
+
const fd = openSync(temporary, "wx", 0o600);
|
|
54
|
+
try { writeFileSync(fd, `${JSON.stringify(next, null, 2)}\n`); } finally { closeSync(fd); }
|
|
55
|
+
renameSync(temporary, file);
|
|
56
|
+
} finally { if (existsSync(temporary)) unlinkSync(temporary); }
|
|
57
|
+
return { changed: true, tracked };
|
|
58
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { closeSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { hubGitSync } from "../hub/git.ts";
|
|
5
|
+
|
|
6
|
+
export const KIMI_MCP_FILE = ".kimi-code/mcp.json";
|
|
7
|
+
type Document = Record<string, unknown> & { mcpServers?: Record<string, unknown> };
|
|
8
|
+
|
|
9
|
+
/** No writes: the daemon uses this check before an ACP launch to avoid a second hub connection. */
|
|
10
|
+
export function kimiMcpDocument(cwd: string): Document {
|
|
11
|
+
const directory = join(cwd, ".kimi-code"), file = join(cwd, KIMI_MCP_FILE);
|
|
12
|
+
const dir = lstatSync(directory, { throwIfNoEntry: false });
|
|
13
|
+
if (dir && (!dir.isDirectory() || dir.isSymbolicLink())) throw new Error(".kimi-code must be a project directory, not a link");
|
|
14
|
+
const stat = lstatSync(file, { throwIfNoEntry: false });
|
|
15
|
+
if (!stat) return {};
|
|
16
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) throw new Error(".kimi-code/mcp.json must be a regular file, not a link");
|
|
17
|
+
let document: unknown;
|
|
18
|
+
try { document = JSON.parse(readFileSync(file, "utf8")); }
|
|
19
|
+
catch { throw new Error(".kimi-code/mcp.json is not valid JSON; correct it before launching Kimi"); }
|
|
20
|
+
if (!document || typeof document !== "object" || Array.isArray(document)) throw new Error(".kimi-code/mcp.json must be an object");
|
|
21
|
+
const servers = (document as Document).mcpServers;
|
|
22
|
+
if (servers !== undefined && (!servers || typeof servers !== "object" || Array.isArray(servers))) throw new Error(".kimi-code/mcp.json mcpServers must be an object");
|
|
23
|
+
return document as Document;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function hasKimiHubMcp(cwd: string): boolean {
|
|
27
|
+
return Object.hasOwn(kimiMcpDocument(cwd).mcpServers ?? {}, "agent-hub");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Only the person's launcher applies this plan; a preview inspects it without writing or displaying foreign data. */
|
|
31
|
+
export function kimiMcpPlan(cwd: string, stateDir: string, bundle: string, mode: "tui" | "headless") {
|
|
32
|
+
const current = kimiMcpDocument(cwd);
|
|
33
|
+
const servers = { ...current.mcpServers };
|
|
34
|
+
if (mode === "tui") servers["agent-hub"] = { command: "bun", args: ["run", bundle], env: { AGENTHUB_PEER_ID: "kimi", AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } };
|
|
35
|
+
else delete servers["agent-hub"];
|
|
36
|
+
const next = { ...current, ...(current.mcpServers !== undefined || mode === "tui" ? { mcpServers: servers } : {}) };
|
|
37
|
+
const tracked = hubGitSync(["ls-files", "--error-unmatch", "--", KIMI_MCP_FILE], { cwd, encoding: "utf8", timeout: 3000 }).status === 0;
|
|
38
|
+
return { file: join(cwd, KIMI_MCP_FILE), action: mode === "tui" ? "merge agent-hub entry" : "remove agent-hub entry", tracked,
|
|
39
|
+
changed: JSON.stringify(current) !== JSON.stringify(next), next };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function configureKimiMcp(cwd: string, stateDir: string, bundle: string, mode: "tui" | "headless"): { changed: boolean; tracked: boolean } {
|
|
43
|
+
const plan = kimiMcpPlan(cwd, stateDir, bundle, mode);
|
|
44
|
+
if (!plan.changed) return { changed: false, tracked: plan.tracked };
|
|
45
|
+
mkdirSync(join(cwd, ".kimi-code"), { recursive: true });
|
|
46
|
+
const temporary = `${plan.file}.${randomUUID()}.tmp`;
|
|
47
|
+
try {
|
|
48
|
+
const fd = openSync(temporary, "wx", 0o600);
|
|
49
|
+
try { writeFileSync(fd, `${JSON.stringify(plan.next, null, 2)}\n`); } finally { closeSync(fd); }
|
|
50
|
+
renameSync(temporary, plan.file);
|
|
51
|
+
} finally { try { unlinkSync(temporary); } catch { /* rename consumed our temporary file */ } }
|
|
52
|
+
return { changed: true, tracked: plan.tracked };
|
|
53
|
+
}
|
package/src/cli/launch.ts
CHANGED
|
@@ -9,11 +9,12 @@ import { peerChildEnv, processTable } from "../hub/child-process.ts";
|
|
|
9
9
|
export const CLAUDE_CHANNEL = "plugin:agent-hub@agent-hub";
|
|
10
10
|
|
|
11
11
|
/** Launch identity belongs to the native child, never to the agent that invoked the wrapper. */
|
|
12
|
-
export function nativeLaunchEnv(tool: "claude" | "codex" | "pi", source: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
12
|
+
export function nativeLaunchEnv(tool: "claude" | "codex" | "pi" | "kimi", source: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
13
13
|
const env = peerChildEnv(tool, source);
|
|
14
14
|
delete env.AGENTHUB_UNATTENDED;
|
|
15
15
|
// Channel evidence for the plugin (issue #205): set beside the development-channel flag `buildLaunch` always passes.
|
|
16
16
|
if (tool === "claude") env.AGENTHUB_CHANNEL = "1";
|
|
17
|
+
if (tool === "kimi") delete env.AGENTHUB_MODE; // its MCP child must attach pull-only, never as a tools socket
|
|
17
18
|
return env;
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -391,3 +392,17 @@ export function buildKimiLaunch(command: string[], model?: string): Launch {
|
|
|
391
392
|
const [cmd, ...args] = command;
|
|
392
393
|
return { cmd: cmd!, args: model ? ["--model", model, ...args] : args };
|
|
393
394
|
}
|
|
395
|
+
|
|
396
|
+
/** Qwen flags follow the configured executable argv, including wrapper/script arguments. */
|
|
397
|
+
export function buildQwenLaunch(command: string[], model?: string): Launch {
|
|
398
|
+
const [cmd, ...args] = command;
|
|
399
|
+
if (!cmd) throw new Error("qwen_cmd must name an executable");
|
|
400
|
+
return { cmd, args: [...args, ...(model ? ["--model", model] : [])] };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** The configured ACP argv has a TUI form only when its exact final word is the ACP subcommand. */
|
|
404
|
+
export function buildKimiTuiLaunch(command: string[], model?: string): Launch {
|
|
405
|
+
if (!command.length || command.some(word => typeof word !== "string" || !word)) throw new Error("kimi_cmd must name an executable");
|
|
406
|
+
if (command.length > 1 && command.at(-1) !== "acp") throw new Error("kimi_cmd has no derivable TUI form; use a single executable or an argv ending in acp, or run ahub kimi --mode headless");
|
|
407
|
+
return buildKimiLaunch(command.length > 1 ? command.slice(0, -1) : command, model);
|
|
408
|
+
}
|