codiedev 0.10.0-beta.1 → 0.10.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/assets/opencode-plugin.mjs +6 -0
- package/assets/session-key.mjs +48 -0
- package/assets/tui-brand-plugin.tsx +73 -0
- package/dist/backup/cache.js +1 -5
- package/dist/backup/match.js +0 -6
- package/dist/backup/run.js +3 -35
- package/dist/captureExclusions.js +0 -12
- package/dist/captureLog.js +0 -28
- package/dist/captureReadiness.js +0 -58
- package/dist/captureSignals.js +184 -0
- package/dist/cli.js +135 -72
- package/dist/cliSession.js +456 -0
- package/dist/codexHookTrust.js +125 -6
- package/dist/codexLimitScan.js +359 -0
- package/dist/codie.js +77 -130
- package/dist/codieBrandInstall.js +80 -0
- package/dist/codieLauncher.js +76 -79
- package/dist/codieRuntime.js +19 -23
- package/dist/codieRuntimeInstall.js +108 -23
- package/dist/codieUsage.js +181 -0
- package/dist/commands/agentTemplates.generated.js +136 -0
- package/dist/commands/agents.js +1101 -0
- package/dist/commands/agentsSdk.generated.js +7 -0
- package/dist/commands/ask.js +0 -3
- package/dist/commands/capture.js +0 -2
- package/dist/commands/createTicket.js +0 -10
- package/dist/commands/doctor.js +53 -120
- package/dist/commands/evals.js +187 -0
- package/dist/commands/inbox.js +0 -2
- package/dist/commands/library.js +0 -3
- package/dist/commands/pack.js +573 -0
- package/dist/commands/ping.js +0 -2
- package/dist/commands/pull.js +6 -10
- package/dist/commands/push.js +2 -12
- package/dist/commands/search.js +0 -4
- package/dist/commands/send.js +0 -5
- package/dist/commands/share.js +0 -4
- package/dist/commands/shared.js +0 -35
- package/dist/commands/traces.js +344 -0
- package/dist/commands/tracesLabel.js +348 -0
- package/dist/configDir.js +18 -11
- package/dist/connect.js +55 -118
- package/dist/connectFlow.js +35 -9
- package/dist/copilotHooks.js +391 -0
- package/dist/copilotInstallation.js +64 -0
- package/dist/cursorHooks.js +38 -30
- package/dist/cursorLifecycle.js +109 -0
- package/dist/detection.js +0 -30
- package/dist/harnessDirs.js +77 -0
- package/dist/hook.js +256 -445
- package/dist/inboxFormat.js +0 -24
- package/dist/launchContext.js +150 -0
- package/dist/launcher.js +168 -0
- package/dist/limitEvent.js +44 -13
- package/dist/limitEventReporter.js +1 -1
- package/dist/managedPi.js +256 -0
- package/dist/mcp.js +5 -384
- package/dist/nativeSteering.js +24 -0
- package/dist/packs/codie.js +182 -0
- package/dist/packs/harnesses.js +411 -0
- package/dist/packs/hookGate.js +256 -0
- package/dist/packs/install.js +286 -0
- package/dist/packs/manifest.js +126 -0
- package/dist/packs/paths.js +81 -0
- package/dist/packs/shim.js +214 -0
- package/dist/packs/store.js +155 -0
- package/dist/packs/sync.js +170 -0
- package/dist/piHooks.js +216 -0
- package/dist/piRuntime.js +120 -0
- package/dist/repoResolver.js +0 -19
- package/dist/secret.js +0 -3
- package/dist/selfUpdate.js +376 -0
- package/dist/skillMarker.js +23 -0
- package/dist/subagents.js +135 -0
- package/dist/teamSkillContext.js +10 -0
- package/dist/transcriptModels.js +205 -0
- package/dist/upgradeNudge.js +0 -30
- package/dist/utils.js +502 -828
- package/dist/version.js +0 -10
- package/dist/versionCheck.js +0 -33
- package/package.json +8 -9
- package/dist/backup/cache.d.ts +0 -14
- package/dist/backup/match.d.ts +0 -2
- package/dist/backup/run.d.ts +0 -25
- package/dist/captureExclusions.d.ts +0 -30
- package/dist/captureLog.d.ts +0 -20
- package/dist/captureReadiness.d.ts +0 -65
- package/dist/cli.d.ts +0 -16
- package/dist/codexHookTrust.d.ts +0 -34
- package/dist/codie.d.ts +0 -2
- package/dist/codieLauncher.d.ts +0 -53
- package/dist/codieRuntime.d.ts +0 -29
- package/dist/codieRuntimeInstall.d.ts +0 -28
- package/dist/codieStartup.d.ts +0 -26
- package/dist/codieStartup.js +0 -30
- package/dist/commands/ask.d.ts +0 -1
- package/dist/commands/capture.d.ts +0 -14
- package/dist/commands/createTicket.d.ts +0 -27
- package/dist/commands/delete.d.ts +0 -1
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/inbox.d.ts +0 -3
- package/dist/commands/library.d.ts +0 -2
- package/dist/commands/note.d.ts +0 -1
- package/dist/commands/ping.d.ts +0 -1
- package/dist/commands/promote.d.ts +0 -1
- package/dist/commands/promote.js +0 -53
- package/dist/commands/pull.d.ts +0 -2
- package/dist/commands/push.d.ts +0 -33
- package/dist/commands/react.d.ts +0 -2
- package/dist/commands/react.js +0 -40
- package/dist/commands/reverseTicket.d.ts +0 -2
- package/dist/commands/reverseTicket.js +0 -477
- package/dist/commands/search.d.ts +0 -2
- package/dist/commands/security.d.ts +0 -1
- package/dist/commands/security.js +0 -156
- package/dist/commands/send.d.ts +0 -2
- package/dist/commands/share.d.ts +0 -2
- package/dist/commands/shared.d.ts +0 -64
- package/dist/configDir.d.ts +0 -1
- package/dist/connect.d.ts +0 -1
- package/dist/connectFlow.d.ts +0 -13
- package/dist/cursorHooks.d.ts +0 -23
- package/dist/detection.d.ts +0 -30
- package/dist/hook.d.ts +0 -6
- package/dist/inboxFormat.d.ts +0 -38
- package/dist/limitEvent.d.ts +0 -16
- package/dist/limitEventReporter.d.ts +0 -21
- package/dist/mcp.d.ts +0 -12
- package/dist/prUrl.d.ts +0 -9
- package/dist/prUrl.js +0 -33
- package/dist/repoResolver.d.ts +0 -17
- package/dist/secret.d.ts +0 -1
- package/dist/security.d.ts +0 -102
- package/dist/security.js +0 -437
- package/dist/securityDeepScan.d.ts +0 -41
- package/dist/securityDeepScan.js +0 -248
- package/dist/securityDoctor.d.ts +0 -32
- package/dist/securityDoctor.js +0 -63
- package/dist/securityFeature.d.ts +0 -5
- package/dist/securityFeature.js +0 -7
- package/dist/securityInstall.d.ts +0 -63
- package/dist/securityInstall.js +0 -239
- package/dist/securityInventory.d.ts +0 -54
- package/dist/securityInventory.js +0 -556
- package/dist/securityPilot.d.ts +0 -19
- package/dist/securityPilot.js +0 -61
- package/dist/securityPolicy.d.ts +0 -33
- package/dist/securityPolicy.js +0 -188
- package/dist/securitySession.d.ts +0 -6
- package/dist/securitySession.js +0 -72
- package/dist/upgradeNudge.d.ts +0 -25
- package/dist/utils.d.ts +0 -342
- package/dist/version.d.ts +0 -1
- 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 # Inside a run: the run's token. A connected agent (running outside Codiedev): its own key.\n # Otherwise: your CLI token.\n key = os.environ.get(\"CODIEDEV_API_KEY\", \"\")\n token = os.environ.get(\"CODIEDEV_RUN_TOKEN\") or (key if key.startswith(\"cdvm_\") else None) or os.environ.get(\"CODIEDEV_TOKEN\")\n if not token:\n raise RuntimeError(\"Set CODIEDEV_API_KEY to your connected agent's key, or CODIEDEV_TOKEN to your 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 teams: \"list[str] | None\" = None,\n fields: \"list[dict] | 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) and teams\n (team names, e.g. [\"Finance\"]) limit who can decide (admins always can).\n Apps your company connects (Agents → Approvals → Apps) are told about it\n by webhook and can decide on a person's behalf.\n\n fields asks the person to fill something in as they decide, e.g.\n [{\"name\": \"rating\", \"type\": \"rating\"}, {\"name\": \"why\", \"type\": \"textarea\"}]\n (types: text, textarea, number, rating, select, multiselect, boolean;\n required, label, help, options, min, max, default). What they entered\n arrives as input[\"approval\"][\"values\"]. With fields and no options, the\n only choice is \"submit\".\"\"\"\n body: dict = {\"title\": title, \"details\": details, \"options\": options, \"approvers\": approvers, \"teams\": teams, \"fields\": fields, \"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 (['submit'] if fields else ['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 wait_for_approval(approval_id: str, timeout: float = 3600, poll: float = 15) -> dict:\n \"\"\"Wait until a person decides (or it expires), then return the approval. For agents that keep\n running while they wait, like a connected agent; a run inside Codiedev usually ends instead and\n is started again with the decision.\"\"\"\n deadline = time.time() + timeout\n while True:\n a = get_approval(approval_id)\n if a.get(\"status\") != \"pending\":\n return a\n if time.time() > deadline:\n raise TimeoutError(f\"approval {approval_id} still pending after {timeout}s\")\n time.sleep(poll)\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 // Inside a run: the run's token. A connected agent (running outside Codiedev): its own key.\n // Otherwise: your CLI token.\n const key = process.env.CODIEDEV_API_KEY || \"\";\n const t = process.env.CODIEDEV_RUN_TOKEN || (key.startsWith(\"cdvm_\") ? key : \"\") || process.env.CODIEDEV_TOKEN;\n if (!t) throw new Error(\"Set CODIEDEV_API_KEY to your connected agent's key, or CODIEDEV_TOKEN to your 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, teams, fields, then, context, expiresSec } = {}) {\n if (process.env.CODIEDEV_LOCAL === \"1\") {\n console.log(`[local] approval requested: ${title} (${(options || (fields ? [\"submit\"] : [\"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 ...(teams ? { teams } : {}),\n ...(fields ? { fields } : {}),\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/** Wait until a person decides (or it expires), then return the approval. For agents that keep\n * running while they wait, like a connected agent; a run inside Codiedev usually ends instead and\n * is started again with the decision. */\nexport async function waitForApproval(id, { timeoutMs = 3_600_000, pollMs = 15_000 } = {}) {\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const a = await getApproval(id);\n if (a.status !== \"pending\") return a;\n if (Date.now() > deadline) throw new Error(`approval ${id} still pending after ${timeoutMs / 1000}s`);\n await new Promise((resolve) => setTimeout(resolve, pollMs));\n }\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 /** The approval app that sent the decision on decidedBy's behalf; null when decided in Codiedev. */\n decidedVia: string | null;\n approvers: string[];\n approverTeams: Array<{ id: string; name: string }>;\n /** The form the person fills in, if the approval asked for one. */\n fields: ApprovalField[];\n /** What they entered, by field name (null until decided). */\n values: Record<string, any> | null;\n url: string | null;\n resumeRunId: string | null;\n}\n/** One thing a person fills in when they decide. */\nexport interface ApprovalField {\n name: string;\n type: \"text\" | \"textarea\" | \"number\" | \"rating\" | \"select\" | \"multiselect\" | \"boolean\";\n label?: string;\n help?: string;\n required?: boolean;\n /** select / multiselect choices */\n options?: string[];\n /** number bounds; rating scale 1–max (default 5) */\n min?: number;\n max?: number;\n /** prefilled, e.g. a proposed amount for the person to correct */\n default?: unknown;\n}\n/** The input a run started by a decision gets. */\nexport interface ApprovalInput<C = any, V = Record<string, any>> {\n approval: { id: string; title: string; decision: string; comment: string | null; values: V; decidedBy: string | null; decidedVia: 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[]; teams?: string[]; fields?: ApprovalField[]; then?: string | null; context?: C; expiresSec?: number }): Promise<Approval>;\nexport function getApproval(id: string): Promise<Approval>;\nexport function waitForApproval(id: string, opts?: { timeoutMs?: number; pollMs?: number }): 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";
|
package/dist/commands/ask.js
CHANGED
|
@@ -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(" ")
|
package/dist/commands/capture.js
CHANGED
|
@@ -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 {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
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(
|
|
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;
|
|
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" : "
|
|
256
|
+
status: readiness.gitOnPath ? "pass" : "warn",
|
|
317
257
|
detail: readiness.gitOnPath
|
|
318
258
|
? undefined
|
|
319
|
-
: "not found — capture can't
|
|
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;
|
|
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:
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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:
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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) {
|