codiedev 0.10.0-beta.4 → 0.10.0-beta.6

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.
Files changed (157) hide show
  1. package/README.md +11 -5
  2. package/assets/tui-brand-plugin.tsx +14 -2
  3. package/dist/backup/cache.js +1 -5
  4. package/dist/backup/match.js +0 -6
  5. package/dist/backup/run.js +3 -35
  6. package/dist/captureExclusions.js +0 -12
  7. package/dist/captureLog.js +0 -28
  8. package/dist/captureReadiness.js +0 -58
  9. package/dist/captureSignals.js +68 -3
  10. package/dist/cli.js +75 -64
  11. package/dist/codexHookTrust.js +125 -6
  12. package/dist/codexLimitScan.js +2 -109
  13. package/dist/codie.js +50 -114
  14. package/dist/codieBrandInstall.js +0 -18
  15. package/dist/codieLauncher.js +20 -92
  16. package/dist/codieRuntime.js +19 -23
  17. package/dist/codieRuntimeInstall.js +108 -23
  18. package/dist/codieUsage.js +38 -41
  19. package/dist/commands/agentTemplates.generated.js +136 -0
  20. package/dist/commands/agents.js +1066 -0
  21. package/dist/commands/agentsSdk.generated.js +7 -0
  22. package/dist/commands/ask.js +0 -3
  23. package/dist/commands/capture.js +0 -2
  24. package/dist/commands/createTicket.js +0 -10
  25. package/dist/commands/doctor.js +53 -120
  26. package/dist/commands/evals.js +187 -0
  27. package/dist/commands/inbox.js +0 -2
  28. package/dist/commands/library.js +0 -3
  29. package/dist/commands/pack.js +573 -0
  30. package/dist/commands/ping.js +0 -2
  31. package/dist/commands/pull.js +6 -10
  32. package/dist/commands/push.js +2 -12
  33. package/dist/commands/search.js +0 -4
  34. package/dist/commands/send.js +0 -5
  35. package/dist/commands/share.js +0 -4
  36. package/dist/commands/shared.js +0 -35
  37. package/dist/commands/traces.js +344 -0
  38. package/dist/commands/tracesLabel.js +348 -0
  39. package/dist/configDir.js +0 -10
  40. package/dist/connect.js +19 -116
  41. package/dist/connectFlow.js +0 -9
  42. package/dist/copilotHooks.js +391 -0
  43. package/dist/copilotInstallation.js +64 -0
  44. package/dist/cursorHooks.js +38 -30
  45. package/dist/cursorLifecycle.js +109 -0
  46. package/dist/detection.js +0 -30
  47. package/dist/harnessDirs.js +77 -0
  48. package/dist/hook.js +182 -469
  49. package/dist/inboxFormat.js +0 -24
  50. package/dist/launchContext.js +150 -0
  51. package/dist/launcher.js +140 -0
  52. package/dist/limitEvent.js +41 -35
  53. package/dist/limitEventReporter.js +1 -1
  54. package/dist/managedPi.js +240 -0
  55. package/dist/mcp.js +3 -384
  56. package/dist/nativeSteering.js +24 -0
  57. package/dist/packs/codie.js +182 -0
  58. package/dist/packs/harnesses.js +411 -0
  59. package/dist/packs/hookGate.js +256 -0
  60. package/dist/packs/install.js +286 -0
  61. package/dist/packs/manifest.js +126 -0
  62. package/dist/packs/paths.js +81 -0
  63. package/dist/packs/shim.js +214 -0
  64. package/dist/packs/store.js +155 -0
  65. package/dist/packs/sync.js +170 -0
  66. package/dist/piHooks.js +216 -0
  67. package/dist/piRuntime.js +120 -0
  68. package/dist/repoResolver.js +0 -19
  69. package/dist/secret.js +0 -3
  70. package/dist/selfUpdate.js +376 -0
  71. package/dist/skillMarker.js +23 -0
  72. package/dist/subagents.js +135 -0
  73. package/dist/teamSkillContext.js +10 -0
  74. package/dist/transcriptModels.js +6 -8
  75. package/dist/upgradeNudge.js +0 -30
  76. package/dist/utils.js +494 -824
  77. package/dist/version.js +0 -10
  78. package/dist/versionCheck.js +0 -33
  79. package/package.json +8 -9
  80. package/dist/backup/cache.d.ts +0 -14
  81. package/dist/backup/match.d.ts +0 -2
  82. package/dist/backup/run.d.ts +0 -25
  83. package/dist/captureExclusions.d.ts +0 -30
  84. package/dist/captureLog.d.ts +0 -20
  85. package/dist/captureReadiness.d.ts +0 -65
  86. package/dist/captureSignals.d.ts +0 -19
  87. package/dist/cli.d.ts +0 -16
  88. package/dist/codexHookTrust.d.ts +0 -34
  89. package/dist/codexLimitScan.d.ts +0 -64
  90. package/dist/codie.d.ts +0 -2
  91. package/dist/codieBrandInstall.d.ts +0 -11
  92. package/dist/codieLauncher.d.ts +0 -86
  93. package/dist/codieRuntime.d.ts +0 -29
  94. package/dist/codieRuntimeInstall.d.ts +0 -28
  95. package/dist/codieStartup.d.ts +0 -26
  96. package/dist/codieStartup.js +0 -30
  97. package/dist/codieUsage.d.ts +0 -57
  98. package/dist/commands/ask.d.ts +0 -1
  99. package/dist/commands/capture.d.ts +0 -14
  100. package/dist/commands/createTicket.d.ts +0 -27
  101. package/dist/commands/delete.d.ts +0 -1
  102. package/dist/commands/doctor.d.ts +0 -1
  103. package/dist/commands/inbox.d.ts +0 -3
  104. package/dist/commands/library.d.ts +0 -2
  105. package/dist/commands/note.d.ts +0 -1
  106. package/dist/commands/ping.d.ts +0 -1
  107. package/dist/commands/promote.d.ts +0 -1
  108. package/dist/commands/promote.js +0 -53
  109. package/dist/commands/pull.d.ts +0 -2
  110. package/dist/commands/push.d.ts +0 -33
  111. package/dist/commands/react.d.ts +0 -2
  112. package/dist/commands/react.js +0 -40
  113. package/dist/commands/reverseTicket.d.ts +0 -2
  114. package/dist/commands/reverseTicket.js +0 -477
  115. package/dist/commands/search.d.ts +0 -2
  116. package/dist/commands/security.d.ts +0 -1
  117. package/dist/commands/security.js +0 -156
  118. package/dist/commands/send.d.ts +0 -2
  119. package/dist/commands/share.d.ts +0 -2
  120. package/dist/commands/shared.d.ts +0 -64
  121. package/dist/configDir.d.ts +0 -1
  122. package/dist/connect.d.ts +0 -1
  123. package/dist/connectFlow.d.ts +0 -13
  124. package/dist/cursorHooks.d.ts +0 -23
  125. package/dist/detection.d.ts +0 -30
  126. package/dist/hook.d.ts +0 -6
  127. package/dist/inboxFormat.d.ts +0 -38
  128. package/dist/limitEvent.d.ts +0 -16
  129. package/dist/limitEventReporter.d.ts +0 -21
  130. package/dist/mcp.d.ts +0 -12
  131. package/dist/prUrl.d.ts +0 -9
  132. package/dist/prUrl.js +0 -33
  133. package/dist/repoResolver.d.ts +0 -17
  134. package/dist/secret.d.ts +0 -1
  135. package/dist/security.d.ts +0 -102
  136. package/dist/security.js +0 -437
  137. package/dist/securityDeepScan.d.ts +0 -41
  138. package/dist/securityDeepScan.js +0 -248
  139. package/dist/securityDoctor.d.ts +0 -32
  140. package/dist/securityDoctor.js +0 -63
  141. package/dist/securityFeature.d.ts +0 -5
  142. package/dist/securityFeature.js +0 -7
  143. package/dist/securityInstall.d.ts +0 -63
  144. package/dist/securityInstall.js +0 -239
  145. package/dist/securityInventory.d.ts +0 -54
  146. package/dist/securityInventory.js +0 -556
  147. package/dist/securityPilot.d.ts +0 -19
  148. package/dist/securityPilot.js +0 -61
  149. package/dist/securityPolicy.d.ts +0 -33
  150. package/dist/securityPolicy.js +0 -188
  151. package/dist/securitySession.d.ts +0 -6
  152. package/dist/securitySession.js +0 -72
  153. package/dist/transcriptModels.d.ts +0 -18
  154. package/dist/upgradeNudge.d.ts +0 -25
  155. package/dist/utils.d.ts +0 -346
  156. package/dist/version.d.ts +0 -1
  157. package/dist/versionCheck.d.ts +0 -68
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.jsPackage = exports.jsTypes = exports.jsIndex = exports.python = void 0;
4
+ exports.python = "\"\"\"Codiedev agents SDK — the run primitive, from Python.\n\nInside a deployed agent (everything is already configured):\n\n import codiedev_agents as agents\n\n task = agents.input() # this run's input (JSON)\n llm = agents.openai() # OpenAI client on the Codiedev gateway, traced\n child = agents.run(\"summarize\", {\"text\": ...}) # start another agent as a child run\n agents.output({\"summary\": child[\"output\"]}) # this run's result\n agents.request() # webhook runs: method, headers, raw body\n agents.respond({\"text\": \"on it\"}) # what a webhook caller waiting (?wait=) gets back\n\n agents.state.get(\"cursor\") # per-agent storage that outlives runs\n agents.state.set(\"cursor\", 42)\n agents.step(\"fetch\", lambda: fetch()) # done once per run, even across retries\n agents.start(\"triage\", {...}, delay=3600) # start a run later (up to 7 days)\n agents.request_approval(\"Refund $120?\", context={...}) # a person decides; a run acts on it\n agents.attempt() # 1, or 2+ when this run is a retry\n\nOutside a run (a script, a notebook, another service), set CODIEDEV_TOKEN\n(your CLI token) and CODIEDEV_AGENTS_URL, then call agents.run() the same way.\nStandard library only, so it drops into any project.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport time\nimport urllib.error\nimport urllib.parse\nimport urllib.request\nfrom typing import Any\n\n__all__ = [\n \"input\", \"output\", \"request\", \"respond\", \"run\", \"start\", \"get\", \"openai\", \"attempt\",\n \"state\", \"step\", \"request_approval\", \"get_approval\", \"AgentRunError\", \"StateConflict\",\n]\n\n_INPUT_PATH = os.environ.get(\"CODIEDEV_INPUT_PATH\", \"/agent/.codiedev/input.json\")\n_OUTPUT_PATH = os.environ.get(\"CODIEDEV_OUTPUT_PATH\", \"/agent/.codiedev/output.json\")\n_REQUEST_PATH = os.environ.get(\"CODIEDEV_REQUEST_PATH\", \"/agent/.codiedev/request.json\")\n_RESPONSE_PATH = os.environ.get(\"CODIEDEV_RESPONSE_PATH\", \"/agent/.codiedev/response.json\")\n# Set by `codiedev agents run --local`: state lives in this file instead of Codiedev.\n_STATE_PATH = os.environ.get(\"CODIEDEV_STATE_PATH\")\n_DONE = {\"succeeded\", \"failed\", \"cancelled\"}\n\n\nclass StateConflict(RuntimeError):\n \"\"\"state.set(..., if_version=n) found the key at another version.\"\"\"\n\n def __init__(self, key: str, version: int):\n super().__init__(f\"state {key!r} changed (now version {version})\")\n self.key = key\n self.version = version\n\n\nclass AgentRunError(RuntimeError):\n def __init__(self, run: dict):\n super().__init__(f\"{run.get('agent')} run {run.get('id')} {run.get('status')}: {run.get('error') or 'no error message'}\")\n self.run = run\n\n\ndef input(default: Any = None) -> Any: # noqa: A001 - mirrors output()\n \"\"\"This run's input, parsed from JSON (None when started without one).\"\"\"\n try:\n with open(_INPUT_PATH) as f:\n value = json.load(f)\n except FileNotFoundError:\n return default\n return default if value is None else value\n\n\ndef output(value: Any) -> None:\n \"\"\"Set this run's result. Must be JSON-serializable; the last call wins.\"\"\"\n os.makedirs(os.path.dirname(_OUTPUT_PATH), exist_ok=True)\n with open(_OUTPUT_PATH, \"w\") as f:\n json.dump(value, f)\n\n\ndef respond(body: Any = None, status: int = 200, headers: \"dict | None\" = None) -> None:\n \"\"\"What a webhook caller gets back when it waits for the run (?wait=N):\n a string is sent as text, anything else as JSON. Without respond(), a\n waiting caller gets the run's output.\"\"\"\n os.makedirs(os.path.dirname(_RESPONSE_PATH), exist_ok=True)\n with open(_RESPONSE_PATH, \"w\") as f:\n json.dump({\"status\": status, \"headers\": headers or {}, \"body\": body}, f)\n\n\ndef attempt() -> int:\n \"\"\"This run's attempt: 1, or 2+ when it's a retry of a failed attempt.\"\"\"\n return int(os.environ.get(\"CODIEDEV_ATTEMPT\", \"1\"))\n\n\ndef request() -> \"dict | None\":\n \"\"\"For a webhook run: {\"method\", \"query\", \"headers\", \"body\"} with the raw\n body, to verify the sender's signature. None for other runs.\"\"\"\n try:\n with open(_REQUEST_PATH) as f:\n return json.load(f)\n except FileNotFoundError:\n return None\n\n\ndef _base() -> str:\n url = os.environ.get(\"CODIEDEV_AGENTS_URL\", \"\").rstrip(\"/\")\n if not url:\n raise RuntimeError(\"Set CODIEDEV_AGENTS_URL (it's set for you inside a run)\")\n return url\n\n\ndef _token() -> str:\n token = os.environ.get(\"CODIEDEV_RUN_TOKEN\") or os.environ.get(\"CODIEDEV_TOKEN\")\n if not token:\n raise RuntimeError(\"Set CODIEDEV_TOKEN to your Codiedev CLI token (inside a run it's set for you)\")\n return token\n\n\ndef _request(method: str, path: str, body: dict | None = None, ok: tuple[int, ...] = ()) -> dict:\n req = urllib.request.Request(\n f\"{_base()}{path}\",\n method=method,\n data=json.dumps(body).encode() if body is not None else None,\n headers={\"Authorization\": f\"Bearer {_token()}\", \"Content-Type\": \"application/json\"},\n )\n try:\n with urllib.request.urlopen(req, timeout=30) as res:\n return json.loads(res.read() or b\"{}\")\n except urllib.error.HTTPError as err:\n detail = err.read().decode(errors=\"replace\")\n if err.code in ok:\n return {\"_status\": err.code, **json.loads(detail or \"{}\")}\n try:\n detail = json.loads(detail).get(\"error\", detail)\n except ValueError:\n pass\n raise RuntimeError(f\"Codiedev agents API {err.code}: {detail}\") from None\n\n\ndef start(\n agent: str,\n input: Any = None, # noqa: A002\n idempotency_key: str | None = None,\n delay: float | None = None,\n retries: int | None = None,\n) -> str:\n \"\"\"Start a run and return its id without waiting. With an idempotency key,\n a second start within a day returns the first run's id. delay (seconds,\n up to 7 days) starts it later; retries overrides the agent's retries.\"\"\"\n body: dict = {\"agent\": agent, \"input\": input}\n if idempotency_key:\n body[\"idempotencyKey\"] = idempotency_key\n if delay:\n body[\"delaySec\"] = int(delay)\n if retries is not None:\n body[\"retries\"] = retries\n return _request(\"POST\", \"/api/agents/v1/run\", body)[\"runId\"]\n\n\ndef get(run_id: str) -> dict:\n \"\"\"A run's status, input, output (parsed), error, log tail and children.\"\"\"\n run = _request(\"GET\", f\"/api/agents/v1/run?id={run_id}\")\n if isinstance(run.get(\"output\"), str):\n try:\n run[\"output\"] = json.loads(run[\"output\"])\n except ValueError:\n pass\n return run\n\n\ndef run(\n agent: str,\n input: Any = None, # noqa: A002\n timeout: float = 900,\n poll: float = 2.0,\n check: bool = True,\n idempotency_key: str | None = None,\n) -> dict:\n \"\"\"Run an agent and wait for it. Returns the run; its result is run[\"output\"].\n\n Inside a run this starts a child run, so the chain shows as one tree.\n Raises AgentRunError if it fails (pass check=False to get the run anyway).\n \"\"\"\n run_id = start(agent, input, idempotency_key)\n deadline = time.monotonic() + timeout\n while True:\n result = get(run_id)\n if result.get(\"status\") in _DONE:\n if check and result[\"status\"] != \"succeeded\":\n raise AgentRunError(result)\n return result\n if time.monotonic() > deadline:\n raise TimeoutError(f\"{agent} run {run_id} still {result.get('status')} after {timeout}s\")\n time.sleep(poll)\n\n\nclass _State:\n \"\"\"Per-agent key-value storage that outlives runs (values: any JSON, up\n to 64 KB; 1,000 keys). Every write bumps the key's version; pass\n if_version to write only if nobody else did since you read it.\"\"\"\n\n def get(self, key: str, default: Any = None) -> Any:\n value, _ = self.get_versioned(key)\n return default if value is None else value\n\n def get_versioned(self, key: str) -> \"tuple[Any, int]\":\n \"\"\"(value, version); (None, 0) when the key isn't set.\"\"\"\n if _STATE_PATH:\n row = _local_state().get(key)\n return (row[\"value\"], row[\"version\"]) if row else (None, 0)\n res = _request(\"GET\", f\"/api/agents/v1/state?{urllib.parse.urlencode({'key': key, 'agent': _agent()})}\", ok=(404,))\n if res.get(\"_status\") == 404:\n return None, 0\n return json.loads(res[\"value\"]), int(res[\"version\"])\n\n def set(self, key: str, value: Any, if_version: \"int | None\" = None, ttl: \"float | None\" = None) -> int:\n \"\"\"Store value; returns the new version. Raises StateConflict when\n if_version is given and the key is at another version (0 = unset).\"\"\"\n return self._write(key, value, if_version, ttl)\n\n def delete(self, key: str, if_version: \"int | None\" = None) -> None:\n self._write(key, None, if_version, None)\n\n def list(self, prefix: str = \"\", values: bool = False) -> \"list[dict]\": # noqa: A003\n \"\"\"Keys (and with values=True, values) starting with prefix.\"\"\"\n if _STATE_PATH:\n rows = sorted((k, r) for k, r in _local_state().items() if k.startswith(prefix))\n return [{\"key\": k, \"version\": r[\"version\"], **({\"value\": r[\"value\"]} if values else {})} for k, r in rows]\n keys: list = []\n after = None\n while True:\n params = {\"prefix\": prefix, \"values\": \"1\" if values else \"0\", \"limit\": 200, \"agent\": _agent()}\n if after:\n params[\"after\"] = after\n page = _request(\"GET\", f\"/api/agents/v1/state?{urllib.parse.urlencode(params)}\")\n for row in page[\"keys\"]:\n if \"value\" in row:\n row[\"value\"] = json.loads(row[\"value\"])\n keys.extend(page[\"keys\"])\n after = page.get(\"next\")\n if not after:\n return keys\n\n def update(self, key: str, fn, default: Any = None, retries: int = 10) -> Any:\n \"\"\"Read-modify-write that's safe with runs in parallel: fn(old) -> new,\n retried if another run wrote first. Returns the new value.\"\"\"\n for _ in range(retries):\n value, version = self.get_versioned(key)\n new = fn(default if value is None else value)\n try:\n self.set(key, new, if_version=version)\n return new\n except StateConflict:\n time.sleep(0.05)\n raise StateConflict(key, -1)\n\n def _write(self, key: str, value: Any, if_version: \"int | None\", ttl: \"float | None\") -> int:\n if _STATE_PATH:\n data = _local_state()\n current = data.get(key, {}).get(\"version\", 0)\n if if_version is not None and if_version != current:\n raise StateConflict(key, current)\n if value is None:\n data.pop(key, None)\n else:\n data[key] = {\"value\": value, \"version\": current + 1}\n with open(_STATE_PATH, \"w\") as f:\n json.dump(data, f, indent=2)\n return 0 if value is None else current + 1\n body: dict = {\"key\": key, \"value\": value, \"agent\": _agent()}\n if if_version is not None:\n body[\"ifVersion\"] = if_version\n if ttl:\n body[\"ttlSec\"] = int(ttl)\n res = _request(\"POST\", \"/api/agents/v1/state\", body, ok=(409,))\n if res.get(\"_status\") == 409:\n raise StateConflict(key, int(res.get(\"version\", 0)))\n return int(res.get(\"version\", 0))\n\n\ndef _agent() -> str:\n # Inside a run the run token picks the agent; a CLI token needs it named.\n return os.environ.get(\"CODIEDEV_AGENT\", \"\")\n\n\ndef _local_state() -> dict:\n try:\n with open(_STATE_PATH or \"\") as f:\n return json.load(f)\n except (FileNotFoundError, ValueError):\n return {}\n\n\nstate = _State()\n\n\ndef step(name: str, fn, ttl: float = 7 * 86400) -> Any:\n \"\"\"Run fn() once for this run and remember its (JSON) result: if the run\n is retried after a failure, finished steps return their saved result\n instead of running again. Use it around side effects (posting a reply,\n charging a card) so a retry doesn't repeat them.\"\"\"\n key = f\"step/{os.environ.get('CODIEDEV_RUN_ID', 'local')}/{name}\"\n value, version = state.get_versioned(key)\n if version:\n return value[\"result\"]\n result = fn()\n state.set(key, {\"result\": result}, ttl=ttl)\n return result\n\n\n_SELF = object()\n\n\ndef request_approval(\n title: str,\n details: Any = None,\n options: \"list[str] | None\" = None,\n approvers: \"list[str] | None\" = None,\n then: Any = _SELF,\n context: Any = None,\n expires_in: \"float | None\" = None,\n) -> dict:\n \"\"\"Ask a person to decide. It shows in Codiedev (Agents → Approvals) and\n at the returned [\"url\"], which you can post anywhere (Slack, email).\n\n When it's decided, a run of `then` (this agent by default; None for no\n run) starts with input {\"approval\": {\"decision\", \"comment\", \"decidedBy\",\n ...}, \"context\": context}. If nobody decides within expires_in seconds\n (default 7 days), it starts with decision \"expired\". Usually the run\n ends after asking, so nothing runs (or costs) while it waits.\n options default to [\"approve\", \"reject\"]; approvers (emails) limit who\n can decide (admins always can).\"\"\"\n body: dict = {\"title\": title, \"details\": details, \"options\": options, \"approvers\": approvers, \"context\": context}\n if then is not _SELF:\n body[\"onDecision\"] = then\n if expires_in:\n body[\"expiresSec\"] = int(expires_in)\n if os.environ.get(\"CODIEDEV_LOCAL\") == \"1\":\n print(f\"[local] approval requested: {title} ({', '.join(options or ['approve', 'reject'])}); not sent on a local run\", flush=True)\n return {\"id\": f\"local-{int(time.time())}\", \"status\": \"pending\", \"url\": None, \"title\": title}\n return _request(\"POST\", \"/api/agents/v1/approvals\", {k: v for k, v in body.items() if v is not None or k == \"onDecision\"})\n\n\ndef get_approval(approval_id: str) -> dict:\n \"\"\"An approval's status (\"pending\", \"decided\", \"expired\"), decision and comment.\"\"\"\n return _request(\"GET\", f\"/api/agents/v1/approvals?id={urllib.parse.quote(approval_id)}\")\n\n\ndef openai(**kwargs):\n \"\"\"An OpenAI client on the Codiedev gateway, with trace headers for this run.\"\"\"\n from openai import OpenAI\n\n headers = {\n \"X-Codiedev-Workload\": os.environ.get(\"CODIEDEV_AGENT\", \"local-agent\"),\n \"X-Codiedev-Run-Id\": os.environ.get(\"CODIEDEV_RUN_ID\", f\"local-{int(time.time())}\"),\n \"X-Codiedev-Environment\": \"production\" if os.environ.get(\"CODIEDEV_RUN_ID\") else \"development\",\n }\n headers.update(kwargs.pop(\"default_headers\", {}) or {})\n return OpenAI(\n base_url=os.environ.get(\"CODIEDEV_GATEWAY_URL\", \"https://gateway.codiedev.com/api/v1\"),\n api_key=os.environ.get(\"CODIEDEV_API_KEY\") or os.environ.get(\"CODIEDEV_TOKEN\"),\n default_headers=headers,\n **kwargs,\n )\n";
5
+ exports.jsIndex = "// Codiedev agents SDK for JavaScript and TypeScript, preinstalled in every\n// run. Same primitive as the Python SDK (codiedev_agents.py):\n//\n// import * as agents from \"codiedev-agents\";\n// const task = agents.input(); // this run's input (JSON)\n// const llm = await agents.openai(); // OpenAI client on the Codiedev gateway, traced\n// const child = await agents.run(\"summarize\", { text }); // another agent, as a child run\n// agents.output({ summary: child.output }); // this run's result\n// agents.respond({ text: \"on it\" }); // what a webhook caller waiting (?wait=) gets back\n// await agents.state.get(\"cursor\"); // per-agent storage that outlives runs\n// await agents.step(\"post\", () => post()); // done once per run, even across retries\n// await agents.start(\"triage\", input, { delaySec: 3600 }); // start a run later\n// await agents.requestApproval(\"Refund $120?\", { context }); // a person decides; a run acts on it\n//\n// Outside a run, set CODIEDEV_TOKEN and CODIEDEV_AGENTS_URL and call run() the same way.\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nconst INPUT_PATH = process.env.CODIEDEV_INPUT_PATH || \"/agent/.codiedev/input.json\";\nconst OUTPUT_PATH = process.env.CODIEDEV_OUTPUT_PATH || \"/agent/.codiedev/output.json\";\nconst REQUEST_PATH = process.env.CODIEDEV_REQUEST_PATH || \"/agent/.codiedev/request.json\";\nconst RESPONSE_PATH = process.env.CODIEDEV_RESPONSE_PATH || \"/agent/.codiedev/response.json\";\n// Set by `codiedev agents run --local`: state lives in this file instead of Codiedev.\nconst STATE_PATH = process.env.CODIEDEV_STATE_PATH;\n// Inside a run the run token picks the agent; a CLI token needs it named.\nconst AGENT = process.env.CODIEDEV_AGENT || \"\";\nconst DONE = new Set([\"succeeded\", \"failed\", \"cancelled\"]);\n\n/** state.set(..., { ifVersion }) found the key at another version. */\nexport class StateConflict extends Error {\n constructor(key, version) {\n super(`state ${JSON.stringify(key)} changed (now version ${version})`);\n this.key = key;\n this.version = version;\n }\n}\n\nexport class AgentRunError extends Error {\n constructor(run) {\n super(`${run.agent} run ${run.id} ${run.status}: ${run.error || \"no error message\"}`);\n this.run = run;\n }\n}\n\nfunction readJson(file) {\n try {\n return JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch (err) {\n if (err.code === \"ENOENT\") return null;\n throw err;\n }\n}\n\n/** This run's input, parsed from JSON (`fallback` when started without one). */\nexport function input(fallback = null) {\n const value = readJson(INPUT_PATH);\n return value ?? fallback;\n}\n\n/** Set this run's result. Must be JSON-serializable; the last call wins. */\nexport function output(value) {\n fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true });\n fs.writeFileSync(OUTPUT_PATH, JSON.stringify(value));\n}\n\n/** What a webhook caller gets back when it waits for the run (?wait=N): a\n * string is sent as text, anything else as JSON. Without it, a waiting\n * caller gets the run's output. */\nexport function respond(body = null, { status = 200, headers = {} } = {}) {\n fs.mkdirSync(path.dirname(RESPONSE_PATH), { recursive: true });\n fs.writeFileSync(RESPONSE_PATH, JSON.stringify({ status, headers, body }));\n}\n\n/** This run's attempt: 1, or 2+ when it's a retry of a failed attempt. */\nexport function attempt() {\n return Number(process.env.CODIEDEV_ATTEMPT || \"1\");\n}\n\n/** For a webhook run: { method, query, headers, body } with the raw body, to verify a signature. */\nexport function request() {\n return readJson(REQUEST_PATH);\n}\n\nfunction base() {\n const url = (process.env.CODIEDEV_AGENTS_URL || \"\").replace(/\\/$/, \"\");\n if (!url) throw new Error(\"Set CODIEDEV_AGENTS_URL (it's set for you inside a run)\");\n return url;\n}\n\nfunction token() {\n const t = process.env.CODIEDEV_RUN_TOKEN || process.env.CODIEDEV_TOKEN;\n if (!t) throw new Error(\"Set CODIEDEV_TOKEN to your Codiedev CLI token (inside a run it's set for you)\");\n return t;\n}\n\nasync function call(method, route, body, ok = []) {\n const res = await fetch(`${base()}${route}`, {\n method,\n headers: { Authorization: `Bearer ${token()}`, \"Content-Type\": \"application/json\" },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n const text = await res.text();\n let data = {};\n try {\n data = text ? JSON.parse(text) : {};\n } catch {\n data = { error: text };\n }\n if (ok.includes(res.status)) return { _status: res.status, ...data };\n if (!res.ok) throw new Error(`Codiedev agents API ${res.status}: ${data.error || text}`);\n return data;\n}\n\n/** Start a run and return its id without waiting. delaySec (up to 7 days)\n * starts it later; retries overrides the agent's retries. */\nexport async function start(agent, input = null, { idempotencyKey, delaySec, retries } = {}) {\n return (await call(\"POST\", \"/api/agents/v1/run\", {\n agent,\n input,\n ...(idempotencyKey ? { idempotencyKey } : {}),\n ...(delaySec ? { delaySec } : {}),\n ...(retries !== undefined ? { retries } : {}),\n })).runId;\n}\n\nfunction localState() {\n try {\n return JSON.parse(fs.readFileSync(STATE_PATH, \"utf8\"));\n } catch {\n return {};\n }\n}\n\nasync function writeState(key, value, { ifVersion, ttlSec } = {}) {\n if (STATE_PATH) {\n const data = localState();\n const current = data[key]?.version ?? 0;\n if (ifVersion !== undefined && ifVersion !== current) throw new StateConflict(key, current);\n if (value === null) delete data[key];\n else data[key] = { value, version: current + 1 };\n fs.writeFileSync(STATE_PATH, JSON.stringify(data, null, 2));\n return value === null ? 0 : current + 1;\n }\n const res = await call(\"POST\", \"/api/agents/v1/state\", { key, value, agent: AGENT, ...(ifVersion !== undefined ? { ifVersion } : {}), ...(ttlSec ? { ttlSec } : {}) }, [409]);\n if (res._status === 409) throw new StateConflict(key, res.version ?? 0);\n return res.version ?? 0;\n}\n\n/** Per-agent key-value storage that outlives runs (values: any JSON, up to\n * 64 KB; 1,000 keys). Every write bumps the key's version; pass ifVersion\n * to write only if nobody else did since you read it. */\nexport const state = {\n async get(key, fallback = null) {\n const { value } = await state.getVersioned(key);\n return value ?? fallback;\n },\n /** { value, version }; { value: null, version: 0 } when the key isn't set. */\n async getVersioned(key) {\n if (STATE_PATH) {\n const row = localState()[key];\n return row ? { value: row.value, version: row.version } : { value: null, version: 0 };\n }\n const res = await call(\"GET\", `/api/agents/v1/state?${new URLSearchParams({ key, agent: AGENT })}`, undefined, [404]);\n if (res._status === 404) return { value: null, version: 0 };\n return { value: JSON.parse(res.value), version: res.version };\n },\n /** Store value; returns the new version. Throws StateConflict on an ifVersion mismatch (0 = unset). */\n set(key, value, { ifVersion, ttlSec } = {}) {\n return writeState(key, value, { ifVersion, ttlSec });\n },\n async delete(key, { ifVersion } = {}) {\n await writeState(key, null, { ifVersion });\n },\n /** Keys (and with values: true, values) starting with prefix. */\n async list(prefix = \"\", { values = false } = {}) {\n if (STATE_PATH) {\n return Object.entries(localState())\n .filter(([k]) => k.startsWith(prefix))\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, row]) => ({ key, version: row.version, ...(values ? { value: row.value } : {}) }));\n }\n const out = [];\n let after = null;\n do {\n const q = new URLSearchParams({ prefix, values: values ? \"1\" : \"0\", limit: \"200\", agent: AGENT, ...(after ? { after } : {}) });\n const page = await call(\"GET\", `/api/agents/v1/state?${q}`);\n out.push(...page.keys.map((row) => (\"value\" in row ? { ...row, value: JSON.parse(row.value) } : row)));\n after = page.next;\n } while (after);\n return out;\n },\n /** Read-modify-write that's safe with runs in parallel: fn(old) => new, retried if another run wrote first. */\n async update(key, fn, fallback = null, retries = 10) {\n for (let i = 0; i < retries; i++) {\n const { value, version } = await state.getVersioned(key);\n const next = await fn(value ?? fallback);\n try {\n await state.set(key, next, { ifVersion: version });\n return next;\n } catch (err) {\n if (!(err instanceof StateConflict)) throw err;\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n }\n throw new StateConflict(key, -1);\n },\n};\n\n/** Run fn() once for this run and remember its (JSON) result: if the run is\n * retried after a failure, finished steps return their saved result instead\n * of running again. Use it around side effects so a retry doesn't repeat them. */\nexport async function step(name, fn, { ttlSec = 7 * 86400 } = {}) {\n const key = `step/${process.env.CODIEDEV_RUN_ID || \"local\"}/${name}`;\n const { value, version } = await state.getVersioned(key);\n if (version) return value.result;\n const result = await fn();\n await state.set(key, { result: result ?? null }, { ttlSec });\n return result;\n}\n\n/** Ask a person to decide. It shows in Codiedev (Agents → Approvals) and at\n * the returned .url, which you can post anywhere. When it's decided, a run of\n * `then` (this agent by default; null for none) starts with input\n * { approval: { decision, comment, decidedBy, ... }, context }. Undecided\n * after expiresSec (default 7 days): decision \"expired\". */\nexport async function requestApproval(title, { details, options, approvers, then, context, expiresSec } = {}) {\n if (process.env.CODIEDEV_LOCAL === \"1\") {\n console.log(`[local] approval requested: ${title} (${(options || [\"approve\", \"reject\"]).join(\", \")}); not sent on a local run`);\n return { id: `local-${Date.now()}`, status: \"pending\", url: null, title };\n }\n return call(\"POST\", \"/api/agents/v1/approvals\", {\n title,\n ...(details !== undefined ? { details } : {}),\n ...(options ? { options } : {}),\n ...(approvers ? { approvers } : {}),\n ...(then !== undefined ? { onDecision: then } : {}),\n ...(context !== undefined ? { context } : {}),\n ...(expiresSec ? { expiresSec } : {}),\n });\n}\n\n/** An approval's status (\"pending\", \"decided\", \"expired\"), decision and comment. */\nexport async function getApproval(id) {\n return call(\"GET\", `/api/agents/v1/approvals?id=${encodeURIComponent(id)}`);\n}\n\n/** A run's status, input, output (parsed), error, log and children. */\nexport async function get(runId) {\n const run = await call(\"GET\", `/api/agents/v1/run?id=${encodeURIComponent(runId)}`);\n if (typeof run.output === \"string\") {\n try {\n run.output = JSON.parse(run.output);\n } catch {\n // not JSON: keep the string\n }\n }\n return run;\n}\n\n/** Run an agent and wait for it; its result is `.output`. Inside a run this\n * starts a child run. Throws AgentRunError if it fails unless check is false. */\nexport async function run(agent, input = null, { timeoutMs = 900_000, pollMs = 2000, check = true, idempotencyKey } = {}) {\n const id = await start(agent, input, { idempotencyKey });\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const r = await get(id);\n if (DONE.has(r.status)) {\n if (check && r.status !== \"succeeded\") throw new AgentRunError(r);\n return r;\n }\n if (Date.now() > deadline) throw new Error(`${agent} run ${id} still ${r.status} after ${timeoutMs / 1000}s`);\n await new Promise((resolve) => setTimeout(resolve, pollMs));\n }\n}\n\n/** Options for `new OpenAI(...)` on the Codiedev gateway, with this run's trace headers. */\nexport function openaiOptions(extra = {}) {\n const inRun = !!process.env.CODIEDEV_RUN_ID;\n return {\n baseURL: process.env.CODIEDEV_GATEWAY_URL || \"https://gateway.codiedev.com/api/v1\",\n apiKey: process.env.CODIEDEV_API_KEY || process.env.CODIEDEV_TOKEN,\n ...extra,\n defaultHeaders: {\n \"X-Codiedev-Workload\": process.env.CODIEDEV_AGENT || \"local-agent\",\n \"X-Codiedev-Run-Id\": process.env.CODIEDEV_RUN_ID || `local-${Date.now()}`,\n \"X-Codiedev-Environment\": inRun ? \"production\" : \"development\",\n ...(extra.defaultHeaders || {}),\n },\n };\n}\n\n/** An OpenAI client on the Codiedev gateway (the `openai` package is preinstalled). */\nexport async function openai(extra = {}) {\n const { default: OpenAI } = await import(\"openai\");\n return new OpenAI(openaiOptions(extra));\n}\n";
6
+ exports.jsTypes = "/* eslint-disable @typescript-eslint/no-explicit-any -- defaults are `any` on purpose: agent code reads its own JSON input/state without declaring types first. */\nexport type RunStatus = \"queued\" | \"running\" | \"succeeded\" | \"failed\" | \"cancelled\";\nexport interface Run<O = unknown> {\n id: string;\n agent: string;\n version: number;\n status: RunStatus;\n trigger: \"cli\" | \"api\" | \"portal\" | \"schedule\" | \"webhook\" | \"child\" | \"approval\";\n parentRunId: string | null;\n rootRunId: string | null;\n depth: number;\n attempt: number;\n maxAttempts: number;\n scheduledFor: number | null;\n input: string | null;\n output: O;\n error: string | null;\n logTail: string | null;\n durationMs: number | null;\n children: Run[];\n}\nexport interface WebhookRequest { method: string; query: Record<string, string>; headers: Record<string, string>; body: string }\nexport interface Approval {\n id: string;\n agent: string;\n title: string;\n details: string | null;\n options: string[];\n status: \"pending\" | \"decided\" | \"expired\" | \"cancelled\";\n decision: string | null;\n comment: string | null;\n decidedBy: string | null;\n url: string | null;\n resumeRunId: string | null;\n}\n/** The input a run started by a decision gets. */\nexport interface ApprovalInput<C = any> {\n approval: { id: string; title: string; decision: string; comment: string | null; decidedBy: string | null; decidedAt: number; requestedByRun: string | null };\n context: C;\n}\nexport class AgentRunError extends Error { run: Run }\nexport class StateConflict extends Error { key: string; version: number }\nexport function input<T = any>(fallback?: T): T;\nexport function output(value: unknown): void;\nexport function request(): WebhookRequest | null;\nexport function respond(body?: unknown, opts?: { status?: number; headers?: Record<string, string> }): void;\nexport function attempt(): number;\nexport function start(agent: string, input?: unknown, opts?: { idempotencyKey?: string; delaySec?: number; retries?: number }): Promise<string>;\nexport function get<O = any>(runId: string): Promise<Run<O>>;\nexport function run<O = any>(agent: string, input?: unknown, opts?: { timeoutMs?: number; pollMs?: number; check?: boolean; idempotencyKey?: string }): Promise<Run<O>>;\nexport const state: {\n get<T = any>(key: string, fallback?: T): Promise<T>;\n getVersioned<T = any>(key: string): Promise<{ value: T | null; version: number }>;\n set(key: string, value: unknown, opts?: { ifVersion?: number; ttlSec?: number }): Promise<number>;\n delete(key: string, opts?: { ifVersion?: number }): Promise<void>;\n list(prefix?: string, opts?: { values?: boolean }): Promise<Array<{ key: string; version: number; value?: any }>>;\n update<T = any>(key: string, fn: (old: T) => T | Promise<T>, fallback?: T, retries?: number): Promise<T>;\n};\nexport function step<T>(name: string, fn: () => T | Promise<T>, opts?: { ttlSec?: number }): Promise<T>;\nexport function requestApproval<C = any>(title: string, opts?: { details?: unknown; options?: string[]; approvers?: string[]; then?: string | null; context?: C; expiresSec?: number }): Promise<Approval>;\nexport function getApproval(id: string): Promise<Approval>;\nexport function openaiOptions(extra?: Record<string, unknown>): { baseURL: string; apiKey: string | undefined; defaultHeaders: Record<string, string> };\nexport function openai(extra?: Record<string, unknown>): Promise<import(\"openai\").default>;";
7
+ exports.jsPackage = "{\n \"name\": \"codiedev-agents\",\n \"version\": \"0.1.0\",\n \"description\": \"Codiedev agents SDK: this run's input and output, chaining, and a traced OpenAI client.\",\n \"type\": \"module\",\n \"main\": \"./index.mjs\",\n \"types\": \"./index.d.ts\",\n \"exports\": { \".\": { \"types\": \"./index.d.ts\", \"default\": \"./index.mjs\" } }\n}\n";
@@ -3,9 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runAsk = runAsk;
4
4
  const shared_1 = require("./shared");
5
5
  function parseArgs(args) {
6
- // Everything after the command, joined back into a question string. Allows
7
- // both quoted and unquoted forms: `codiedev ask "why pg-bouncer?"` or
8
- // `codiedev ask why did we drop pg-bouncer`.
9
6
  const question = args
10
7
  .filter((a) => !a.startsWith("--"))
11
8
  .join(" ")
@@ -123,7 +123,6 @@ async function runCaptureCommand(args, depsOverride = {}) {
123
123
  return;
124
124
  }
125
125
  if (subcommand === "ignored") {
126
- // `ignored` lists everything; `ignored <path>` answers "is this ignored?".
127
126
  const query = args.slice(1).join(" ").trim();
128
127
  if (query) {
129
128
  reportCheck(query, current, deps);
@@ -139,7 +138,6 @@ async function runCaptureCommand(args, depsOverride = {}) {
139
138
  deps.log(` ${value}`);
140
139
  return;
141
140
  }
142
- // Back-compat: the earlier `codiedev capture check <path>` form.
143
141
  if (subcommand === "check") {
144
142
  reportCheck(requirePath(args), current, deps);
145
143
  return;
@@ -100,7 +100,6 @@ async function runCreateTicket(args) {
100
100
  await runBare(config, parsed.prompt);
101
101
  return;
102
102
  }
103
- // submit mode
104
103
  if (!parsed.submitSource) {
105
104
  console.error("--submit requires a path or '-' for stdin");
106
105
  process.exit(1);
@@ -114,7 +113,6 @@ async function runCreateTicket(args) {
114
113
  console.error(`Failed to read plan: ${err.message}`);
115
114
  process.exit(1);
116
115
  }
117
- // Translate fullNames → IDs before submitting
118
116
  let parsedPlan;
119
117
  try {
120
118
  parsedPlan = JSON.parse(raw);
@@ -141,14 +139,6 @@ async function runCreateTicket(args) {
141
139
  }
142
140
  await runSubmit(config, JSON.stringify(parsedPlan));
143
141
  }
144
- /**
145
- * Bare-mode entrypoint. Fetches the protocol body from the backend so we can
146
- * iterate on it without shipping a new CLI, substitutes the user's prompt,
147
- * and writes the result to stdout for the agent to follow. Hard-errors on
148
- * fetch failure rather than falling back to a stale local copy — the whole
149
- * point of moving the protocol server-side is that the freshest version is
150
- * the only version.
151
- */
152
142
  async function runBare(config, prompt) {
153
143
  let res;
154
144
  try {
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.configDirInUseCheck = configDirInUseCheck;
36
37
  exports.runDoctor = runDoctor;
37
38
  const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
@@ -45,14 +46,28 @@ const version_1 = require("../version");
45
46
  const upgradeNudge_1 = require("../upgradeNudge");
46
47
  const captureReadiness_1 = require("../captureReadiness");
47
48
  const captureLog_1 = require("../captureLog");
48
- const securityDoctor_1 = require("../securityDoctor");
49
- const securityFeature_1 = require("../securityFeature");
50
- const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
51
- const CLAUDE_INSTRUCTIONS_PATH = path.join(os.homedir(), ".claude", "CLAUDE.md");
52
- const CODEX_HOOKS_PATH = path.join(os.homedir(), ".codex", "hooks.json");
53
- const CODEX_INSTRUCTIONS_PATH = path.join(os.homedir(), ".codex", "AGENTS.md");
49
+ const copilotInstallation_1 = require("../copilotInstallation");
50
+ const piHooks_1 = require("../piHooks");
51
+ const harnessDirs_1 = require("../harnessDirs");
52
+ const CLAUDE_DIR = (0, harnessDirs_1.claudeConfigDir)();
53
+ const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
54
+ const CLAUDE_INSTRUCTIONS_PATH = path.join(CLAUDE_DIR, "CLAUDE.md");
55
+ const CODEX_DIR = (0, harnessDirs_1.codexHome)();
56
+ const CODEX_HOOKS_PATH = path.join(CODEX_DIR, "hooks.json");
57
+ const CODEX_INSTRUCTIONS_PATH = path.join(CODEX_DIR, "AGENTS.md");
54
58
  const CURSOR_HOOKS_PATH = path.join(os.homedir(), ".cursor", "hooks.json");
55
59
  const CURSOR_INSTRUCTIONS_PATH = path.join(os.homedir(), ".cursor", "rules", "codiedev.mdc");
60
+ function configDirInUseCheck(agent, dir, inUse, envVar) {
61
+ return {
62
+ name: `${agent} reads this config dir`,
63
+ status: inUse ? "pass" : "warn",
64
+ detail: inUse
65
+ ? (0, harnessDirs_1.displayPath)(dir)
66
+ : `no ${agent} activity in ${(0, harnessDirs_1.displayPath)(dir)} — if you've run ${agent} here, it reads its config from ` +
67
+ `somewhere else, so the hooks below never fire. Set ${envVar} to that dir (or run as the user that runs ` +
68
+ `${agent}) and re-run \`codiedev connect\``,
69
+ };
70
+ }
56
71
  function symbol(status) {
57
72
  if (status === "pass")
58
73
  return "✓";
@@ -61,17 +76,11 @@ function symbol(status) {
61
76
  return "✗";
62
77
  }
63
78
  function codexInstalled() {
64
- return fs.existsSync(path.join(os.homedir(), ".codex"));
79
+ return fs.existsSync(CODEX_DIR);
65
80
  }
66
81
  function cursorInstalled() {
67
82
  return fs.existsSync(path.join(os.homedir(), ".cursor"));
68
83
  }
69
- // `isCodiedevHookCommand` lives in ../detection alongside detectClaudeCode
70
- // so utils.ts and doctor.ts share one definition (they used to drift —
71
- // each had its own copy).
72
- // Cursor's hooks.json schema is { hooks: { sessionEnd: [{ command, ... }] } }
73
- // — flat array of objects with `command`, not the Claude/Codex nested
74
- // `{ hooks: [{ command }] }` wrapper.
75
84
  function hasCursorHook() {
76
85
  try {
77
86
  if (!fs.existsSync(CURSOR_HOOKS_PATH))
@@ -118,29 +127,10 @@ function hasCodiedevInstructions(instructionsPath) {
118
127
  return false;
119
128
  }
120
129
  }
121
- function hasGhCli() {
122
- try {
123
- (0, child_process_1.execSync)("gh --version", { stdio: ["pipe", "pipe", "pipe"] });
124
- return true;
125
- }
126
- catch {
127
- return false;
128
- }
129
- }
130
- function hasGlabCli() {
131
- try {
132
- (0, child_process_1.execSync)("glab --version", { stdio: ["pipe", "pipe", "pipe"] });
133
- return true;
134
- }
135
- catch {
136
- return false;
137
- }
138
- }
139
130
  async function runDoctor(_args) {
140
131
  const checks = [];
141
132
  console.log("\nCodiedev doctor — checking your setup…\n");
142
133
  await (0, upgradeNudge_1.maybePrintUpgradeNudge)();
143
- // 1. Config file
144
134
  const config = (0, utils_1.readConfig)();
145
135
  if (!config) {
146
136
  checks.push({
@@ -156,19 +146,12 @@ async function runDoctor(_args) {
156
146
  status: "pass",
157
147
  detail: `connected to ${config.companyName}`,
158
148
  });
159
- // 2. Token still valid + backend reachable (re-validate). The detail names
160
- // the backend host so users (and their IT) can confirm which domain the CLI
161
- // talks to — the same host capture uploads use, so this check exercises the
162
- // exact TLS path a corporate proxy would intercept.
163
149
  let backendHost = "the capture host";
164
150
  try {
165
151
  backendHost = new URL(config.backendUrl).host;
166
152
  }
167
153
  catch {
168
- // malformed backendUrl in config — keep the generic placeholder
169
154
  }
170
- // Live tracked-repo list fetched from validateToken below. Null until (and
171
- // unless) the backend answers; the checks fall back to `config.repos`.
172
155
  let liveRepos = null;
173
156
  try {
174
157
  const res = await fetch(`${config.backendUrl}/api/cli/validateToken`, {
@@ -185,20 +168,13 @@ async function runDoctor(_args) {
185
168
  status: "pass",
186
169
  detail: backendHost,
187
170
  });
188
- // validateToken returns the company's CURRENT tracked repos. Capture
189
- // them: the repo-resolution checks below must reflect what the SERVER
190
- // will resolve at capture time (repoResolver.ts resolves against the
191
- // live list), not the possibly-stale `config.repos` frozen at the last
192
- // `connect`. A repo linked in the portal after that connect otherwise
193
- // reports a false "isn't a tracked repo — link it in the portal" here
194
- // even though sessions/MRs for it attribute fine server-side.
195
171
  try {
196
172
  const data = (await res.json());
197
173
  if (Array.isArray(data.repos))
198
174
  liveRepos = data.repos;
199
175
  }
200
176
  catch {
201
- liveRepos = null; // malformed body → fall back to the cached list
177
+ liveRepos = null;
202
178
  }
203
179
  }
204
180
  else {
@@ -210,9 +186,6 @@ async function runDoctor(_args) {
210
186
  }
211
187
  }
212
188
  catch (err) {
213
- // fetch buries the real TLS/socket error in .cause ("fetch failed" alone
214
- // is undiagnosable); unwrap it and, when it's an SSL-inspection signature,
215
- // say what actually fixes it.
216
189
  const desc = (0, captureReadiness_1.describeFetchError)(err);
217
190
  const tlsHint = (0, captureReadiness_1.tlsInterceptionHint)(desc, backendHost);
218
191
  checks.push({
@@ -221,7 +194,6 @@ async function runDoctor(_args) {
221
194
  detail: tlsHint ? `${desc} — ${tlsHint}` : `${desc} (host: ${backendHost})`,
222
195
  });
223
196
  }
224
- // 3. Authenticated endpoint smoke test (inbox query)
225
197
  try {
226
198
  await (0, shared_1.apiRequest)("GET", "/api/cli/inbox", {
227
199
  config,
@@ -239,38 +211,16 @@ async function runDoctor(_args) {
239
211
  detail: err.message,
240
212
  });
241
213
  }
242
- // 4. Tracked repos. Prefer the live list from validateToken over the cache:
243
- // a repo linked after the last `connect` is tracked server-side but absent
244
- // from `config.repos`, and resolving against the stale cache is exactly what
245
- // produced the false "isn't a tracked repo — link it in the portal" warning.
246
- // Treat an EMPTY live list as "no usable live data", NOT an authoritative
247
- // zero: a transient permission glitch / backend hiccup can return `repos: []`,
248
- // and both driving the checks from it AND healing the cache to empty would be
249
- // wrong — the write would durably break every offline doctor run (and the
250
- // capture hook's providedRepoId) until the next successful connect. So when
251
- // live is empty, fall back to the cache and skip the write, exactly like
252
- // backend-unreachable. A genuinely repo-less company also has an empty cache,
253
- // so it still reports "0 repos" correctly.
254
214
  const trackedRepos = liveRepos !== null && liveRepos.length > 0 ? liveRepos : (config.repos ?? []);
255
- // Detect drift by CONTENT, not count: a same-size swap (one repo removed +
256
- // another added between connects) or a repo whose path/URL changed while its
257
- // id stayed stable (GitLab move/rename — exactly the SONIFI migration case)
258
- // both leave the count unchanged. A length-only check would skip the heal and
259
- // leave the capture hook's cache — and its advisory providedRepoId — stale.
260
215
  const canonRepos = (rs) => JSON.stringify([...rs].map((r) => [r.repoId, r.url, r.fullName]).sort());
261
216
  const cacheRefreshed = liveRepos !== null &&
262
217
  liveRepos.length > 0 &&
263
218
  canonRepos(liveRepos) !== canonRepos(config.repos ?? []);
264
- // Heal the on-disk cache so the capture hook (which reads `config.repos` for
265
- // the advisory providedRepoId) stops trailing the portal too. Best-effort:
266
- // doctor must never fail because the config couldn't be rewritten.
267
219
  if (cacheRefreshed && liveRepos) {
268
220
  try {
269
221
  (0, utils_1.writeConfig)({ ...config, repos: liveRepos, lastSynced: new Date().toISOString() });
270
222
  }
271
223
  catch {
272
- // read-only home / permission issue — the in-memory list still drives
273
- // the checks below; the user just re-syncs on the next `connect`.
274
224
  }
275
225
  }
276
226
  const repoCount = trackedRepos.length;
@@ -281,11 +231,6 @@ async function runDoctor(_args) {
281
231
  ? `${repoCount} repo${repoCount === 1 ? "" : "s"}${cacheRefreshed ? " (refreshed from portal)" : ""}`
282
232
  : "0 — link repos in the portal so sessions get captured",
283
233
  });
284
- // 4b. Capture readiness — the part that actually decides whether a session
285
- // lands. doctor used to verify only that the hook was *installed*; these
286
- // checks answer "from this directory, would a session capture, and did my
287
- // last one?" — the blind spot behind lean-container / submodule / no-remote
288
- // zeros.
289
234
  const cwd = process.cwd();
290
235
  const readiness = (0, captureReadiness_1.assessCaptureReadiness)(cwd, trackedRepos, {
291
236
  exec: (cmd, dir) => (0, child_process_1.execSync)(cmd, { cwd: dir, stdio: ["pipe", "pipe", "pipe"] }).toString(),
@@ -294,11 +239,6 @@ async function runDoctor(_args) {
294
239
  getBranch: utils_1.getGitBranch,
295
240
  getHeadSha: utils_1.getGitHeadSha,
296
241
  });
297
- // Workspace-root mode: the cwd isn't a repo itself, but git repos sit
298
- // directly under it (one `.devcontainer/` at a parent folder of several
299
- // cloned repos). Capture tags the sub-repo a session edits, so report that
300
- // instead of flatly warning "not a work tree / no remote" — the exact output
301
- // that made working workspace-root setups read as broken.
302
242
  const workspaceScan = readiness.gitOnPath && !readiness.insideWorkTree
303
243
  ? (0, captureReadiness_1.assessWorkspaceRepos)(cwd, trackedRepos, {
304
244
  listChildDirs: (dir) => fs
@@ -313,10 +253,10 @@ async function runDoctor(_args) {
313
253
  const workspaceMode = workspaceScan.repoCount > 0;
314
254
  checks.push({
315
255
  name: "git on PATH",
316
- status: readiness.gitOnPath ? "pass" : "fail",
256
+ status: readiness.gitOnPath ? "pass" : "warn",
317
257
  detail: readiness.gitOnPath
318
258
  ? undefined
319
- : "not found — capture can't tag sessions to a repo (install git, e.g. `apk add git`)",
259
+ : "not found — sessions still capture but can't be tagged to a repo (install git if you work in repos)",
320
260
  });
321
261
  checks.push({
322
262
  name: "Inside a git work tree (current dir)",
@@ -349,9 +289,6 @@ async function runDoctor(_args) {
349
289
  ? "branch/sha resolve from the sub-repo each session writes to"
350
290
  : "no branch/sha resolved — session→merge attribution will be weaker",
351
291
  });
352
- // 4c. The payoff: did the last session actually land? Ask the server for
353
- // portal truth (does the row exist for this user?), falling back to the local
354
- // 2xx breadcrumb if the endpoint is unreachable or predates this CLI/backend.
355
292
  const localMarker = (0, utils_1.readLastCapture)();
356
293
  let server = null;
357
294
  try {
@@ -372,7 +309,7 @@ async function runDoctor(_args) {
372
309
  server = (await res.json());
373
310
  }
374
311
  catch {
375
- server = null; // unreachable / old backend → fall back to the local marker
312
+ server = null;
376
313
  }
377
314
  const lastCapture = (0, captureReadiness_1.summarizeServerCapture)(server, localMarker, Date.now());
378
315
  checks.push({
@@ -380,7 +317,6 @@ async function runDoctor(_args) {
380
317
  status: lastCapture.status,
381
318
  detail: lastCapture.detail,
382
319
  });
383
- // 4d. Capture-log activity: did recent uploads succeed? Are there pending jobs?
384
320
  const logTail = (0, captureLog_1.readCaptureLogTail)(40);
385
321
  let pendingJobCount = 0;
386
322
  try {
@@ -397,25 +333,25 @@ async function runDoctor(_args) {
397
333
  status: captureActivity.status,
398
334
  detail: captureActivity.detail,
399
335
  });
400
- // 5. Claude Code setup (if present)
401
336
  if ((0, utils_1.claudeCodeInstalled)()) {
402
337
  checks.push({
403
338
  name: "Claude Code detected",
404
339
  status: "pass",
405
- detail: "~/.claude",
340
+ detail: (0, harnessDirs_1.displayPath)(CLAUDE_DIR),
406
341
  });
342
+ checks.push(configDirInUseCheck("Claude Code", CLAUDE_DIR, (0, harnessDirs_1.claudeConfigDirInUse)(CLAUDE_DIR), "CLAUDE_CONFIG_DIR"));
407
343
  checks.push({
408
344
  name: "Claude Code SessionEnd hook",
409
345
  status: hasCodiedevHook(CLAUDE_SETTINGS_PATH, "SessionEnd") ? "pass" : "fail",
410
346
  detail: hasCodiedevHook(CLAUDE_SETTINGS_PATH, "SessionEnd")
411
- ? "~/.claude/settings.json"
347
+ ? (0, harnessDirs_1.displayPath)(CLAUDE_SETTINGS_PATH)
412
348
  : "missing — re-run `codiedev connect`",
413
349
  });
414
350
  checks.push({
415
351
  name: "Claude Code agent instructions",
416
352
  status: hasCodiedevInstructions(CLAUDE_INSTRUCTIONS_PATH) ? "pass" : "fail",
417
353
  detail: hasCodiedevInstructions(CLAUDE_INSTRUCTIONS_PATH)
418
- ? "~/.claude/CLAUDE.md"
354
+ ? (0, harnessDirs_1.displayPath)(CLAUDE_INSTRUCTIONS_PATH)
419
355
  : "missing — re-run `codiedev connect`",
420
356
  });
421
357
  }
@@ -426,25 +362,25 @@ async function runDoctor(_args) {
426
362
  detail: "not installed on this machine",
427
363
  });
428
364
  }
429
- // 6. Codex setup (if present)
430
365
  if (codexInstalled()) {
431
366
  checks.push({
432
367
  name: "Codex detected",
433
368
  status: "pass",
434
- detail: "~/.codex",
369
+ detail: (0, harnessDirs_1.displayPath)(CODEX_DIR),
435
370
  });
371
+ checks.push(configDirInUseCheck("Codex", CODEX_DIR, (0, harnessDirs_1.codexHomeInUse)(CODEX_DIR), "CODEX_HOME"));
436
372
  checks.push({
437
373
  name: "Codex Stop hook",
438
374
  status: hasCodiedevHook(CODEX_HOOKS_PATH, "Stop") ? "pass" : "fail",
439
375
  detail: hasCodiedevHook(CODEX_HOOKS_PATH, "Stop")
440
- ? "~/.codex/hooks.json"
376
+ ? (0, harnessDirs_1.displayPath)(CODEX_HOOKS_PATH)
441
377
  : "missing — re-run `codiedev connect`",
442
378
  });
443
379
  checks.push({
444
380
  name: "Codex agent instructions",
445
381
  status: hasCodiedevInstructions(CODEX_INSTRUCTIONS_PATH) ? "pass" : "fail",
446
382
  detail: hasCodiedevInstructions(CODEX_INSTRUCTIONS_PATH)
447
- ? "~/.codex/AGENTS.md"
383
+ ? (0, harnessDirs_1.displayPath)(CODEX_INSTRUCTIONS_PATH)
448
384
  : "missing — re-run `codiedev connect`",
449
385
  });
450
386
  }
@@ -455,7 +391,6 @@ async function runDoctor(_args) {
455
391
  detail: "not installed on this machine",
456
392
  });
457
393
  }
458
- // 7. Cursor setup (if present)
459
394
  if (cursorInstalled()) {
460
395
  checks.push({
461
396
  name: "Cursor detected",
@@ -484,28 +419,26 @@ async function runDoctor(_args) {
484
419
  detail: "not installed on this machine",
485
420
  });
486
421
  }
487
- // Agent Security is part of installation health, not a separate setup
488
- // concept. Keep `codiedev security doctor` as an alias for focused output,
489
- // while the main doctor remains the one command users need to remember.
490
- if ((0, securityFeature_1.isAgentSecurityEnabled)(config)) {
491
- checks.push(...(0, securityDoctor_1.securityDoctorChecks)((0, securityDoctor_1.collectSecurityDoctorReport)(config.companyId)));
422
+ if ((0, utils_1.copilotInstalled)()) {
423
+ const hooks = (0, copilotInstallation_1.inspectCopilotHooks)();
424
+ checks.push({ name: "GitHub Copilot capture hooks", status: hooks.status === "pass" ? "pass" : "fail", detail: hooks.detail });
425
+ checks.push({ name: "GitHub Copilot CLI instructions",
426
+ status: hasCodiedevInstructions((0, utils_1.copilotInstructionsPath)()) ? "pass" : "fail",
427
+ detail: hasCodiedevInstructions((0, utils_1.copilotInstructionsPath)()) ? (0, utils_1.copilotInstructionsPath)() : "missing — re-run `codiedev connect`" });
428
+ }
429
+ else {
430
+ checks.push({ name: "GitHub Copilot", status: "warn", detail: "not installed on this machine" });
431
+ }
432
+ if ((0, utils_1.piInstalled)()) {
433
+ const extension = (0, piHooks_1.inspectPiExtension)();
434
+ checks.push({ name: "pi capture extension", status: extension.status === "pass" ? "pass" : "fail", detail: extension.detail });
435
+ checks.push({ name: "pi agent instructions",
436
+ status: hasCodiedevInstructions((0, utils_1.piInstructionsPath)()) ? "pass" : "fail",
437
+ detail: hasCodiedevInstructions((0, utils_1.piInstructionsPath)()) ? (0, harnessDirs_1.displayPath)((0, utils_1.piInstructionsPath)()) : "missing — re-run `codiedev connect`" });
438
+ }
439
+ else {
440
+ checks.push({ name: "pi", status: "warn", detail: "not installed on this machine" });
492
441
  }
493
- // 8. gh / glab CLIs (optional — only needed for reverse-ticket from a
494
- // specific PR/MR URL; branch mode works without either).
495
- checks.push({
496
- name: "GitHub CLI (`gh`) for reverse-ticket from a PR url",
497
- status: hasGhCli() ? "pass" : "warn",
498
- detail: hasGhCli()
499
- ? "installed"
500
- : "not installed — only needed for `codiedev reverse-ticket <github-pr-url>`",
501
- });
502
- checks.push({
503
- name: "GitLab CLI (`glab`) for reverse-ticket from an MR url",
504
- status: hasGlabCli() ? "pass" : "warn",
505
- detail: hasGlabCli()
506
- ? "installed"
507
- : "not installed — only needed for `codiedev reverse-ticket <gitlab-mr-url>`",
508
- });
509
442
  report(checks);
510
443
  const failures = checks.filter((c) => c.status === "fail");
511
444
  if (failures.length > 0) {