@staix/agent-hub 0.12.23 → 0.12.24
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 +98 -0
- 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 +8 -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 +4 -0
- package/docs/agent-notes/tests.md +2 -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 +651 -14
- 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 +1 -1
- 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 +282 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +601 -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/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 +94 -20
- 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 +910 -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 +96 -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 +892 -87
- package/src/hub/text-projection.ts +126 -0
- package/src/hub/tooling.ts +159 -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 +750 -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/role-text.d.ts +1 -0
- package/src/ui/index.html +842 -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/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
|
+
}
|