File size: 9,422 Bytes
c1bf102
 
 
 
 
 
 
c75f885
c1bf102
c75f885
c1bf102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c75f885
 
 
 
c1bf102
c75f885
 
81ee344
 
c75f885
 
 
 
 
c1bf102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c75f885
 
 
 
 
 
 
 
 
 
c1bf102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c75f885
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c1bf102
 
 
 
 
81ee344
 
c1bf102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81ee344
 
 
c1bf102
 
 
 
 
 
 
 
 
 
 
 
c75f885
 
 
 
 
 
 
81ee344
 
 
 
 
c1bf102
 
 
 
 
 
c75f885
 
 
 
c1bf102
 
 
c75f885
 
 
 
c1bf102
 
81ee344
c1bf102
 
 
 
c75f885
 
 
 
 
c1bf102
 
 
 
 
81ee344
 
c1bf102
c75f885
 
 
 
 
c1bf102
 
 
 
 
 
 
 
c75f885
c1bf102
 
c75f885
 
 
 
 
c1bf102
c75f885
 
81ee344
 
 
 
c1bf102
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#!/usr/bin/env python3
"""Install the Kaiju Coder 7 OpenCode provider and lean agent locally."""

from __future__ import annotations

import argparse
import json
import shlex
import shutil
import stat
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
AGENT_SOURCE_CANDIDATES = [
    ROOT / ".opencode/agents/kaiju-coder-7.md",
    ROOT / "agents/kaiju-coder-7.md",
]
CONFIG_SOURCE_CANDIDATES = [
    ROOT / "release/opencode/opencode.kaiju-coder-7.jsonc",
    ROOT / "opencode.kaiju-coder-7.jsonc",
]
PLUGIN_SOURCE_CANDIDATES = [
    ROOT / "scripts/opencode-kaiju-no-autocontinue.mjs",
    ROOT / "opencode-kaiju-no-autocontinue.mjs",
]
COMMAND_SOURCE_CANDIDATES = [
    ROOT / ".opencode/commands/kaiju.md",
    ROOT / "commands/kaiju.md",
]
PLUGIN_DEST_NAME = "kaiju-no-autocontinue.mjs"
RUNTIME_DEST_NAME = "kaiju-coder-7-runtime"
RUNNER_NAME = "kaiju-coder-7-run"
DEFAULT_MODEL = "kaiju/kaiju-coder-7"
DEFAULT_AGENT = "kaiju-coder-7"
RUNTIME_REQUIRED = [
    ROOT / "kaiju_harness",
    ROOT / "prompts",
    ROOT / "scripts/run_kaiju_router.py",
]


def strip_jsonc(text: str) -> str:
    # The template intentionally stays plain JSON-compatible today. This helper
    # keeps the installer tolerant if comments are added later.
    lines = []
    for line in text.splitlines():
        if line.lstrip().startswith("//"):
            continue
        lines.append(line)
    return "\n".join(lines)


def load_json(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {}
    return json.loads(strip_jsonc(path.read_text(encoding="utf-8")))


def write_json(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")


def write_plugin_package_json(config_dir: Path) -> Path:
    path = config_dir / "package.json"
    data = load_json(path)
    dependencies = dict(data.get("dependencies") or {})
    dependencies.setdefault("@opencode-ai/plugin", "1.14.33")
    data["dependencies"] = dependencies
    write_json(path, data)
    return path


def first_existing(candidates: list[Path], label: str) -> Path:
    for candidate in candidates:
        if candidate.is_file():
            return candidate
    joined = ", ".join(str(candidate) for candidate in candidates)
    raise FileNotFoundError(f"Missing {label}. Looked in: {joined}")


def plugin_list(value: Any) -> list[str]:
    if isinstance(value, str):
        return [value]
    if isinstance(value, list):
        return [item for item in value if isinstance(item, str)]
    return []


def runtime_available() -> bool:
    return all(path.exists() for path in RUNTIME_REQUIRED)


def ignore_runtime_junk(_directory: str, names: list[str]) -> set[str]:
    return {
        name
        for name in names
        if name == "__pycache__" or name.endswith(".pyc") or name == ".DS_Store"
    }


def copy_runtime(runtime_dest: Path) -> None:
    if not runtime_available():
        missing = ", ".join(str(path) for path in RUNTIME_REQUIRED if not path.exists())
        raise FileNotFoundError(f"Missing Kaiju router runtime file(s): {missing}")

    shutil.rmtree(runtime_dest, ignore_errors=True)
    (runtime_dest / "scripts").mkdir(parents=True, exist_ok=True)
    shutil.copytree(ROOT / "kaiju_harness", runtime_dest / "kaiju_harness", ignore=ignore_runtime_junk)
    shutil.copytree(ROOT / "prompts", runtime_dest / "prompts", ignore=ignore_runtime_junk)
    shutil.copy2(ROOT / "scripts/run_kaiju_router.py", runtime_dest / "scripts/run_kaiju_router.py")


def write_runner(runner_dest: Path, runtime_dest: Path) -> None:
    runner_dest.parent.mkdir(parents=True, exist_ok=True)
    runtime_arg = shlex.quote(str(runtime_dest))
    script_arg = shlex.quote(str(runtime_dest / "scripts/run_kaiju_router.py"))
    content = f"""#!/usr/bin/env bash
set -euo pipefail

RUNTIME_DIR={runtime_arg}
PYTHON_BIN="${{KAIJU_PYTHON:-python3}}"
BASE_URL="${{KAIJU_OPENAI_BASE_URL:-http://127.0.0.1:18181/v1}}"
MODEL="${{KAIJU_MODEL:-kaiju-coder-7}}"
PLANNER_TIMEOUT="${{KAIJU_PLANNER_TIMEOUT:-45}}"

if [ ! -f {script_arg} ]; then
  echo "Kaiju Coder 7 runtime is missing: $RUNTIME_DIR" >&2
  exit 2
fi

exec "$PYTHON_BIN" {script_arg} \\
  --openai-base-url "$BASE_URL" \\
  --model "$MODEL" \\
  --planner-timeout "$PLANNER_TIMEOUT" \\
  "$@"
"""
    runner_dest.write_text(content, encoding="utf-8")
    runner_dest.chmod(runner_dest.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)


def merge_provider(
    existing: dict[str, Any],
    template: dict[str, Any],
    base_url: str | None,
    plugin_path: Path,
    *,
    set_defaults: bool,
) -> dict[str, Any]:
    merged = dict(existing)
    provider = dict(merged.get("provider") or {})
    kaiju = dict((template.get("provider") or {})["kaiju"])
    if base_url:
        options = dict(kaiju.get("options") or {})
        options["baseURL"] = base_url
        kaiju["options"] = options
    provider["kaiju"] = kaiju
    merged["$schema"] = merged.get("$schema") or template.get("$schema", "https://opencode.ai/config.json")
    merged["provider"] = provider
    plugins = plugin_list(merged.get("plugin"))
    plugin_path_str = str(plugin_path)
    if plugin_path_str not in plugins:
        plugins.append(plugin_path_str)
    merged["plugin"] = plugins
    if set_defaults:
        merged["model"] = DEFAULT_MODEL
        merged["default_agent"] = DEFAULT_AGENT
    return merged


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--config-dir",
        type=Path,
        default=Path.home() / ".config/opencode",
        help="OpenCode config directory to update.",
    )
    parser.add_argument("--base-url", default=None, help="Override Kaiju OpenAI-compatible base URL.")
    parser.add_argument(
        "--bin-dir",
        type=Path,
        default=Path.home() / ".local/bin",
        help="Directory where the kaiju-coder-7-run command is installed.",
    )
    parser.add_argument("--skip-runner", action="store_true", help="Install only the OpenCode provider, agent, and plugin.")
    parser.add_argument(
        "--no-defaults",
        action="store_true",
        help="Do not set Kaiju Coder 7 as the default OpenCode model and default agent.",
    )
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()

    config_path = args.config_dir / "opencode.jsonc"
    agent_dest = args.config_dir / "agents/kaiju-coder-7.md"
    plugin_dest = args.config_dir / PLUGIN_DEST_NAME
    command_dest = args.config_dir / "commands/kaiju.md"
    package_dest = args.config_dir / "package.json"
    runtime_dest = args.config_dir / RUNTIME_DEST_NAME
    runner_dest = args.bin_dir / RUNNER_NAME
    agent_source = first_existing(AGENT_SOURCE_CANDIDATES, "Kaiju Coder 7 OpenCode agent")
    config_source = first_existing(CONFIG_SOURCE_CANDIDATES, "Kaiju Coder 7 OpenCode config")
    plugin_source = first_existing(PLUGIN_SOURCE_CANDIDATES, "Kaiju Coder 7 OpenCode loop guard")
    command_source = first_existing(COMMAND_SOURCE_CANDIDATES, "Kaiju Coder 7 OpenCode command")
    if not args.skip_runner and not runtime_available():
        missing = ", ".join(str(path) for path in RUNTIME_REQUIRED if not path.exists())
        raise FileNotFoundError(f"Missing Kaiju router runtime file(s): {missing}")
    existing = load_json(config_path)
    template = load_json(config_source)
    merged = merge_provider(existing, template, args.base_url, plugin_dest, set_defaults=not args.no_defaults)

    print(f"Config: {config_path}")
    print(f"Agent:  {agent_dest}")
    print(f"Plugin: {plugin_dest}")
    print(f"Command: {command_dest}")
    print(f"Package: {package_dest}")
    if not args.skip_runner:
        print(f"Runtime: {runtime_dest}")
        print(f"Runner:  {runner_dest}")
    if args.dry_run:
        print(
            json.dumps(
                {
                    "plugin": merged.get("plugin", []),
                    "model": merged.get("model"),
                    "default_agent": merged.get("default_agent"),
                    "kaiju": merged.get("provider", {}).get("kaiju", {}),
                    "command": str(command_dest),
                    "package": str(package_dest),
                    "package_dependency": "@opencode-ai/plugin",
                    "runtime": None if args.skip_runner else str(runtime_dest),
                    "runner": None if args.skip_runner else str(runner_dest),
                },
                indent=2,
            )
        )
        return 0

    write_json(config_path, merged)
    agent_dest.parent.mkdir(parents=True, exist_ok=True)
    command_dest.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(agent_source, agent_dest)
    shutil.copy2(plugin_source, plugin_dest)
    shutil.copy2(command_source, command_dest)
    write_plugin_package_json(args.config_dir)
    if not args.skip_runner:
        copy_runtime(runtime_dest)
        write_runner(runner_dest, runtime_dest)
    print("Installed Kaiju Coder 7 OpenCode profile.")
    if not args.skip_runner:
        print(f"Runner command: {runner_dest}")
    if args.no_defaults:
        print("Run: opencode -m kaiju/kaiju-coder-7 --agent kaiju-coder-7")
    else:
        print("Run: opencode")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())