codiedev 0.9.80 → 0.9.81
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/dist/cli.js +47 -0
- package/dist/commands/agentTemplates.generated.d.ts +11 -0
- package/dist/commands/agentTemplates.generated.js +139 -0
- package/dist/commands/agents.d.ts +7 -0
- package/dist/commands/agents.js +1130 -0
- package/dist/commands/agentsSdk.generated.d.ts +4 -0
- package/dist/commands/agentsSdk.generated.js +10 -0
- package/dist/commands/scrub.d.ts +11 -0
- package/dist/commands/scrub.js +102 -0
- package/dist/hook.js +18 -0
- package/dist/knownSecrets.d.ts +43 -0
- package/dist/knownSecrets.js +241 -0
- package/dist/localScrub.d.ts +172 -0
- package/dist/localScrub.js +820 -0
- package/dist/localScrubReport.d.ts +83 -0
- package/dist/localScrubReport.js +268 -0
- package/dist/scrub/extraRules.d.ts +4 -0
- package/dist/scrub/extraRules.js +17 -0
- package/dist/scrub/rules.d.ts +86 -0
- package/dist/scrub/rules.js +645 -0
- package/dist/scrub/scrubProviderRules.d.ts +14 -0
- package/dist/scrub/scrubProviderRules.js +149 -0
- package/package.json +5 -4
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const python = "\"\"\"Codiedev agents SDK \u2014 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 \u2192 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 \u2192 Approvals \u2192 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";
|
|
2
|
+
export declare const 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 \u2192 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";
|
|
3
|
+
export declare const 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\u2013max (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>;";
|
|
4
|
+
export declare const 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";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Generated by scripts/sync-agents-sdk.mjs. Don't edit; change the source
|
|
3
|
+
// (modal/agent_runtime, packages/codiedev-cli/agent-templates) and run
|
|
4
|
+
// `node scripts/sync-agents-sdk.mjs`.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.jsPackage = exports.jsTypes = exports.jsIndex = exports.python = void 0;
|
|
7
|
+
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";
|
|
8
|
+
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";
|
|
9
|
+
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>;";
|
|
10
|
+
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";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type RunSummary } from "../localScrub";
|
|
2
|
+
export declare const SCRUB_HELP = "codiedev scrub \u2014 remove keys from past agent sessions on this machine\n\nUsage:\n codiedev scrub [--dry-run] [--path <file>]... [--session <id>]... [--min-age <minutes>]\n\nCleans the session files Claude Code, Codex, Cursor, Copilot, OpenCode/Codie\nand pi keep locally, using the same rules as the Codiedev server. Each key is\nreplaced with [REDACTED:<kind>]. Sessions changed in the last hour are skipped\nso open sessions are never touched. When signed in, what was found (session,\nkey type and last 4, removed or not) shows on your Security page; the keys\nthemselves never leave this machine.\n\n --dry-run Count what would be cleaned; change nothing\n --path <file> Only this file (repeatable)\n --session <id> With --path <opencode.db>: only this OpenCode/Codie session\n --min-age <min> Skip files changed in the last <min> minutes (default 60)\n\nThe hourly background run can also remove your own secrets (values from\nyour credential files and .env files) if your admin allows it. It reads\n~/.aws/credentials, ~/.npmrc, ~/.pypirc, ~/.netrc, ~/.git-credentials,\n~/.config/gh/hosts.yml, ~/.docker/config.json, ~/.config/hub,\n~/.gem/credentials, ~/.config/openai/credentials, and .env / .env.* in the\nrepos your sessions start in. Values stay on this machine; only if your admin\nchose to check uploaded sessions too is a keyed fingerprint of each sent.\n";
|
|
3
|
+
export declare function parseScrubArgs(args: string[]): {
|
|
4
|
+
dryRun: boolean;
|
|
5
|
+
only?: string[];
|
|
6
|
+
onlySessions?: string[];
|
|
7
|
+
minAgeMs: number;
|
|
8
|
+
help: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function formatScrubSummary(s: RunSummary): string;
|
|
11
|
+
export declare function runScrubCommand(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SCRUB_HELP = void 0;
|
|
4
|
+
exports.parseScrubArgs = parseScrubArgs;
|
|
5
|
+
exports.formatScrubSummary = formatScrubSummary;
|
|
6
|
+
exports.runScrubCommand = runScrubCommand;
|
|
7
|
+
// `codiedev scrub`: run the local session scrub now, in the foreground.
|
|
8
|
+
// The same scrub runs on its own in the background from session-start hooks
|
|
9
|
+
// (hourly); this is for checking it or forcing a pass.
|
|
10
|
+
const localScrub_1 = require("../localScrub");
|
|
11
|
+
exports.SCRUB_HELP = `codiedev scrub — remove keys from past agent sessions on this machine
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
codiedev scrub [--dry-run] [--path <file>]... [--session <id>]... [--min-age <minutes>]
|
|
15
|
+
|
|
16
|
+
Cleans the session files Claude Code, Codex, Cursor, Copilot, OpenCode/Codie
|
|
17
|
+
and pi keep locally, using the same rules as the Codiedev server. Each key is
|
|
18
|
+
replaced with [REDACTED:<kind>]. Sessions changed in the last hour are skipped
|
|
19
|
+
so open sessions are never touched. When signed in, what was found (session,
|
|
20
|
+
key type and last 4, removed or not) shows on your Security page; the keys
|
|
21
|
+
themselves never leave this machine.
|
|
22
|
+
|
|
23
|
+
--dry-run Count what would be cleaned; change nothing
|
|
24
|
+
--path <file> Only this file (repeatable)
|
|
25
|
+
--session <id> With --path <opencode.db>: only this OpenCode/Codie session
|
|
26
|
+
--min-age <min> Skip files changed in the last <min> minutes (default 60)
|
|
27
|
+
|
|
28
|
+
The hourly background run can also remove your own secrets (values from
|
|
29
|
+
your credential files and .env files) if your admin allows it. It reads
|
|
30
|
+
~/.aws/credentials, ~/.npmrc, ~/.pypirc, ~/.netrc, ~/.git-credentials,
|
|
31
|
+
~/.config/gh/hosts.yml, ~/.docker/config.json, ~/.config/hub,
|
|
32
|
+
~/.gem/credentials, ~/.config/openai/credentials, and .env / .env.* in the
|
|
33
|
+
repos your sessions start in. Values stay on this machine; only if your admin
|
|
34
|
+
chose to check uploaded sessions too is a keyed fingerprint of each sent.
|
|
35
|
+
`;
|
|
36
|
+
function parseScrubArgs(args) {
|
|
37
|
+
let dryRun = false;
|
|
38
|
+
let help = false;
|
|
39
|
+
const only = [];
|
|
40
|
+
const sessions = [];
|
|
41
|
+
let minAgeMs = localScrub_1.MIN_AGE_MS;
|
|
42
|
+
for (let i = 0; i < args.length; i++) {
|
|
43
|
+
const a = args[i];
|
|
44
|
+
if (a === "--dry-run")
|
|
45
|
+
dryRun = true;
|
|
46
|
+
else if (a === "--help" || a === "-h")
|
|
47
|
+
help = true;
|
|
48
|
+
else if (a === "--path" && args[i + 1])
|
|
49
|
+
only.push(args[++i]);
|
|
50
|
+
else if (a === "--session" && args[i + 1])
|
|
51
|
+
sessions.push(args[++i]);
|
|
52
|
+
else if (a === "--min-age" && args[i + 1]) {
|
|
53
|
+
const m = Number(args[++i]);
|
|
54
|
+
if (Number.isFinite(m) && m >= 0)
|
|
55
|
+
minAgeMs = m * 60_000;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { dryRun, help, minAgeMs, ...(only.length ? { only } : {}), ...(sessions.length ? { onlySessions: sessions } : {}) };
|
|
59
|
+
}
|
|
60
|
+
function formatScrubSummary(s) {
|
|
61
|
+
const lines = [];
|
|
62
|
+
const verb = s.dryRun ? "Would remove" : "Removed";
|
|
63
|
+
lines.push(s.redactions === 0
|
|
64
|
+
? `No keys found in ${s.scanned} session file${s.scanned === 1 ? "" : "s"} checked.`
|
|
65
|
+
: `${verb} ${s.redactions} key${s.redactions === 1 ? "" : "s"}${s.dryRun ? "" : ` from ${s.cleaned} file${s.cleaned === 1 ? "" : "s"}`} (${s.scanned} checked).`);
|
|
66
|
+
for (const [h, t] of Object.entries(s.byHarness))
|
|
67
|
+
lines.push(` ${h}: ${t.redactions} in ${t.files} file${t.files === 1 ? "" : "s"}`);
|
|
68
|
+
if (s.skippedRecent)
|
|
69
|
+
lines.push(`Skipped ${s.skippedRecent} changed in the last hour (may be open).`);
|
|
70
|
+
if (s.skippedLarge)
|
|
71
|
+
lines.push(`Skipped ${s.skippedLarge} over 100 MB.`);
|
|
72
|
+
if (s.changedWhileCleaning)
|
|
73
|
+
lines.push(`Left ${s.changedWhileCleaning} alone: changed while cleaning.`);
|
|
74
|
+
if (s.errors)
|
|
75
|
+
lines.push(`${s.errors} couldn't be read or written.`);
|
|
76
|
+
if (s.unfinished)
|
|
77
|
+
lines.push("Stopped at the time limit; the next run continues.");
|
|
78
|
+
if (s.leftInCursor.keys > 0) {
|
|
79
|
+
const { keys, chats } = s.leftInCursor;
|
|
80
|
+
lines.push(`${keys} key${keys === 1 ? " is" : "s are"} still in ${chats} Cursor chat${chats === 1 ? "" : "s"}. ` +
|
|
81
|
+
"Cursor's own chat store can't be cleaned without breaking the chat; delete those chats in Cursor to remove them.");
|
|
82
|
+
}
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
async function runScrubCommand(args) {
|
|
86
|
+
const opts = parseScrubArgs(args);
|
|
87
|
+
if (opts.help) {
|
|
88
|
+
console.log(exports.SCRUB_HELP);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
const summary = await (0, localScrub_1.runAndReport)({
|
|
92
|
+
dryRun: opts.dryRun,
|
|
93
|
+
only: opts.only,
|
|
94
|
+
onlySessions: opts.onlySessions,
|
|
95
|
+
minAgeMs: opts.minAgeMs,
|
|
96
|
+
budgetMs: 10 * 60_000,
|
|
97
|
+
});
|
|
98
|
+
console.log(formatScrubSummary(summary));
|
|
99
|
+
if (summary.reported)
|
|
100
|
+
console.log("Sent to your Security page in Codiedev (key type and last 4 only, never the key).");
|
|
101
|
+
return summary.errors > 0 ? 1 : 0;
|
|
102
|
+
}
|
package/dist/hook.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
exports.buildDiscoveryContext = buildDiscoveryContext;
|
|
38
|
+
const localScrub_1 = require("./localScrub");
|
|
38
39
|
const captureSignals_1 = require("./captureSignals");
|
|
39
40
|
const transcriptModels_1 = require("./transcriptModels");
|
|
40
41
|
const fs = __importStar(require("fs"));
|
|
@@ -542,6 +543,18 @@ async function main() {
|
|
|
542
543
|
}
|
|
543
544
|
process.exit(0);
|
|
544
545
|
}
|
|
546
|
+
// Detached local session scrub (see localScrub.ts). No stdin.
|
|
547
|
+
if (process.argv[2] === "local-scrub") {
|
|
548
|
+
try {
|
|
549
|
+
const { runScheduledScrub } = await Promise.resolve().then(() => __importStar(require("./localScrub")));
|
|
550
|
+
// Off unless the person or their company turned it on.
|
|
551
|
+
await runScheduledScrub();
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
// best-effort — never throw out of the detached child
|
|
555
|
+
}
|
|
556
|
+
process.exit(0);
|
|
557
|
+
}
|
|
545
558
|
// Detached daily self-upgrade of this install (see selfUpdate.ts).
|
|
546
559
|
if (process.argv[2] === "self-update") {
|
|
547
560
|
await (0, selfUpdate_1.runSelfUpdate)(process.argv[1]);
|
|
@@ -613,6 +626,9 @@ async function main() {
|
|
|
613
626
|
// --- starter packs: Codex has no SessionStart hook; same hourly throttle ---
|
|
614
627
|
(0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
|
|
615
628
|
// --- end starter packs ---
|
|
629
|
+
// Local session scrub: Codex has no SessionStart, so its first prompt
|
|
630
|
+
// starts it (hourly throttle inside).
|
|
631
|
+
(0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()), cwd: hookInput.cwd });
|
|
616
632
|
}
|
|
617
633
|
if (process.argv[2] !== "discover-copilot")
|
|
618
634
|
await runDiscover(hookInput.prompt);
|
|
@@ -635,6 +651,8 @@ async function main() {
|
|
|
635
651
|
process.argv[2] === "session-start-pi") {
|
|
636
652
|
// --- starter packs: hourly detached `pack install --quiet` + daily nudge ---
|
|
637
653
|
(0, sync_1.maybeSpawnPackSync)({ connected: Boolean((0, utils_1.readConfig)()), cli: path.join(__dirname, "cli.js") });
|
|
654
|
+
// Local session scrub of past sessions on this machine (hourly, detached).
|
|
655
|
+
(0, localScrub_1.maybeSpawnLocalScrub)({ connected: Boolean((0, utils_1.readConfig)()), cwd: hookInput.cwd });
|
|
638
656
|
const packNag = process.argv[2] === "session-start-opencode" || process.argv[2] === "session-start-pi" ? null : (0, sync_1.packNagLine)();
|
|
639
657
|
// Claude Code adds SessionStart stdout to the session context.
|
|
640
658
|
if (packNag && process.argv[2] === "session-start")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ExtraRule } from "./scrub/rules";
|
|
2
|
+
export interface KnownSecret {
|
|
3
|
+
value: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
/** Where it came from: last two path parts, e.g. `api/.env`. */
|
|
6
|
+
file: string;
|
|
7
|
+
}
|
|
8
|
+
/** A value worth matching: long and random enough that it can't be an
|
|
9
|
+
* ordinary word, path or setting, and safe to fingerprint (a short or
|
|
10
|
+
* low-entropy value could be guessed back from its hash). */
|
|
11
|
+
export declare function worthMatching(v: string): boolean;
|
|
12
|
+
/** KEY=value lines (.env, .ini-style credentials, .npmrc, .pypirc). */
|
|
13
|
+
export declare function parseAssignments(text: string): Array<{
|
|
14
|
+
name: string;
|
|
15
|
+
value: string;
|
|
16
|
+
}>;
|
|
17
|
+
/** Per-user credential files, and .env files in the repos sessions ran in. */
|
|
18
|
+
/** Per-user credential files read, relative to home. Listed in the product
|
|
19
|
+
* (Team Security → Settings) and in `codiedev scrub --help`. */
|
|
20
|
+
export declare const CREDENTIAL_FILES: readonly [".aws/credentials", ".npmrc", ".pypirc", ".netrc", ".git-credentials", ".config/gh/hosts.yml", ".docker/config.json", ".config/hub", ".gem/credentials", ".config/openai/credentials"];
|
|
21
|
+
export declare function credentialFiles(home: string, cwds: readonly string[]): string[];
|
|
22
|
+
export declare function collectKnownSecrets(home?: string, cwds?: readonly string[]): KnownSecret[];
|
|
23
|
+
/** One scrub rule matching any of these exact values (and their JSON-escaped
|
|
24
|
+
* forms), on non-alphanumeric boundaries. */
|
|
25
|
+
export declare function knownSecretRule(secrets: readonly KnownSecret[]): ExtraRule | null;
|
|
26
|
+
/** Same as the server's knownHash: HMAC-SHA256 with the company's key (hex,
|
|
27
|
+
* from the scrub policy), first 32 hex. */
|
|
28
|
+
export declare function knownHash(keyHex: string, value: string): string;
|
|
29
|
+
export declare const hintOf: (value: string) => string;
|
|
30
|
+
export declare function fingerprints(keyHex: string, secrets: readonly KnownSecret[]): {
|
|
31
|
+
file: string;
|
|
32
|
+
name?: string | undefined;
|
|
33
|
+
hash: string;
|
|
34
|
+
hint: string;
|
|
35
|
+
}[];
|
|
36
|
+
/** Send keyed fingerprints (never values). Replaces this person's set.
|
|
37
|
+
* Only in "server" mode, with the key the server sent. Best-effort. */
|
|
38
|
+
export declare function uploadKnownSecrets(config: {
|
|
39
|
+
backendUrl?: string;
|
|
40
|
+
token?: string;
|
|
41
|
+
} | null, keyHex: string, secrets: readonly KnownSecret[], fetchImpl?: typeof fetch): Promise<boolean>;
|
|
42
|
+
export declare function rememberCwd(stateDir: string, cwd: string | undefined): void;
|
|
43
|
+
export declare function recentCwds(stateDir: string): string[];
|