Harbor as of 2026-10-05: the coordination hub for the Open Sea's decomp team
Messages, threads, acknowledgements, claims, bands, tasks, review items and stop flags for a team of AI agents; the chat page and board; host hooks and MCP server; autopilot; tools/watch.py. Put under version control after the weekend's changes so they have history and can be rolled back. Local only.
This commit is contained in:
commit
dbfd1720c5
43 files changed
+12440
No files matched your search
@@ -0,0 +1,395 @@
|
||||
"""Connect agent hosts to Harbor: `harbor install <host> --agent <Name> [--project <name>] [--dry-run|--uninstall]`.
|
||||
|
||||
Hosts and what gets written (every edited file is backed up first; Harbor's part sits between marker lines so it
|
||||
can be replaced or removed without touching anything else):
|
||||
|
||||
codex ~/.codex/config.toml [mcp_servers.harbor] (Harbor's MCP tools)
|
||||
~/.codex/hooks.json SessionStart, UserPromptSubmit, PreToolUse, Stop -> `harbor hook ...`
|
||||
~/.codex/AGENTS.md the Harbor protocol, applied only inside Harbor projects
|
||||
Codex runs hooks only after you trust them: Settings > Hooks > Trust.
|
||||
opencode ~/.config/opencode/opencode.jsonc "mcp": {"harbor": ...}
|
||||
~/.config/opencode/plugin/harbor.js STOP gate, session brief, idle nudges
|
||||
~/.config/opencode/AGENTS.md
|
||||
deepseek ~/.dsh/profiles/desktop/cordis.patch.yml MCP client row + Claude-format hooks bridge row
|
||||
<harbor home>/integrations/deepseek/hooks.json
|
||||
~/.dsh/AGENTS.md
|
||||
MCP tools hot-reload; the hooks file is read when DeepSeek Harness starts.
|
||||
kimi <harbor home>/integrations/kimi/harbor-<project>/ a Kimi personal plugin (MCP, hooks, system prompt),
|
||||
validated and registered with Kimi's own tools; you install it from Kimi's Plugins > Personal tab.
|
||||
Kimi works outside the project folder, so its plugin is bound to one project.
|
||||
claude prints how to add the Claude Code plugin in integrations/claude-code.
|
||||
|
||||
Hooks resolve the project from the agent's working directory (or --project when bound), and stay silent in folders
|
||||
that are not Harbor projects, so installing them in a host used for other work is harmless.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from . import projects
|
||||
from .texts import launcher, protocol as agents_section # noqa: F401
|
||||
|
||||
PACKAGE_ROOT = projects.PACKAGE_ROOT
|
||||
INTEGRATIONS = PACKAGE_ROOT / "integrations"
|
||||
START, END = "# >>> harbor >>>", "# <<< harbor <<<"
|
||||
MD_START, MD_END = "<!-- >>> harbor >>> -->", "<!-- <<< harbor <<< -->"
|
||||
HOSTS = ("codex", "opencode", "deepseek", "kimi", "claude")
|
||||
DEFAULT_AGENT = {"codex": "Codex", "opencode": "OpenCode", "deepseek": "DeepSeek", "kimi": "Kimi", "claude": "Claude"}
|
||||
|
||||
|
||||
class InstallError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def user_home():
|
||||
return Path(os.environ.get("HARBOR_INSTALL_HOME") or Path.home())
|
||||
|
||||
|
||||
def identity_args(agent, project):
|
||||
return ["--as", agent] + (["--project", project] if project else [])
|
||||
|
||||
|
||||
class Plan:
|
||||
"""Collects file writes so --dry-run can show them and a real run can back up and apply them."""
|
||||
|
||||
def __init__(self, host, dry_run):
|
||||
self.host = host
|
||||
self.dry_run = dry_run
|
||||
self.writes = []
|
||||
self.notes = []
|
||||
self.stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
|
||||
def write(self, path, text, validate=None):
|
||||
if validate and text is not None:
|
||||
validate(text)
|
||||
self.writes.append((Path(path), text))
|
||||
|
||||
def remove(self, path):
|
||||
self.writes.append((Path(path), None))
|
||||
|
||||
def apply(self):
|
||||
report = []
|
||||
for path, text in self.writes:
|
||||
old = path.read_text(encoding="utf-8") if path.exists() else None
|
||||
if old == text:
|
||||
report.append(f"unchanged {path}")
|
||||
continue
|
||||
action = "remove" if text is None else ("update" if old is not None else "create")
|
||||
report.append(f"{action:<10} {path}")
|
||||
if self.dry_run:
|
||||
continue
|
||||
if old is not None:
|
||||
backup = projects.harbor_home() / "backups" / "integrations" / self.stamp / self.host / path.name
|
||||
backup.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(path, backup)
|
||||
report[-1] += f" (backup: {backup})"
|
||||
if text is None:
|
||||
path.unlink()
|
||||
else:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8", newline="\n")
|
||||
return report + self.notes
|
||||
|
||||
|
||||
def replace_block(text, block, start=START, end=END):
|
||||
"""Put block between the markers (replacing an earlier Harbor block); block=None removes it."""
|
||||
pattern = re.compile(re.escape(start) + r".*?" + re.escape(end) + r"\n?", re.S)
|
||||
text = pattern.sub("", text or "")
|
||||
if block is None:
|
||||
return text.rstrip("\n") + "\n" if text.strip() else ""
|
||||
return (text.rstrip("\n") + "\n\n" if text.strip() else "") + f"{start}\n{block.rstrip()}\n{end}\n"
|
||||
|
||||
|
||||
def _hook_entries(cmd, agent, project, host, events):
|
||||
base = " ".join(cmd + identity_args(agent, project) + ["hook"])
|
||||
names = {"SessionStart": "session-start", "UserPromptSubmit": "prompt-submit", "PreToolUse": "pre-tool",
|
||||
"Stop": "stop"}
|
||||
return {event: [{"hooks": [{"type": "command", "command": f"{base} {names[event]} --host {host}", "timeout": 30}]}]
|
||||
for event in events}
|
||||
|
||||
|
||||
def _is_ours(entry):
|
||||
return "run.py" in json.dumps(entry) and " hook " in json.dumps(entry)
|
||||
|
||||
|
||||
def merge_hooks_json(text, ours, remove=False):
|
||||
data = json.loads(text) if text and text.strip() else {}
|
||||
hooks = data.setdefault("hooks", {})
|
||||
for event in list(hooks):
|
||||
hooks[event] = [e for e in hooks[event] if not _is_ours(e)]
|
||||
if not hooks[event]:
|
||||
del hooks[event]
|
||||
if not remove:
|
||||
for event, entries in ours.items():
|
||||
hooks.setdefault(event, []).extend(entries)
|
||||
if not hooks:
|
||||
data.pop("hooks")
|
||||
return json.dumps(data, indent=2) + "\n"
|
||||
|
||||
|
||||
def _read(path):
|
||||
return path.read_text(encoding="utf-8") if path.exists() else ""
|
||||
|
||||
|
||||
def _md_block(path, section, remove):
|
||||
return replace_block(_read(path), None if remove else section, MD_START, MD_END)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------------- hosts
|
||||
def install_codex(plan, agent, project, cmd, remove):
|
||||
home = user_home() / ".codex"
|
||||
config = home / "config.toml"
|
||||
text = _read(config)
|
||||
outside = replace_block(text, None)
|
||||
if re.search(r"^\[mcp_servers\.harbor\]", outside, re.M):
|
||||
raise InstallError(f"{config} already defines [mcp_servers.harbor] outside Harbor's block; remove it first")
|
||||
if re.search(r"^\[hooks\]", outside, re.M):
|
||||
raise InstallError(f"{config} has a [hooks] table; Codex refuses hooks in both config.toml and hooks.json")
|
||||
args = cmd[1:] + identity_args(agent, project) + ["mcp"]
|
||||
env = {"HARBOR_AGENT": agent, "HARBOR_HOST": "codex", **({"HARBOR_PROJECT": project} if project else {})}
|
||||
block = (f"# Managed by `harbor install codex`; remove with `harbor install codex --uninstall`.\n"
|
||||
f"[mcp_servers.harbor]\ncommand = {json.dumps(cmd[0])}\nargs = {json.dumps(args)}\n"
|
||||
f"startup_timeout_sec = 30\ntool_timeout_sec = 1900\n\n[mcp_servers.harbor.env]\n"
|
||||
+ "".join(f"{k} = {json.dumps(v)}\n" for k, v in env.items()))
|
||||
plan.write(config, replace_block(text, None if remove else block), validate=tomllib.loads)
|
||||
hooks_path = home / "hooks.json"
|
||||
ours = _hook_entries(cmd, agent, project, "codex", ("SessionStart", "UserPromptSubmit", "PreToolUse", "Stop"))
|
||||
merged = merge_hooks_json(_read(hooks_path), ours, remove=remove)
|
||||
plan.write(hooks_path, merged if json.loads(merged) else None, validate=json.loads)
|
||||
plan.write(home / "AGENTS.md", _md_block(home / "AGENTS.md", agents_section(agent, project, cmd), remove))
|
||||
if not remove:
|
||||
plan.notes.append("Codex: open Settings > Hooks and trust the Harbor hooks (until then Codex skips them). "
|
||||
"The MCP tools load in new Codex threads.")
|
||||
|
||||
|
||||
def _jsonc_load(text):
|
||||
if re.search(r"(^|\s)//|/\*", re.sub(r'"(?:\\.|[^"\\])*"', '""', text)):
|
||||
raise InstallError("opencode.jsonc has comments; add the \"mcp\" entry by hand (see `--dry-run` output)")
|
||||
return json.loads(text) if text.strip() else {}
|
||||
|
||||
|
||||
def install_opencode(plan, agent, project, cmd, remove):
|
||||
home = user_home() / ".config" / "opencode"
|
||||
config = next((p for p in (home / "opencode.jsonc", home / "opencode.json") if p.exists()), home / "opencode.jsonc")
|
||||
data = _jsonc_load(_read(config))
|
||||
mcp = data.setdefault("mcp", {})
|
||||
mcp.pop("harbor", None)
|
||||
if not remove:
|
||||
env = {"HARBOR_AGENT": agent, "HARBOR_HOST": "opencode", **({"HARBOR_PROJECT": project} if project else {})}
|
||||
mcp["harbor"] = {"type": "local", "command": cmd + identity_args(agent, project) + ["mcp"],
|
||||
"environment": env, "enabled": True, "timeout": 15000}
|
||||
if not mcp:
|
||||
data.pop("mcp")
|
||||
plan.write(config, json.dumps(data, indent=2) + "\n", validate=json.loads)
|
||||
plugin = home / "plugin" / "harbor.js"
|
||||
if remove:
|
||||
plan.remove(plugin) if plugin.exists() else None
|
||||
else:
|
||||
source = (INTEGRATIONS / "opencode" / "harbor.js").read_text(encoding="utf-8")
|
||||
settings = (f"const AGENT = {json.dumps(agent)};\nconst PROJECT = {json.dumps(project)};\n"
|
||||
f"const COMMAND = {json.dumps(cmd)};\nconst WAKE_IDLE_SESSIONS = true; // nudge idle sessions "
|
||||
"when messages arrive (never while stopped)\n")
|
||||
source = re.sub(r"(// >>> harbor settings >>>\n).*?(// <<< harbor settings <<<)",
|
||||
lambda m: m.group(1) + settings + m.group(2), source, flags=re.S)
|
||||
plan.write(plugin, source)
|
||||
plan.write(home / "AGENTS.md", _md_block(home / "AGENTS.md", agents_section(agent, project, cmd), remove))
|
||||
if not remove:
|
||||
plan.notes.append("OpenCode: restart OpenCode to load the plugin and the MCP server.")
|
||||
|
||||
|
||||
class _TolerantLoader:
|
||||
@staticmethod
|
||||
def load(text):
|
||||
import yaml
|
||||
|
||||
class Loader(yaml.SafeLoader):
|
||||
pass
|
||||
Loader.add_multi_constructor("", lambda loader, suffix, node: None)
|
||||
Loader.add_multi_constructor("tag:yaml.org,2002:js", lambda loader, suffix, node: None)
|
||||
value = yaml.load(text, Loader=Loader)
|
||||
if not isinstance(value, list):
|
||||
raise InstallError("cordis.patch.yml must stay a YAML list")
|
||||
return value
|
||||
|
||||
|
||||
def install_deepseek(plan, agent, project, cmd, remove):
|
||||
home = user_home() / ".dsh"
|
||||
patch = home / "profiles" / "desktop" / "cordis.patch.yml"
|
||||
if not patch.exists() and not remove:
|
||||
raise InstallError(f"{patch} not found: is DeepSeek Harness set up for this user?")
|
||||
hooks_path = projects.harbor_home() / "integrations" / "deepseek" / "hooks.json"
|
||||
args = cmd[1:] + identity_args(agent, project) + ["mcp"]
|
||||
env = {"HARBOR_AGENT": agent, "HARBOR_HOST": "deepseek", **({"HARBOR_PROJECT": project} if project else {})}
|
||||
q = lambda s: "'" + str(s).replace("'", "''") + "'"
|
||||
block = ("# Managed by `harbor install deepseek`: Harbor's MCP tools and hooks.\n- insert:\n"
|
||||
" - id: mcp-harbor\n name: '@deepseek-ai/dsh-mcp-client'\n config:\n"
|
||||
f" serverName: harbor\n transport: stdio\n command: {q(cmd[0])}\n"
|
||||
f" args: [{', '.join(q(a) for a in args)}]\n"
|
||||
f" env: {{{', '.join(f'{k}: {q(v)}' for k, v in env.items())}}}\n"
|
||||
" toolCallTimeoutMs: 1900000\n"
|
||||
" - id: hooks-harbor\n name: '@deepseek-ai/dsh-hooks-claude-code'\n config:\n"
|
||||
f" configPath: {q(hooks_path.as_posix())}\n defaultTimeoutMs: 30000\n")
|
||||
text = replace_block(_read(patch), None if remove else block)
|
||||
plan.write(patch, text if text.strip() else "[]\n", validate=_TolerantLoader.load)
|
||||
if remove:
|
||||
plan.remove(hooks_path) if hooks_path.exists() else None
|
||||
else:
|
||||
ours = _hook_entries(cmd, agent, project, "deepseek", ("SessionStart", "UserPromptSubmit", "PreToolUse", "Stop"))
|
||||
plan.write(hooks_path, json.dumps({"hooks": ours}, indent=2) + "\n", validate=json.loads)
|
||||
plan.write(home / "AGENTS.md", _md_block(home / "AGENTS.md", agents_section(agent, project, cmd), remove))
|
||||
if not remove:
|
||||
plan.notes.append("DeepSeek Harness: the MCP tools hot-reload; restart the app once to load the hooks file.")
|
||||
|
||||
|
||||
def kimi_share():
|
||||
return Path(os.environ.get("KIMI_SHARE_DIR") or Path(os.environ.get("APPDATA", user_home())) / "kimi-desktop" / "daimon-share")
|
||||
|
||||
|
||||
def install_kimi(plan, agent, project, cmd, remove):
|
||||
if not project:
|
||||
project = projects.resolve().name
|
||||
folder = projects.harbor_home() / "integrations" / "kimi" / f"harbor-{project}"
|
||||
name = f"harbor-{project}"
|
||||
if remove:
|
||||
for path in sorted(folder.rglob("*"), reverse=True) if folder.exists() else []:
|
||||
if path.is_file():
|
||||
plan.remove(path)
|
||||
plan.notes.append(f"Kimi: uninstall '{name}' from Plugins > Personal in Kimi as well.")
|
||||
return
|
||||
base = " ".join(cmd + identity_args(agent, project) + ["hook"])
|
||||
title = projects.load_named(project).title
|
||||
manifest = {
|
||||
"$schema": "https://catalog.msh.team/misc/kimi.plugin.schema.json",
|
||||
"name": name, "version": "1.3.0",
|
||||
"description": f"Connects Kimi to the Harbor team hub for {title}: messages, acknowledgements, claims, the "
|
||||
"review queue and the stop switch shared with the other agents.",
|
||||
"keywords": ["harbor", "team", "coordination"], "author": "Harbor", "license": "MIT",
|
||||
"skillInstructions": "Use the Harbor tools for all team communication on this project.",
|
||||
"interface": {
|
||||
"displayName": f"Harbor: {title}", "shortDescription": f"Team coordination for {title}",
|
||||
"longDescription": f"Harbor tools, session brief, stop gate and message nudges for {title}.",
|
||||
"developerName": "Harbor", "websiteURL": "", "iconUrl": "", "category": "PRODUCTIVITY",
|
||||
"defaultLocale": "en-US", "locales": "./locales/",
|
||||
"tryPrompts": ["Check Harbor for messages", "What is the team doing right now?"],
|
||||
"skillOverrides": {name: {"displayName": f"Harbor: {title}", "description": f"Team coordination for {title}"}},
|
||||
"hostKind": "local", "platforms": ["windows", "macos", "linux"],
|
||||
"mcpOverrides": {"harbor": {"displayName": "Harbor", "description": f"Team coordination for {title}"}},
|
||||
},
|
||||
"skills": "./skills/",
|
||||
"mcpServers": {"harbor": {"command": cmd[0], "args": cmd[1:] + identity_args(agent, project) + ["mcp"],
|
||||
"env": {"HARBOR_AGENT": agent, "HARBOR_PROJECT": project, "HARBOR_HOST": "kimi"},
|
||||
"toolTimeoutMs": 1900000}},
|
||||
"systemPromptPath": "./system-prompt.md",
|
||||
"hooks": [
|
||||
{"event": "UserPromptSubmit", "command": f"{base} prompt-submit --host kimi", "timeout": 30},
|
||||
{"event": "PreToolUse", "command": f"{base} pre-tool --host kimi", "timeout": 30},
|
||||
{"event": "Stop", "command": f"{base} stop --host kimi", "timeout": 30},
|
||||
],
|
||||
}
|
||||
zh = {"description": f"把 Kimi 接入 {title} 的 Harbor 团队协作:消息、确认、认领、评审队列和暂停开关。",
|
||||
"interface": {"displayName": f"Harbor:{title}", "shortDescription": f"{title} 的团队协作",
|
||||
"longDescription": f"{title} 的 Harbor 工具、会话简报、暂停闸门和消息提醒。",
|
||||
"tryPrompts": ["查看 Harbor 消息", "团队现在在做什么?"],
|
||||
"skillOverrides": {name: {"displayName": f"Harbor:{title}", "description": f"{title} 的团队协作"}},
|
||||
"mcpOverrides": {"harbor": {"displayName": "Harbor", "description": f"{title} 的团队协作"}}}}
|
||||
plan.write(folder / "kimi.plugin.json", json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", validate=json.loads)
|
||||
plan.write(folder / "locales" / "zh-CN.json", json.dumps(zh, indent=2, ensure_ascii=False) + "\n", validate=json.loads)
|
||||
plan.write(folder / "system-prompt.md", agents_section(agent, project, cmd))
|
||||
plan.write(folder / "skills" / name / "SKILL.md",
|
||||
f"---\nname: {name}\ndescription: Team coordination for {title} through Harbor (messages, claims, "
|
||||
"review queue, stop switch). Use at the start of every session and whenever you talk to the team.\n---\n\n"
|
||||
+ agents_section(agent, project, cmd))
|
||||
plan.notes.append(f"Kimi: plugin folder {folder}. After it is registered, install it from Kimi's Plugins > "
|
||||
"Personal tab (Kimi asks you to review plugins that carry hooks).")
|
||||
plan.kimi_folder = folder
|
||||
|
||||
|
||||
def kimi_register(folder, dry_run):
|
||||
scripts = kimi_share() / "daimon" / "skills" / "plugin-builder" / "scripts"
|
||||
daimon = Path(os.environ.get("APPDATA", "")) / "kimi-desktop" / "daimon-bundle" / "bin" / "kimi-daimon.cmd"
|
||||
if not (scripts / "register_personal.py").exists() or not daimon.exists():
|
||||
return "Kimi: its plugin tools were not found; ask Kimi to register the plugin folder (plugin-builder skill)."
|
||||
validate = subprocess.run([sys.executable, str(scripts / "validate_plugin.py"), str(folder)], capture_output=True,
|
||||
text=True, encoding="utf-8", errors="replace")
|
||||
errors = [line for line in validate.stdout.splitlines() + validate.stderr.splitlines() if "[ERROR]" in line]
|
||||
if errors:
|
||||
return "Kimi validation failed:\n " + "\n ".join(errors)
|
||||
if dry_run:
|
||||
return "Kimi: plugin validates; a real run registers it in the Personal market."
|
||||
r = subprocess.run([sys.executable, str(scripts / "register_personal.py"), "--share-dir", str(kimi_share()),
|
||||
"--daimon-bin", str(daimon), str(folder)], capture_output=True, text=True, encoding="utf-8",
|
||||
errors="replace")
|
||||
return ("Kimi: registered in the Personal market. " if r.returncode == 0 else
|
||||
f"Kimi registration failed (exit {r.returncode}): ") + (r.stdout.strip() or r.stderr.strip())[-600:]
|
||||
|
||||
|
||||
def install_claude(plan, agent, project, cmd, remove):
|
||||
"""A local Claude Code marketplace holding the Harbor plugin (MCP server, hooks, skill)."""
|
||||
market = projects.harbor_home() / "integrations" / "claude"
|
||||
plugin = market / "harbor"
|
||||
if remove:
|
||||
for path in sorted(market.rglob("*"), reverse=True) if market.exists() else []:
|
||||
if path.is_file():
|
||||
plan.remove(path)
|
||||
plan.notes.append("Claude Code: also run `/plugin uninstall harbor@harbor-local`.")
|
||||
return
|
||||
ident = ["--project", project] if project else []
|
||||
base = " ".join(cmd + ident + ["hook"])
|
||||
hooks = {event: [{"hooks": [{"type": "command", "command": f"{base} {name} --host claude", "timeout": 30}]}]
|
||||
for event, name in (("SessionStart", "session-start"), ("UserPromptSubmit", "prompt-submit"),
|
||||
("PreToolUse", "pre-tool"), ("Stop", "stop"))}
|
||||
dump = lambda value: json.dumps(value, indent=2) + "\n"
|
||||
plan.write(market / ".claude-plugin" / "marketplace.json", dump({
|
||||
"name": "harbor-local", "owner": {"name": "Harbor"},
|
||||
"plugins": [{"name": "harbor", "source": "./harbor",
|
||||
"description": "Harbor team coordination: MCP tools, session brief, stop gate, message nudges"}]}),
|
||||
validate=json.loads)
|
||||
plan.write(plugin / ".claude-plugin" / "plugin.json", dump({
|
||||
"name": "harbor", "version": "2.0.0", "author": {"name": "Harbor"},
|
||||
"description": "Harbor team coordination: MCP tools, session brief, stop gate and message nudges. The agent "
|
||||
"identity comes from the HARBOR_AGENT environment variable."}), validate=json.loads)
|
||||
plan.write(plugin / "hooks" / "hooks.json", dump({"hooks": hooks}), validate=json.loads)
|
||||
env = {"HARBOR_AGENT": "${HARBOR_AGENT:-" + agent + "}", "HARBOR_HOST": "claude",
|
||||
**({"HARBOR_PROJECT": project} if project else {})}
|
||||
plan.write(plugin / ".mcp.json", dump({"mcpServers": {"harbor": {"command": cmd[0], "args": cmd[1:] + ident + ["mcp"],
|
||||
"env": env}}}), validate=json.loads)
|
||||
plan.write(plugin / "skills" / "harbor" / "SKILL.md",
|
||||
"---\nname: harbor\ndescription: Team coordination through Harbor (messages, claims, review queue, "
|
||||
"stop switch). Use at the start of a session in a Harbor project and whenever you talk to the team.\n"
|
||||
"---\n\n" + agents_section("the agent named by HARBOR_AGENT", project, cmd))
|
||||
settings = json.dumps({"env": {"HARBOR_AGENT": agent}})
|
||||
plan.notes.append(f"Claude Code: run `/plugin marketplace add {market.as_posix()}` then `/plugin install "
|
||||
f"harbor@harbor-local`, and set HARBOR_AGENT for the session, for example {settings} in "
|
||||
".claude/settings.json.")
|
||||
|
||||
|
||||
def install(host, agent=None, project=None, dry_run=False, remove=False, python=None, register=True):
|
||||
if host not in HOSTS:
|
||||
raise InstallError(f"host must be one of {', '.join(HOSTS)}")
|
||||
agent = agent or DEFAULT_AGENT[host]
|
||||
if project:
|
||||
p = projects.load_named(project)
|
||||
else:
|
||||
try:
|
||||
p = projects.resolve()
|
||||
except projects.ProjectError:
|
||||
p = None
|
||||
if p and agent not in p.ai_names:
|
||||
raise InstallError(f"{agent} is not an agent of project {p.name} ({', '.join(p.ai_names)})")
|
||||
cmd = launcher(python)
|
||||
plan = Plan(host, dry_run)
|
||||
{"codex": install_codex, "opencode": install_opencode, "deepseek": install_deepseek, "kimi": install_kimi,
|
||||
"claude": install_claude}[host](plan, agent, project, cmd, remove)
|
||||
report = plan.apply()
|
||||
if host == "kimi" and not remove and getattr(plan, "kimi_folder", None):
|
||||
if dry_run:
|
||||
report.append("Kimi: a real run validates the plugin with Kimi's validator and registers it.")
|
||||
elif register:
|
||||
report.append(kimi_register(plan.kimi_folder, dry_run))
|
||||
return report
|
||||
Reference in new issue
Block a user