@caddis/cli 0.4.39 → 0.4.41

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "caddis",
3
- "version": "1.3.126",
3
+ "version": "1.3.127",
4
4
  "description": "caddis — agent-agnostic dev harness as an Antigravity (agy) plugin: TDD skills + commands, single-sourced with the Claude Code plugin."
5
5
  }
@@ -23,9 +23,21 @@ import shutil
23
23
  import subprocess
24
24
  import sys
25
25
  import time
26
+ from collections.abc import Callable, Mapping
26
27
  from datetime import datetime, timezone
27
28
  from pathlib import Path
28
29
 
30
+ FIX_HINTS: dict[str, str] = {
31
+ "python-missing": "install Python 3.11 or later; on Linux also run: sudo apt install python-is-python3",
32
+ "python-store-alias": "this is the Microsoft Store alias; install Python from python.org or with winget, then turn off the alias in Settings > Apps > App execution aliases",
33
+ "python-old": "install Python 3.11 or later and make it the first python on PATH",
34
+ "git-missing": "install git (Windows: winget install Git.Git; Linux: sudo apt install git)",
35
+ "keys-none": "optional: run caddis keys to add GLM or DeepSeek keys",
36
+ "deny-rules": "run /caddis:setup-project-ai to add the missing deny rules",
37
+ "gitignore": "run /caddis:setup-project-ai to add the missing .caddis ignore entries",
38
+ "misfiled": "move the file into a caddis document folder, or delete it",
39
+ }
40
+
29
41
  # Probed contract versions (docs/analysis/*-contract.md). WARN when the installed binary drifts past
30
42
  # these — the recorded schemas may be stale and want a re-probe.
31
43
  PROBED_VERSIONS = {"codex": "0.137", "agy": "1.1.5"}
@@ -36,6 +48,29 @@ AGENTS_MD_BUDGET = 200 # always-loaded rules file line budget (mirrors check_do
36
48
  ARTIFACT_DIRS = (".caddis",)
37
49
  ENV_PREFIX = "CADDIS"
38
50
 
51
+ # Known top-level document and working artifact folders under .caddis/
52
+ KNOWN_CADDIS_DIRS: set[str] = {
53
+ "plans",
54
+ "prd",
55
+ "kb",
56
+ "rca",
57
+ "decisions",
58
+ "parking-lot",
59
+ "reviews",
60
+ "todo-list",
61
+ "comms",
62
+ "handoffs",
63
+ "prompts",
64
+ "relay",
65
+ "session-state",
66
+ "advisory-hub-reports",
67
+ "orchestrator",
68
+ "spawn-session",
69
+ "metrics",
70
+ "agent-docs",
71
+ "backlog",
72
+ }
73
+
39
74
 
40
75
  def _home() -> Path:
41
76
  return Path(os.environ.get(f"{ENV_PREFIX}_FAKE_HOME") or Path.home())
@@ -288,7 +323,7 @@ def _file_signals(dest: Path) -> list[str]:
288
323
  signals.append(f"{len(dangling)} dangling DOC-MAP link(s) — run /caddis:kb")
289
324
  days = days_since_last_doctor()
290
325
  if days is not None and days >= 30:
291
- signals.append(f"{days} days since last caddis doctor — run caddis-init --doctor")
326
+ signals.append(f"{days} days since last caddis doctor — run caddis doctor")
292
327
  # Listed LAST but it is the most dangerous signal here: the others degrade a session's quality,
293
328
  # this one silently runs old code and reports success. It is cheap (a file compare) and almost
294
329
  # always empty, so it costs nothing to carry.
@@ -312,19 +347,30 @@ def nudge_line(dest: Path) -> str | None:
312
347
  return f"[caddis] {head}{more}"
313
348
 
314
349
 
350
+ def _load_sibling(name: str, fallback_dir: Path | str | None = None):
351
+ """Load a helper module by filename/module name: sibling first, then fallback_dir."""
352
+ scripts = Path(__file__).resolve().parent
353
+ file_name = name if name.endswith(".py") else f"{name}.py"
354
+ mod_name = name[:-3] if name.endswith(".py") else name
355
+ path = scripts / file_name
356
+ if not path.is_file() and fallback_dir:
357
+ path = Path(fallback_dir) / file_name
358
+ if not path.is_file():
359
+ return None
360
+ spec = importlib.util.spec_from_file_location(f"caddis_{mod_name}", path)
361
+ if spec is None or spec.loader is None:
362
+ return None
363
+ module = importlib.util.module_from_spec(spec)
364
+ spec.loader.exec_module(module)
365
+ return module
366
+
367
+
315
368
  def hook_errors(dest: Path) -> str:
316
369
  """Recent hook failures, counted by hook. Empty when the ledger is clean."""
317
370
  scripts = Path(__file__).resolve().parent
318
- hook_log_path = scripts / "hook_log.py"
319
- if not hook_log_path.is_file(): # source checkout; installed copies are siblings
320
- hook_log_path = scripts.parent / "claude-harness" / "scripts" / "hook_log.py"
321
371
  try:
322
- spec = importlib.util.spec_from_file_location("caddis_hook_log", hook_log_path)
323
- if spec is None or spec.loader is None:
324
- return ""
325
- hook_log = importlib.util.module_from_spec(spec)
326
- spec.loader.exec_module(hook_log)
327
- if not hook_log.summarise(str(dest), days=7):
372
+ hook_log = _load_sibling("hook_log.py", scripts.parent / "claude-harness" / "scripts")
373
+ if hook_log is None or not hook_log.summarise(str(dest), days=7):
328
374
  return ""
329
375
  cutoff = time.strftime("%Y-%m-%d", time.localtime(time.time() - 7 * 86400))
330
376
  counts: dict[str, int] = {}
@@ -342,6 +388,182 @@ def hook_errors(dest: Path) -> str:
342
388
  return "" # a damaged ledger must not break the doctor
343
389
 
344
390
 
391
+ def machine_findings(which: Callable[[str], str | None] | None = None,
392
+ version: Callable[[str], str | None] | None = None) -> list[tuple[str, str]]:
393
+ """Machine faults that stop caddis on a new laptop. Levels: OK, INFO, FAIL (never raises)."""
394
+ which = which or shutil.which # resolved at call time, so a test can patch shutil.which
395
+ version = version or _binary_version
396
+
397
+ out: list[tuple[str, str]] = []
398
+
399
+ # python
400
+ py_path = which("python")
401
+ if py_path is None:
402
+ out.append(("FAIL", f"python (on PATH): not found — {FIX_HINTS['python-missing']}"))
403
+ elif "windowsapps" in py_path.lower() or "WindowsApps" in py_path:
404
+ out.append(("FAIL", f"python (on PATH): [{py_path}] — {FIX_HINTS['python-store-alias']}"))
405
+ else:
406
+ ver_str = None
407
+ try:
408
+ ver_str = version(py_path)
409
+ except Exception:
410
+ ver_str = None
411
+
412
+ m = re.search(r"(\d+)\.(\d+)(?:\.(\d+))?", ver_str or "")
413
+ if not m:
414
+ out.append(("INFO", "python (on PATH): could not read its version"))
415
+ else:
416
+ major, minor = int(m.group(1)), int(m.group(2))
417
+ ver_display = (ver_str or "").strip()
418
+ if ver_display.startswith("Python "):
419
+ ver_display = ver_display[7:].strip()
420
+ if (major, minor) < (3, 11):
421
+ out.append(("FAIL", f"python (on PATH): {ver_display} [{py_path}] — {FIX_HINTS['python-old']}"))
422
+ else:
423
+ out.append(("OK", f"python (on PATH): {ver_display} [{py_path}]"))
424
+
425
+ # git
426
+ git_path = which("git")
427
+ if git_path is None:
428
+ out.append(("FAIL", f"git: not found — {FIX_HINTS['git-missing']}"))
429
+ else:
430
+ out.append(("OK", f"git: [{git_path}]"))
431
+
432
+ return out
433
+
434
+
435
+ def keys_line(env: Mapping[str, str]) -> tuple[str, str]:
436
+ scripts = Path(__file__).resolve().parent
437
+ try:
438
+ oss_model = _load_sibling("oss_model.py", scripts.parent / "claude-harness" / "scripts")
439
+ if oss_model is None:
440
+ return ("INFO", "keys: could not check")
441
+
442
+ env_dict = dict(env)
443
+ fake_home = env_dict.get(f"{ENV_PREFIX}_FAKE_HOME") or os.environ.get(f"{ENV_PREFIX}_FAKE_HOME")
444
+ if fake_home and "CADDIS_KEYS_FILE" not in env_dict:
445
+ env_dict["CADDIS_KEYS_FILE"] = str(Path(fake_home) / ".caddis" / "keys.env")
446
+
447
+ providers = oss_model.configured_providers(env_dict)
448
+ if providers:
449
+ return ("OK", f"keys: {', '.join(providers)}")
450
+ return ("INFO", f"keys: none — {FIX_HINTS['keys-none']}")
451
+ except Exception:
452
+ return ("INFO", "keys: could not check")
453
+
454
+
455
+ # ── project checks (read-only — WARN or INFO, never FAIL) ─────────────────────
456
+ def deny_rule_findings(dest: Path) -> list[tuple[str, str]]:
457
+ dest = Path(dest)
458
+ if _is_caddis_source(dest):
459
+ return []
460
+
461
+ scripts = Path(__file__).resolve().parent
462
+ tmpl_path = scripts.parent / "settings.template.json"
463
+ if not tmpl_path.is_file():
464
+ tmpl_path = scripts.parent / "claude-harness" / "settings.template.json"
465
+ if not tmpl_path.is_file():
466
+ return []
467
+
468
+ settings_path = dest / ".claude" / "settings.json"
469
+ if not settings_path.is_file():
470
+ return [("INFO", "no .claude/settings.json")]
471
+
472
+ try:
473
+ tmpl = json.loads(tmpl_path.read_text(encoding="utf-8"))
474
+ required_deny = tmpl.get("permissions", {}).get("deny", [])
475
+ except Exception:
476
+ return []
477
+
478
+ try:
479
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
480
+ dest_deny = set(settings.get("permissions", {}).get("deny", []))
481
+ except Exception:
482
+ dest_deny = set()
483
+
484
+ missing = [r for r in required_deny if r not in dest_deny]
485
+ if missing:
486
+ return [("WARN", f"deny rules: {len(missing)} missing — {FIX_HINTS['deny-rules']}")]
487
+ return []
488
+
489
+
490
+ def gitignore_findings(dest: Path) -> list[tuple[str, str]]:
491
+ dest = Path(dest)
492
+ if _is_caddis_source(dest):
493
+ return []
494
+ if not (dest / ".caddis").is_dir():
495
+ return []
496
+
497
+ scripts = Path(__file__).resolve().parent
498
+ try:
499
+ setup_mod = _load_sibling("setup_project_ai.py", scripts.parent / "scripts")
500
+ if setup_mod is None:
501
+ return [("INFO", "gitignore: could not check")]
502
+ artifact_gitignore = getattr(setup_mod, "ARTIFACT_GITIGNORE", None)
503
+ if not artifact_gitignore:
504
+ return [("INFO", "gitignore: could not check")]
505
+
506
+ entries = [
507
+ line.strip()
508
+ for line in artifact_gitignore.splitlines()
509
+ if line.strip() and not line.strip().startswith("#")
510
+ ]
511
+
512
+ caddis_ignore = dest / ".caddis" / ".gitignore"
513
+ caddis_lines: set[str] = set()
514
+ if caddis_ignore.is_file():
515
+ try:
516
+ caddis_lines = {line.strip() for line in caddis_ignore.read_text(encoding="utf-8", errors="replace").splitlines()}
517
+ except OSError:
518
+ pass
519
+
520
+ root_ignore = dest / ".gitignore"
521
+ root_lines: set[str] = set()
522
+ if root_ignore.is_file():
523
+ try:
524
+ root_lines = {line.strip() for line in root_ignore.read_text(encoding="utf-8", errors="replace").splitlines()}
525
+ except OSError:
526
+ pass
527
+
528
+ missing: list[str] = []
529
+ for entry in entries:
530
+ if entry in caddis_lines or f".caddis/{entry}" in root_lines:
531
+ continue
532
+ missing.append(entry)
533
+
534
+ if missing:
535
+ first_three = ", ".join(missing[:3])
536
+ return [("WARN", f"gitignore: {len(missing)} missing ({first_three}) — {FIX_HINTS['gitignore']}")]
537
+ return []
538
+ except Exception:
539
+ return [("INFO", "gitignore: could not check")]
540
+
541
+
542
+ def misfiled_findings(dest: Path) -> list[tuple[str, str]]:
543
+ dest = Path(dest)
544
+ if _is_caddis_source(dest):
545
+ return []
546
+
547
+ caddis_dir = dest / ".caddis"
548
+ if not caddis_dir.is_dir():
549
+ return []
550
+
551
+ out: list[tuple[str, str]] = []
552
+ try:
553
+ subdirs = sorted(p for p in caddis_dir.iterdir() if p.is_dir())
554
+ except OSError:
555
+ return []
556
+
557
+ for top_dir in subdirs:
558
+ if top_dir.name in _SKIP_DIRS:
559
+ continue
560
+ if top_dir.name not in KNOWN_CADDIS_DIRS:
561
+ md_files = list(top_dir.rglob("*.md"))
562
+ if md_files:
563
+ out.append(("WARN", f".caddis/{top_dir.name}/ is not a caddis folder ({len(md_files)} file(s)) — {FIX_HINTS['misfiled']}"))
564
+ return out
565
+
566
+
345
567
  # ── report ───────────────────────────────────────────────────────────────────
346
568
  def run(dest: Path, quiet: bool) -> int:
347
569
  if quiet: # SessionStart mode: only the nudge, never non-zero
@@ -352,7 +574,7 @@ def run(dest: Path, quiet: bool) -> int:
352
574
 
353
575
  hard = 0
354
576
  print(f"=== caddis doctor — {dest} ===")
355
- print(f"python: {sys.version.split()[0]} ({sys.executable})")
577
+ print(f"python (running this check): {sys.version.split()[0]} ({sys.executable})")
356
578
  for name in ("codex", "agy", "claude"):
357
579
  path = _which(name)
358
580
  if path:
@@ -361,6 +583,26 @@ def run(dest: Path, quiet: bool) -> int:
361
583
  else:
362
584
  print(f" {name:7} not found on PATH (install, or open a new terminal so PATH refreshes)")
363
585
 
586
+ print("-- machine")
587
+ for level, text in machine_findings():
588
+ if level == "FAIL":
589
+ hard = 1
590
+ print(f" {level:4} {text}")
591
+ k_level, k_text = keys_line(os.environ)
592
+ if k_level == "FAIL":
593
+ hard = 1
594
+ print(f" {k_level:4} {k_text}")
595
+
596
+ print("-- project")
597
+ proj_findings = deny_rule_findings(dest) + gitignore_findings(dest) + misfiled_findings(dest)
598
+ if proj_findings:
599
+ for level, text in proj_findings:
600
+ if level == "FAIL":
601
+ hard = 1
602
+ print(f" {level:4} {text}")
603
+ else:
604
+ print(" OK deny rules, gitignore entries and document folders clean")
605
+
364
606
  print("-- rules integrity")
365
607
  findings = rules_findings(dest)
366
608
  if findings:
@@ -0,0 +1,37 @@
1
+ {
2
+ "$comment": "Base harness settings deployed by setup-project-ai: permissions + a status line. The generator adds stack-specific Bash allow entries (pytest/npm/etc.) based on detection. HOOKS ARE NOT DEFINED HERE: the harness loop hooks (relay.md resume on SessionStart/PreCompact, end-of-session nudge + usage digest on Stop, auto-lint on Edit/Write) ship with the caddis plugin via plugin/hooks/hooks.json and auto-activate on install — defining them here too would double-fire and re-introduce machine-specific paths. The default loop is just a plan file + relay.md; there is no opt-in pipeline/MCP power-mode.",
3
+ "$comment_permissions": "IMPORTANT: any entry in `ask` overrides bypassPermissions (yolo) mode and forces a prompt every time, even when the user has opted into no-prompts. Keep `ask` empty (or as minimal as possible) so bypassPermissions actually bypasses. `Bash(git push:*)` was moved out of `ask` into `allow` for exactly this reason — it was forcing a push prompt on every /ship under bypassPermissions.",
4
+ "permissions": {
5
+ "allow": [
6
+ "Read",
7
+ "Edit",
8
+ "Write",
9
+ "Glob",
10
+ "Grep",
11
+ "Bash(git status)",
12
+ "Bash(git diff:*)",
13
+ "Bash(git add:*)",
14
+ "Bash(git commit:*)",
15
+ "Bash(git log:*)",
16
+ "Bash(git push:*)"
17
+ ],
18
+ "ask": [],
19
+ "deny": [
20
+ "Read(**/.env)",
21
+ "Read(**/.env.local)",
22
+ "Read(**/.env.*.local)",
23
+ "Read(**/.env.production)",
24
+ "Read(**/.env.development)",
25
+ "Edit(**/.env)",
26
+ "Edit(**/.env.local)",
27
+ "Edit(**/.env.*.local)",
28
+ "Edit(**/.env.production)",
29
+ "Edit(**/.env.development)",
30
+ "Write(**/.env)",
31
+ "Write(**/.env.local)",
32
+ "Write(**/.env.*.local)",
33
+ "Write(**/.env.production)",
34
+ "Write(**/.env.development)"
35
+ ]
36
+ }
37
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
- "poolVersion": "1.3.126",
2
+ "poolVersion": "1.3.127",
3
3
  "bundles": {
4
- "antigravity-plugin": "1.3.126",
4
+ "antigravity-plugin": "1.3.127",
5
5
  "antigravity-plugin-extras": "1.3.40",
6
6
  "codex": "unknown"
7
7
  }
package/dist/cli.js CHANGED
@@ -1209,7 +1209,7 @@ var require_command = __commonJS({
1209
1209
  "use strict";
1210
1210
  var EventEmitter = __require("events").EventEmitter;
1211
1211
  var childProcess = __require("child_process");
1212
- var path5 = __require("path");
1212
+ var path6 = __require("path");
1213
1213
  var fs = __require("fs");
1214
1214
  var process3 = __require("process");
1215
1215
  var { Argument: Argument2, humanReadableArgName } = require_argument();
@@ -2222,9 +2222,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2222
2222
  let launchWithNode = false;
2223
2223
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
2224
2224
  function findFile(baseDir, baseName) {
2225
- const localBin = path5.resolve(baseDir, baseName);
2225
+ const localBin = path6.resolve(baseDir, baseName);
2226
2226
  if (fs.existsSync(localBin)) return localBin;
2227
- if (sourceExt.includes(path5.extname(baseName))) return void 0;
2227
+ if (sourceExt.includes(path6.extname(baseName))) return void 0;
2228
2228
  const foundExt = sourceExt.find(
2229
2229
  (ext) => fs.existsSync(`${localBin}${ext}`)
2230
2230
  );
@@ -2242,17 +2242,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
2242
2242
  } catch {
2243
2243
  resolvedScriptPath = this._scriptPath;
2244
2244
  }
2245
- executableDir = path5.resolve(
2246
- path5.dirname(resolvedScriptPath),
2245
+ executableDir = path6.resolve(
2246
+ path6.dirname(resolvedScriptPath),
2247
2247
  executableDir
2248
2248
  );
2249
2249
  }
2250
2250
  if (executableDir) {
2251
2251
  let localFile = findFile(executableDir, executableFile);
2252
2252
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
2253
- const legacyName = path5.basename(
2253
+ const legacyName = path6.basename(
2254
2254
  this._scriptPath,
2255
- path5.extname(this._scriptPath)
2255
+ path6.extname(this._scriptPath)
2256
2256
  );
2257
2257
  if (legacyName !== this._name) {
2258
2258
  localFile = findFile(
@@ -2263,7 +2263,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2263
2263
  }
2264
2264
  executableFile = localFile || executableFile;
2265
2265
  }
2266
- launchWithNode = sourceExt.includes(path5.extname(executableFile));
2266
+ launchWithNode = sourceExt.includes(path6.extname(executableFile));
2267
2267
  let proc;
2268
2268
  if (process3.platform !== "win32") {
2269
2269
  if (launchWithNode) {
@@ -3178,7 +3178,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3178
3178
  * @return {Command}
3179
3179
  */
3180
3180
  nameFromFilename(filename) {
3181
- this._name = path5.basename(filename, path5.extname(filename));
3181
+ this._name = path6.basename(filename, path6.extname(filename));
3182
3182
  return this;
3183
3183
  }
3184
3184
  /**
@@ -3192,9 +3192,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3192
3192
  * @param {string} [path]
3193
3193
  * @return {(string|null|Command)}
3194
3194
  */
3195
- executableDir(path6) {
3196
- if (path6 === void 0) return this._executableDir;
3197
- this._executableDir = path6;
3195
+ executableDir(path7) {
3196
+ if (path7 === void 0) return this._executableDir;
3197
+ this._executableDir = path7;
3198
3198
  return this;
3199
3199
  }
3200
3200
  /**
@@ -3731,6 +3731,23 @@ var require_lib = __commonJS({
3731
3731
  }
3732
3732
  });
3733
3733
 
3734
+ // src/util/which.ts
3735
+ async function findBin(name) {
3736
+ try {
3737
+ const found = await (0, import_which.default)(name, { nothrow: true });
3738
+ return found ?? null;
3739
+ } catch {
3740
+ return null;
3741
+ }
3742
+ }
3743
+ var import_which;
3744
+ var init_which = __esm({
3745
+ "src/util/which.ts"() {
3746
+ "use strict";
3747
+ import_which = __toESM(require_lib(), 1);
3748
+ }
3749
+ });
3750
+
3734
3751
  // src/util/pkg.ts
3735
3752
  import { readFileSync, existsSync } from "fs";
3736
3753
  import { fileURLToPath } from "url";
@@ -4843,7 +4860,7 @@ var init_init = __esm({
4843
4860
  });
4844
4861
 
4845
4862
  // src/providers/resolve.ts
4846
- import { readFileSync as readFileSync4 } from "fs";
4863
+ import { readFileSync as readFileSync3 } from "fs";
4847
4864
  import { homedir } from "os";
4848
4865
  import { isAbsolute, join } from "path";
4849
4866
  function expandHome(p2) {
@@ -4856,10 +4873,10 @@ function keysFilePath(env) {
4856
4873
  const expanded = expandHome(configured);
4857
4874
  return isAbsolute(expanded) ? expanded : expanded;
4858
4875
  }
4859
- function parseKeysFile(path5) {
4876
+ function parseKeysFile(path6) {
4860
4877
  let text;
4861
4878
  try {
4862
- text = readFileSync4(path5, "utf8");
4879
+ text = readFileSync3(path6, "utf8");
4863
4880
  } catch {
4864
4881
  return {};
4865
4882
  }
@@ -4917,7 +4934,7 @@ __export(keys_exports, {
4917
4934
  probeKey: () => probeKey,
4918
4935
  upsertKeyLines: () => upsertKeyLines
4919
4936
  });
4920
- import { chmodSync, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
4937
+ import { chmodSync, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
4921
4938
  import { dirname } from "path";
4922
4939
  async function probeKey(baseUrl, model, apiKey) {
4923
4940
  if (!baseUrl) return { ok: false, rejected: false, detail: "no endpoint for this provider" };
@@ -4971,18 +4988,18 @@ function upsertKeyLines(existing, updates) {
4971
4988
  if (!text.endsWith("\n")) text += "\n";
4972
4989
  return text;
4973
4990
  }
4974
- function restrictPermissions(path5) {
4991
+ function restrictPermissions(path6) {
4975
4992
  if (process.platform === "win32") return null;
4976
4993
  try {
4977
- chmodSync(path5, 384);
4994
+ chmodSync(path6, 384);
4978
4995
  return null;
4979
4996
  } catch (error) {
4980
4997
  return error instanceof Error ? error.message : String(error);
4981
4998
  }
4982
4999
  }
4983
5000
  async function keys(options = {}) {
4984
- const path5 = keysFilePath(process.env);
4985
- const fileKeys = parseKeysFile(path5);
5001
+ const path6 = keysFilePath(process.env);
5002
+ const fileKeys = parseKeysFile(path6);
4986
5003
  const names = Object.keys(PROVIDERS).sort();
4987
5004
  const current = names.map((name) => {
4988
5005
  const preset = PROVIDERS[name];
@@ -4998,7 +5015,7 @@ async function keys(options = {}) {
4998
5015
  if (options.check) {
4999
5016
  const configured = current.filter((entry) => entry.value);
5000
5017
  if (!configured.length) {
5001
- process.stdout.write(`no provider keys configured (looked in $ENV and ${path5})
5018
+ process.stdout.write(`no provider keys configured (looked in $ENV and ${path6})
5002
5019
  `);
5003
5020
  return 1;
5004
5021
  }
@@ -5013,7 +5030,7 @@ async function keys(options = {}) {
5013
5030
  return bad ? 1 : 0;
5014
5031
  }
5015
5032
  Ie("caddis keys");
5016
- process.stdout.write(` keys file: ${path5}
5033
+ process.stdout.write(` keys file: ${path6}
5017
5034
 
5018
5035
  `);
5019
5036
  for (const entry of current) {
@@ -5073,20 +5090,20 @@ async function keys(options = {}) {
5073
5090
  return 0;
5074
5091
  }
5075
5092
  if (options.dryRun) {
5076
- Se(`dry run \u2014 would write ${Object.keys(collected).join(", ")} to ${path5}`);
5093
+ Se(`dry run \u2014 would write ${Object.keys(collected).join(", ")} to ${path6}`);
5077
5094
  return 0;
5078
5095
  }
5079
5096
  let existing = "";
5080
5097
  try {
5081
- existing = readFileSync5(path5, "utf8");
5098
+ existing = readFileSync4(path6, "utf8");
5082
5099
  } catch {
5083
5100
  existing = "";
5084
5101
  }
5085
- mkdirSync2(dirname(path5), { recursive: true });
5086
- writeFileSync2(path5, upsertKeyLines(existing, collected), { encoding: "utf8" });
5087
- const permWarning = restrictPermissions(path5);
5102
+ mkdirSync(dirname(path6), { recursive: true });
5103
+ writeFileSync(path6, upsertKeyLines(existing, collected), { encoding: "utf8" });
5104
+ const permWarning = restrictPermissions(path6);
5088
5105
  process.stdout.write(`
5089
- wrote ${Object.keys(collected).join(", ")} to ${path5}
5106
+ wrote ${Object.keys(collected).join(", ")} to ${path6}
5090
5107
  `);
5091
5108
  if (permWarning) process.stdout.write(color.yellow(` could not set 0600 on the keys file: ${permWarning}
5092
5109
  `));
@@ -5160,18 +5177,22 @@ var doctor_exports = {};
5160
5177
  __export(doctor_exports, {
5161
5178
  doctor: () => doctor
5162
5179
  });
5180
+ import { existsSync as existsSync5 } from "fs";
5163
5181
  import os3 from "os";
5164
5182
  import process2 from "process";
5183
+ import path5 from "path";
5165
5184
  async function doctor(options) {
5166
5185
  const report = await gather(options.adapters, { checkCliUpdate: true });
5167
5186
  const findings = [];
5168
5187
  if (options.json) {
5169
- line(JSON.stringify(toJson(report), null, 2));
5188
+ const project = await renderProject(findings, { quiet: true });
5189
+ line(JSON.stringify(toJson(report, project), null, 2));
5170
5190
  return 0;
5171
5191
  }
5172
5192
  renderBanner({ cliVersion: report.cliVersion, poolVersion: report.poolVersion });
5173
5193
  renderEnvironment(report, findings);
5174
5194
  renderAgents(report, findings);
5195
+ await renderProject(findings);
5175
5196
  renderSummary(report, findings);
5176
5197
  return options.strict && findings.some((f) => f.kind === "problem") ? 1 : 0;
5177
5198
  }
@@ -5236,6 +5257,57 @@ function renderAgents(report, problems) {
5236
5257
  }
5237
5258
  }
5238
5259
  }
5260
+ async function renderProject(problems, options) {
5261
+ const quiet = options?.quiet ?? false;
5262
+ const python = await findBin("python");
5263
+ if (!python) {
5264
+ if (!quiet) {
5265
+ heading("Project");
5266
+ item("fail", "python not on PATH");
5267
+ }
5268
+ problems.push({
5269
+ kind: "problem",
5270
+ text: "python not on PATH",
5271
+ fix: "install Python 3.11 or later (see docs/ONBOARDING.md)"
5272
+ });
5273
+ return { ran: false, exitCode: null };
5274
+ }
5275
+ const bundle = bundlePath("antigravity-plugin", { requireManifest: false });
5276
+ const script = bundle ? path5.join(bundle, "scripts", "claudster_doctor.py") : null;
5277
+ if (!script || !existsSync5(script)) {
5278
+ if (!quiet) {
5279
+ heading("Project");
5280
+ item("fail", "claudster_doctor.py missing");
5281
+ }
5282
+ problems.push({
5283
+ kind: "problem",
5284
+ text: "claudster_doctor.py missing from antigravity-plugin bundle",
5285
+ fix: "reinstall: npm i -g @caddis/cli"
5286
+ });
5287
+ return { ran: false, exitCode: null };
5288
+ }
5289
+ if (!quiet) {
5290
+ heading("Project");
5291
+ }
5292
+ const result = await run(python, [script, "--dest", process2.cwd()], { timeout: 6e4 });
5293
+ if (!quiet && result.stdout) {
5294
+ const lines = result.stdout.replace(/\r\n/g, "\n").split("\n");
5295
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
5296
+ lines.pop();
5297
+ }
5298
+ for (const l2 of lines) {
5299
+ line(l2 ? ` ${l2}` : "");
5300
+ }
5301
+ }
5302
+ if (result.code === 1 || !result.ok) {
5303
+ problems.push({
5304
+ kind: "problem",
5305
+ text: "project health check failed",
5306
+ fix: "read the FAIL lines above"
5307
+ });
5308
+ }
5309
+ return { ran: true, exitCode: result.code };
5310
+ }
5239
5311
  function renderSummary(report, findings) {
5240
5312
  heading("Summary");
5241
5313
  const counts = report.agents.reduce(
@@ -5368,7 +5440,7 @@ function problemFor(entry, poolVersion) {
5368
5440
  return null;
5369
5441
  }
5370
5442
  }
5371
- function toJson(report) {
5443
+ function toJson(report, project) {
5372
5444
  return {
5373
5445
  cliVersion: report.cliVersion,
5374
5446
  cliUpdate: report.cliUpdate ?? null,
@@ -5389,7 +5461,8 @@ function toJson(report) {
5389
5461
  extrasVersion: entry.status.extras?.version ?? null,
5390
5462
  extrasDrift: entry.extrasDrift ?? null,
5391
5463
  note: entry.status.note ?? entry.detection.note ?? null
5392
- }))
5464
+ })),
5465
+ project: project ?? { ran: false, exitCode: null }
5393
5466
  };
5394
5467
  }
5395
5468
  var REQUIRED_NODE_MAJOR, REQUIRED_NODE_MINOR;
@@ -5397,7 +5470,9 @@ var init_doctor = __esm({
5397
5470
  "src/commands/doctor.ts"() {
5398
5471
  "use strict";
5399
5472
  init_log();
5473
+ init_exec();
5400
5474
  init_pkg();
5475
+ init_which();
5401
5476
  init_report();
5402
5477
  REQUIRED_NODE_MAJOR = 20;
5403
5478
  REQUIRED_NODE_MINOR = 19;
@@ -5544,19 +5619,7 @@ var {
5544
5619
 
5545
5620
  // src/agents/claude.ts
5546
5621
  init_exec();
5547
-
5548
- // src/util/which.ts
5549
- var import_which = __toESM(require_lib(), 1);
5550
- async function findBin(name) {
5551
- try {
5552
- const found = await (0, import_which.default)(name, { nothrow: true });
5553
- return found ?? null;
5554
- } catch {
5555
- return null;
5556
- }
5557
- }
5558
-
5559
- // src/agents/claude.ts
5622
+ init_which();
5560
5623
  var BIN = "claude";
5561
5624
  var MARKETPLACE = "caddis";
5562
5625
  var PLUGIN = "caddis@caddis";
@@ -5592,14 +5655,14 @@ function* pluginBlocks(stdout) {
5592
5655
  if (current) yield current;
5593
5656
  }
5594
5657
  async function detect() {
5595
- const path5 = await findBin(BIN);
5596
- if (!path5) {
5658
+ const path6 = await findBin(BIN);
5659
+ if (!path6) {
5597
5660
  return { present: false, note: "no `claude` binary on PATH" };
5598
5661
  }
5599
5662
  const version = await run(BIN, ["--version"], { timeout: 2e4 });
5600
5663
  return {
5601
5664
  present: true,
5602
- path: path5,
5665
+ path: path6,
5603
5666
  agentVersion: version.ok ? version.stdout.trim().split(/\s+/)[0] || void 0 : void 0,
5604
5667
  note: version.ok ? void 0 : "binary found but `claude --version` failed"
5605
5668
  };
@@ -5630,6 +5693,7 @@ async function status() {
5630
5693
  function steps(action) {
5631
5694
  if (action === "install") {
5632
5695
  return [
5696
+ { cmd: BIN, args: ["plugin", "marketplace", "add", "saajunaid/caddis-plugin"] },
5633
5697
  { cmd: BIN, args: ["plugin", "marketplace", "update", MARKETPLACE] },
5634
5698
  { cmd: BIN, args: ["plugin", "install", PLUGIN] }
5635
5699
  ];
@@ -5657,13 +5721,25 @@ async function drive(action, options) {
5657
5721
  for (const step of planned) {
5658
5722
  const command = formatCommand(step.cmd, step.args);
5659
5723
  const result = await run(step.cmd, step.args);
5724
+ const isMarketplaceAdd = step.args[1] === "marketplace" && step.args[2] === "add";
5725
+ const isMarketplaceUpdate = step.args[1] === "marketplace" && step.args[2] === "update";
5726
+ let ok = result.ok;
5727
+ let code = result.code;
5728
+ if (!ok && isMarketplaceAdd) {
5729
+ const output = `${result.stdout}
5730
+ ${result.stderr}`;
5731
+ if (/already (exists|added|configured|installed|on disk)/i.test(output)) {
5732
+ ok = true;
5733
+ code = 0;
5734
+ }
5735
+ }
5660
5736
  results.push({
5661
5737
  command,
5662
- ok: result.ok,
5663
- code: result.code,
5664
- output: result.ok ? void 0 : tail(result.stderr || result.stdout || result.failure || "")
5738
+ ok,
5739
+ code,
5740
+ output: ok ? void 0 : tail(result.stderr || result.stdout || result.failure || "")
5665
5741
  });
5666
- if (!result.ok && step.args[1] !== "marketplace") {
5742
+ if (!ok && !isMarketplaceUpdate) {
5667
5743
  return { ok: false, skipped: false, steps: results, message: `\`${command}\` failed` };
5668
5744
  }
5669
5745
  }
@@ -5684,10 +5760,11 @@ var claudeAdapter = {
5684
5760
 
5685
5761
  // src/agents/agy.ts
5686
5762
  init_exec();
5763
+ init_which();
5764
+ init_pkg();
5687
5765
  import { readFileSync as readFileSync2, existsSync as existsSync2, mkdtempSync, cpSync, rmSync } from "fs";
5688
5766
  import os from "os";
5689
5767
  import path2 from "path";
5690
- init_pkg();
5691
5768
  var BIN2 = "agy";
5692
5769
  var BUNDLE = "antigravity-plugin";
5693
5770
  var PLUGIN2 = "caddis";
@@ -5815,24 +5892,24 @@ async function drive2(_action, options) {
5815
5892
  }
5816
5893
  const cleanups = [];
5817
5894
  try {
5818
- const steps2 = [];
5895
+ const steps3 = [];
5819
5896
  for (const step of planned) {
5820
5897
  const command = formatCommand(step.cmd, step.args);
5821
5898
  const staged = stageForAgy(step.bundleDir);
5822
5899
  cleanups.push(staged.cleanup);
5823
5900
  const execArgs = [...step.args.slice(0, -1), staged.installPath];
5824
5901
  const result = await run(step.cmd, execArgs);
5825
- steps2.push({
5902
+ steps3.push({
5826
5903
  command,
5827
5904
  ok: result.ok,
5828
5905
  code: result.code,
5829
5906
  output: result.ok ? void 0 : (result.stderr || result.stdout || result.failure || "").trim().split(/\r?\n/).slice(-3).join("\n")
5830
5907
  });
5831
5908
  if (!result.ok) {
5832
- return { ok: false, skipped: false, steps: steps2, message: `\`${command}\` failed` };
5909
+ return { ok: false, skipped: false, steps: steps3, message: `\`${command}\` failed` };
5833
5910
  }
5834
5911
  }
5835
- return { ok: true, skipped: false, steps: steps2 };
5912
+ return { ok: true, skipped: false, steps: steps3 };
5836
5913
  } finally {
5837
5914
  cleanups.forEach((fn) => fn());
5838
5915
  }
@@ -5849,12 +5926,14 @@ var agyAdapter = {
5849
5926
 
5850
5927
  // src/agents/codex.ts
5851
5928
  init_exec();
5852
- import { cpSync as cpSync2, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync } from "fs";
5929
+ init_which();
5930
+ import { existsSync as existsSync3, rmSync as rmSync2 } from "fs";
5853
5931
  import os2 from "os";
5854
5932
  import path3 from "path";
5855
- init_pkg();
5856
5933
  var BIN3 = "codex";
5857
- var BUNDLE2 = "codex";
5934
+ var MARKETPLACE_URL = "https://github.com/saajunaid/caddis-plugin";
5935
+ var MARKETPLACE2 = "caddis";
5936
+ var PLUGIN3 = "caddis-codex@caddis";
5858
5937
  function caddisSkillsDir(home = os2.homedir()) {
5859
5938
  return path3.join(home, ".codex", "skills", "caddis");
5860
5939
  }
@@ -5874,7 +5953,6 @@ async function detect3() {
5874
5953
  return {
5875
5954
  present: true,
5876
5955
  path: binPath,
5877
- // `codex --version` prints "codex-cli 0.150.1"; take the trailing token.
5878
5956
  agentVersion: version.ok ? version.stdout.trim().split(/\s+/).pop() || void 0 : void 0,
5879
5957
  note: version.ok ? void 0 : "binary found but `codex --version` failed"
5880
5958
  };
@@ -5883,104 +5961,146 @@ function shortenHome2(target) {
5883
5961
  const home = os2.homedir();
5884
5962
  return target.startsWith(home) ? path3.join("~", target.slice(home.length)) : target;
5885
5963
  }
5886
- function statusFromHome2(home) {
5887
- const marker = versionFile(home);
5888
- const dir = caddisSkillsDir(home);
5889
- if (existsSync3(marker)) {
5890
- try {
5891
- const version = readFileSync3(marker, "utf8").trim();
5892
- if (version) return { installed: true, version, source: shortenHome2(marker) };
5893
- } catch {
5964
+ function parseCodexPluginList(stdout, plugin = PLUGIN3) {
5965
+ const wanted = plugin.toLowerCase();
5966
+ for (const rawLine of stdout.split(/\r?\n/)) {
5967
+ const line2 = rawLine.trim();
5968
+ if (!line2.toLowerCase().startsWith(wanted)) continue;
5969
+ const rest = line2.slice(wanted.length).trim();
5970
+ if (/^not installed/i.test(rest)) {
5971
+ return { installed: false };
5972
+ }
5973
+ const match = rest.match(/^installed,\s*(enabled|disabled)(?:\s+([^\s]+))?/i);
5974
+ if (match) {
5975
+ return {
5976
+ installed: true,
5977
+ disabled: match[1]?.toLowerCase() === "disabled",
5978
+ version: match[2]
5979
+ };
5980
+ }
5981
+ const simpleMatch = rest.match(/^installed(?:\s+([^\s]+))?/i);
5982
+ if (simpleMatch) {
5983
+ return {
5984
+ installed: true,
5985
+ disabled: false,
5986
+ version: simpleMatch[1]
5987
+ };
5894
5988
  }
5895
- return { installed: true, source: shortenHome2(marker), note: "version marker present but unreadable" };
5896
- }
5897
- if (existsSync3(dir)) {
5898
- return {
5899
- installed: true,
5900
- source: shortenHome2(dir),
5901
- note: "skills present but no version marker \u2014 installed by hand, or by a caddis older than this one"
5902
- };
5903
5989
  }
5904
- return { installed: false, source: shortenHome2(dir), note: "caddis skills not installed into codex" };
5990
+ return null;
5905
5991
  }
5906
5992
  async function status3() {
5907
5993
  const detected = await detect3();
5908
5994
  if (!detected.present) return { installed: false, note: "agent not installed" };
5909
- return statusFromHome2(os2.homedir());
5995
+ const listed = await run(BIN3, ["plugin", "list"], { timeout: 6e4 });
5996
+ if (!listed.ok) {
5997
+ return {
5998
+ installed: false,
5999
+ source: "`codex plugin list`",
6000
+ note: `could not read plugin list (${listed.failure ?? `exit ${listed.code}`})`
6001
+ };
6002
+ }
6003
+ const entry = parseCodexPluginList(listed.stdout);
6004
+ if (!entry || !entry.installed) {
6005
+ return { installed: false, source: "`codex plugin list`", note: "caddis-codex plugin not installed" };
6006
+ }
6007
+ return {
6008
+ installed: true,
6009
+ version: entry.version,
6010
+ disabled: entry.disabled,
6011
+ source: "`codex plugin list`",
6012
+ note: entry.version ? void 0 : "installed but the version is not reported"
6013
+ };
6014
+ }
6015
+ function steps2(action) {
6016
+ if (action === "install") {
6017
+ return [
6018
+ { cmd: BIN3, args: ["plugin", "marketplace", "add", MARKETPLACE_URL] },
6019
+ { cmd: BIN3, args: ["plugin", "add", PLUGIN3] }
6020
+ ];
6021
+ }
6022
+ return [
6023
+ { cmd: BIN3, args: ["plugin", "marketplace", "upgrade", MARKETPLACE2] },
6024
+ { cmd: BIN3, args: ["plugin", "add", PLUGIN3] }
6025
+ ];
5910
6026
  }
5911
6027
  async function drive3(action, options) {
5912
6028
  const detected = await detect3();
5913
6029
  if (!detected.present) {
5914
6030
  return { ok: true, skipped: true, steps: [], message: "codex not installed \u2014 skipped" };
5915
6031
  }
5916
- const bundle = bundlePath(BUNDLE2, { requireManifest: false });
5917
- if (!bundle) {
5918
- return {
5919
- ok: false,
5920
- skipped: false,
5921
- steps: [],
5922
- message: `the ${BUNDLE2} bundle is missing from this package \u2014 reinstall @caddis/cli`
5923
- };
5924
- }
5925
- const source = path3.join(bundle, ".codex", "skills");
5926
- if (!existsSync3(source)) {
5927
- return {
5928
- ok: false,
5929
- skipped: false,
5930
- steps: [],
5931
- message: `the ${BUNDLE2} bundle has no .codex/skills directory \u2014 the export shape changed`
5932
- };
5933
- }
5934
6032
  const home = os2.homedir();
5935
- const dest = caddisSkillsDir(home);
5936
- const version = bundleManifest().poolVersion || "unknown";
5937
- const label = `copy ${BUNDLE2} skills -> ${shortenHome2(dest)}`;
5938
- const steps2 = [];
6033
+ const legacyDir = caddisSkillsDir(home);
6034
+ const legacyMarker = versionFile(home);
6035
+ const hasLegacyMarker = existsSync3(legacyMarker);
6036
+ const planned = steps2(action);
6037
+ const removalCommand = `remove legacy ${shortenHome2(legacyDir)}`;
5939
6038
  if (options.dryRun) {
5940
- return {
5941
- ok: true,
5942
- skipped: true,
5943
- steps: [{ command: label, ok: true, code: 0 }],
5944
- message: `dry run \u2014 would ${action} caddis ${version} into ${shortenHome2(dest)}`
5945
- };
6039
+ const plannedSteps = planned.map((step) => ({ command: formatCommand(step.cmd, step.args), ok: true, code: null }));
6040
+ if (hasLegacyMarker) {
6041
+ plannedSteps.push({ command: removalCommand, ok: true, code: null });
6042
+ }
6043
+ return { ok: true, skipped: true, steps: plannedSteps, message: "dry run \u2014 nothing executed" };
5946
6044
  }
5947
- try {
5948
- if (existsSync3(dest)) rmSync2(dest, { recursive: true, force: true });
5949
- mkdirSync(path3.dirname(dest), { recursive: true });
5950
- cpSync2(source, dest, { recursive: true });
5951
- writeFileSync(versionFile(home), `${version}
5952
- `, "utf8");
5953
- steps2.push({ command: label, ok: true, code: 0 });
5954
- } catch (error) {
5955
- const message = error instanceof Error ? error.message : String(error);
5956
- steps2.push({ command: label, ok: false, code: null, output: message });
5957
- return { ok: false, skipped: false, steps: steps2, message: `could not install into codex: ${message}` };
6045
+ const results = [];
6046
+ for (const step of planned) {
6047
+ const command = formatCommand(step.cmd, step.args);
6048
+ const result = await run(step.cmd, step.args);
6049
+ const isMarketplaceAdd = step.args[1] === "marketplace" && step.args[2] === "add";
6050
+ const isMarketplaceUpgrade = step.args[1] === "marketplace" && step.args[2] === "upgrade";
6051
+ let ok = result.ok;
6052
+ let code = result.code;
6053
+ if (!ok && isMarketplaceAdd) {
6054
+ const output = `${result.stdout}
6055
+ ${result.stderr}`;
6056
+ if (/already (exists|added|configured|installed)/i.test(output)) {
6057
+ ok = true;
6058
+ code = 0;
6059
+ }
6060
+ }
6061
+ results.push({
6062
+ command,
6063
+ ok,
6064
+ code,
6065
+ output: ok ? void 0 : tail2(result.stderr || result.stdout || result.failure || "")
6066
+ });
6067
+ if (!ok && !isMarketplaceUpgrade) {
6068
+ return { ok: false, skipped: false, steps: results, message: `\`${command}\` failed` };
6069
+ }
5958
6070
  }
5959
- const notes = [];
5960
- for (const asset of ["AGENTS.md", path3.join(".codex", "config.toml.example")]) {
5961
- const from = path3.join(bundle, asset);
5962
- if (!existsSync3(from)) continue;
5963
- const to = path3.join(dest, `caddis-${path3.basename(asset)}`);
6071
+ if (hasLegacyMarker) {
5964
6072
  try {
5965
- cpSync2(from, to);
5966
- notes.push(path3.basename(to));
5967
- } catch {
6073
+ rmSync2(legacyDir, { recursive: true, force: true });
6074
+ results.push({ command: removalCommand, ok: true, code: 0 });
6075
+ } catch (error) {
6076
+ const message = error instanceof Error ? error.message : String(error);
6077
+ results.push({ command: removalCommand, ok: false, code: null, output: message });
5968
6078
  }
5969
6079
  }
5970
- const extrasNote = options.extras ? " (--extras has no codex bundle yet; core only)" : "";
5971
- const referenceNote = notes.length ? ` Reference copies beside it: ${notes.join(", ")} \u2014 config.toml is NEVER merged for you.` : "";
6080
+ const notes = [];
6081
+ if (!hasLegacyMarker && existsSync3(legacyDir)) {
6082
+ notes.push(
6083
+ `${shortenHome2(legacyDir)} exists but caddis did not write it (no version marker), so it was left in place; Codex may list the same skills twice. Remove it by hand if you do not need it.`
6084
+ );
6085
+ }
6086
+ if (options.extras) {
6087
+ notes.push("--extras has no Codex plugin; core only");
6088
+ }
5972
6089
  return {
5973
6090
  ok: true,
5974
6091
  skipped: false,
5975
- steps: steps2,
5976
- message: `caddis ${version} ${action === "install" ? "installed" : "updated"} in ${shortenHome2(dest)}.${referenceNote}${extrasNote}`
6092
+ steps: results,
6093
+ ...notes.length ? { message: notes.join(" ") } : {}
5977
6094
  };
5978
6095
  }
6096
+ function tail2(text, lines = 3) {
6097
+ return text.trim().split(/\r?\n/).slice(-lines).join("\n");
6098
+ }
5979
6099
  var codexAdapter = {
5980
6100
  id: "codex",
5981
6101
  name: "Codex",
5982
6102
  supported: true,
5983
- summary: "copy the caddis skill pool into ~/.codex/skills/caddis/ (config.toml never merged)",
6103
+ summary: "install the caddis-codex plugin from the caddis marketplace (config.toml never merged)",
5984
6104
  detect: detect3,
5985
6105
  drive: drive3,
5986
6106
  status: status3
@@ -5990,6 +6110,7 @@ var codexAdapter = {
5990
6110
  import path4 from "path";
5991
6111
 
5992
6112
  // src/agents/stub.ts
6113
+ init_which();
5993
6114
  import { existsSync as existsSync4 } from "fs";
5994
6115
  var NOT_YET_SUPPORTED = "not yet supported (v0.2)";
5995
6116
  function createStubAdapter(spec) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caddis/cli",
3
- "version": "0.4.39",
3
+ "version": "0.4.41",
4
4
  "description": "Install, update and diagnose the caddis dev harness across every coding agent you use (Claude Code, agy/Antigravity, and more).",
5
5
  "keywords": [
6
6
  "caddis",