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 CHANGED
@@ -13,6 +13,39 @@
13
13
  * codiedev note "<text>" — capture a thought
14
14
  * codiedev help — show this help
15
15
  */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
16
49
  Object.defineProperty(exports, "__esModule", { value: true });
17
50
  const connect_1 = require("./connect");
18
51
  const cliSession_1 = require("./cliSession");
@@ -34,6 +67,7 @@ const capture_1 = require("./commands/capture");
34
67
  const pack_1 = require("./commands/pack");
35
68
  const traces_1 = require("./commands/traces");
36
69
  const evals_1 = require("./commands/evals");
70
+ const agents_1 = require("./commands/agents");
37
71
  // --- starter packs phase 2 ---
38
72
  const hookGate_1 = require("./packs/hookGate");
39
73
  const sync_1 = require("./packs/sync");
@@ -98,6 +132,9 @@ Starter packs:
98
132
  codiedev pack status Installed packs, up to date or not
99
133
 
100
134
  Traces (agents on the gateway):
135
+ codiedev agents init | deploy | run | runs | show | secrets | webhook
136
+ Deploy agents and run them in fresh sandboxes
137
+ (codiedev agents help)
101
138
  codiedev traces agents Agents you maintain, with saved test cases
102
139
  codiedev traces cases <agent> [--dataset name [--version n]] [--model id]
103
140
  Pull an agent's test cases (or one dataset)
@@ -121,6 +158,7 @@ Privacy:
121
158
  codiedev ignore <path> Never capture this folder or its children
122
159
  codiedev unignore <path> Stop ignoring a folder
123
160
  codiedev ignored [path] List ignored folders, or check one path
161
+ codiedev scrub [--dry-run] Remove keys from past agent sessions on this machine
124
162
 
125
163
  Capture:
126
164
  codiedev note "<text>" Capture a passing thought
@@ -172,6 +210,7 @@ status sync never weakens it.
172
210
  const COMMAND_HELP = {
173
211
  pack: pack_1.PACK_HELP,
174
212
  traces: traces_1.TRACES_HELP,
213
+ agents: agents_1.AGENTS_HELP,
175
214
  evals: evals_1.EVALS_HELP,
176
215
  connect: `
177
216
  codiedev connect — link your agent CLI to Codiedev
@@ -569,9 +608,17 @@ async function main() {
569
608
  case "read":
570
609
  await (0, inbox_1.runRead)(rest);
571
610
  return;
611
+ case "scrub": {
612
+ const { runScrubCommand } = await Promise.resolve().then(() => __importStar(require("./commands/scrub")));
613
+ process.exitCode = await runScrubCommand(rest);
614
+ return;
615
+ }
572
616
  case "note":
573
617
  await (0, note_1.runNote)(rest);
574
618
  return;
619
+ case "agents":
620
+ await (0, agents_1.runAgents)(rest);
621
+ return;
575
622
  case "promote":
576
623
  case "reverse-ticket":
577
624
  case "reverseTicket":
@@ -0,0 +1,11 @@
1
+ export type AgentTemplate = {
2
+ id: string;
3
+ title: string;
4
+ summary: string;
5
+ trigger: string;
6
+ uses: string[];
7
+ lang: string;
8
+ adapt: string;
9
+ files: Record<string, string>;
10
+ };
11
+ export declare const TEMPLATES: AgentTemplate[];
@@ -0,0 +1,139 @@
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.TEMPLATES = void 0;
7
+ exports.TEMPLATES = [
8
+ {
9
+ "id": "support-triage",
10
+ "title": "Support triage",
11
+ "summary": "Triages each new ticket from your helpdesk, drafts a reply, and asks a person before it promises a refund.",
12
+ "trigger": "Webhook from Zendesk, Salesforce, Intercom or a form",
13
+ "uses": [
14
+ "webhook",
15
+ "approvals",
16
+ "steps",
17
+ "retries"
18
+ ],
19
+ "lang": "python",
20
+ "adapt": "point it at our helpdesk's webhook and post replies back through its API",
21
+ "files": {
22
+ "README.md": "# %NAME% (support-triage template)\n\nTriages each new support ticket, drafts a reply, and asks a person before\npromising a refund over `REFUND_APPROVAL_OVER` (default $50).\n\n## Try it\n\n```bash\ncodiedev agents run --local # sample ticket, on your machine\ncodiedev agents deploy && codiedev agents run %NAME% --input '{\"id\": \"42\", \"subject\": \"Charged twice\", \"body\": \"Please refund $89\", \"customer\": \"sam@example.com\"}'\n```\n\n## Connect your helpdesk\n\n1. `codiedev agents webhook %NAME%` gives a URL. In Zendesk (trigger → webhook),\n Salesforce (Flow → HTTP callout), Intercom or a form, POST the new ticket to it.\n2. `codiedev agents secrets %NAME% set SHARED_SECRET=…` and send the same value\n as an `X-Shared-Secret` header, so only your helpdesk can start runs.\n3. Replies go to `REPLY_URL` (+ `REPLY_TOKEN`) as `{ticketId, reply, internalNote}`.\n Until it's set the agent does a dry run and logs the reply. Change\n `post_reply()` to call your helpdesk's API directly (Zendesk ticket comments,\n Salesforce CaseComment).\n\nRefund decisions show under Agents → Approvals in Codiedev; the approval's\n`url` can also be posted to Slack or email.\n",
23
+ "agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.py\",\n \"timeoutSec\": 300,\n \"retries\": 2\n}\n",
24
+ "agent.py": "\"\"\"%NAME% — triage a support ticket, draft a reply, and ask a person before\npromising a refund. Built from the support-triage template; see README.md.\n\nStarted by a helpdesk webhook (or `codiedev agents run %NAME% --input '{...}'`):\n 1. classify the ticket (priority, category) and draft a reply with a model\n 2. refund over REFUND_APPROVAL_OVER? ask a person (agents.request_approval)\n and stop; their decision starts a new run that posts the right reply\n 3. otherwise post the reply back to the helpdesk\n\"\"\"\n\nimport hmac\nimport json\nimport os\nimport urllib.request\n\nimport codiedev_agents as agents\n\nMODEL = os.environ.get(\"MODEL\", \"%MODEL%\")\nREFUND_APPROVAL_OVER = float(os.environ.get(\"REFUND_APPROVAL_OVER\", \"50\"))\n\nTRIAGE_PROMPT = \"\"\"You triage customer support tickets. Reply with JSON only:\n{\n \"priority\": \"p1\" | \"p2\" | \"p3\" | \"p4\", // p1 = outage or data loss\n \"category\": \"billing\" | \"bug\" | \"how-to\" | \"account\" | \"other\",\n \"summary\": \"<one sentence>\",\n \"refund_requested\": true | false,\n \"refund_amount\": <number or null>,\n \"reply\": \"<a warm, specific reply to the customer>\",\n \"reply_without_refund\": \"<the same reply, but politely declining any refund>\"\n}\"\"\"\n\n\ndef ask_json(system: str, user: str) -> dict:\n llm = agents.openai()\n res = llm.chat.completions.create(\n model=MODEL,\n messages=[{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": user}],\n response_format={\"type\": \"json_object\"},\n )\n text = res.choices[0].message.content or \"{}\"\n return json.loads(text[text.find(\"{\") : text.rfind(\"}\") + 1])\n\n\ndef check_sender() -> None:\n \"\"\"Webhook runs: when SHARED_SECRET is set, the sender must send it as X-Shared-Secret.\"\"\"\n req, secret = agents.request(), os.environ.get(\"SHARED_SECRET\")\n if req and secret:\n sent = {k.lower(): v for k, v in req[\"headers\"].items()}.get(\"x-shared-secret\", \"\")\n if not hmac.compare_digest(sent, secret):\n raise SystemExit(\"Rejected: missing or wrong X-Shared-Secret header\")\n\n\ndef normalize(payload) -> dict:\n \"\"\"Accepts {id, subject, body, customer} or a Zendesk / Salesforce-style payload.\"\"\"\n if not isinstance(payload, dict):\n return {\"id\": \"unknown\", \"subject\": \"\", \"body\": str(payload or \"\"), \"customer\": \"\"}\n t = payload.get(\"ticket\") or payload.get(\"case\") or payload\n requester = t.get(\"requester\") or {}\n return {\n \"id\": str(t.get(\"id\") or t.get(\"Id\") or t.get(\"CaseNumber\") or \"unknown\"),\n \"subject\": t.get(\"subject\") or t.get(\"Subject\") or \"\",\n \"body\": t.get(\"body\") or t.get(\"description\") or t.get(\"Description\") or \"\",\n \"customer\": t.get(\"customer\") or requester.get(\"email\") or t.get(\"SuppliedEmail\") or \"\",\n }\n\n\ndef post_reply(ticket_id: str, text: str, note: str = \"\") -> dict:\n \"\"\"Posts the reply to REPLY_URL (your helpdesk's API, or a small relay) when\n it's set; otherwise just logs it, so you can try the agent first.\"\"\"\n url = os.environ.get(\"REPLY_URL\")\n if not url:\n print(f\"[dry run] would reply on {ticket_id}:\\n{text}\\n{note}\")\n return {\"posted\": False, \"dryRun\": True}\n body = json.dumps({\"ticketId\": ticket_id, \"reply\": text, \"internalNote\": note}).encode()\n headers = {\"Content-Type\": \"application/json\"}\n if os.environ.get(\"REPLY_TOKEN\"):\n headers[\"Authorization\"] = f\"Bearer {os.environ['REPLY_TOKEN']}\"\n with urllib.request.urlopen(urllib.request.Request(url, data=body, headers=headers, method=\"POST\"), timeout=30) as res:\n return {\"posted\": True, \"status\": res.status}\n\n\ntask = agents.input() or {}\n\n# A person decided on a refund: post the matching reply.\nif \"approval\" in task:\n decision, ctx = task[\"approval\"], task[\"context\"]\n approved = decision[\"decision\"] == \"approve\"\n text = ctx[\"reply\"] if approved else ctx[\"reply_without_refund\"]\n note = f\"Refund {decision['decision']} by {decision['decidedBy']}\" + (f\": {decision['comment']}\" if decision.get(\"comment\") else \"\")\n # step(): if this run is retried, a reply that already went out isn't sent twice.\n result = agents.step(\"post-reply\", lambda: post_reply(ctx[\"ticket\"][\"id\"], text, note))\n agents.output({\"ticket\": ctx[\"ticket\"][\"id\"], \"refund\": decision[\"decision\"], **result})\n raise SystemExit(0)\n\ncheck_sender()\nticket = normalize(task or {\"id\": \"demo-1\", \"subject\": \"Charged twice\", \"body\": \"I was billed twice for March ($89). Please refund one.\", \"customer\": \"sam@example.com\"})\ntriage = ask_json(TRIAGE_PROMPT, json.dumps(ticket))\nprint(f\"{ticket['id']}: {triage.get('priority')} {triage.get('category')} · {triage.get('summary')}\")\n\namount = triage.get(\"refund_amount\") or 0\nif triage.get(\"refund_requested\") and amount > REFUND_APPROVAL_OVER:\n approval = agents.step(\"ask\", lambda: agents.request_approval(\n f\"Refund ${amount:,.2f} to {ticket['customer'] or ticket['id']}?\",\n details={\"ticket\": ticket, \"summary\": triage.get(\"summary\"), \"draftReply\": triage.get(\"reply\")},\n context={\"ticket\": ticket, \"reply\": triage[\"reply\"], \"reply_without_refund\": triage[\"reply_without_refund\"]},\n ))\n print(f\"Waiting for a decision: {approval.get('url')}\")\n agents.output({\"ticket\": ticket[\"id\"], \"triage\": triage, \"waitingForApproval\": approval.get(\"url\")})\nelse:\n result = agents.step(\"post-reply\", lambda: post_reply(ticket[\"id\"], triage[\"reply\"], f\"{triage.get('priority')} {triage.get('category')}\"))\n agents.output({\"ticket\": ticket[\"id\"], \"triage\": triage, **result})\n",
25
+ "requirements.txt": "# Preinstalled: openai, httpx, pydantic, mcp. Add more here.\n"
26
+ }
27
+ },
28
+ {
29
+ "id": "slack-bot",
30
+ "title": "Slack bot",
31
+ "summary": "Answers when someone @-mentions it in Slack, in the thread, and remembers each thread's conversation.",
32
+ "trigger": "Slack Events API webhook",
33
+ "uses": [
34
+ "webhook",
35
+ "signatures",
36
+ "state"
37
+ ],
38
+ "lang": "ts",
39
+ "adapt": "give it our team's context and the tools it should use",
40
+ "files": {
41
+ "README.md": "# %NAME% (slack-bot template)\n\nAnswers when someone @-mentions it in Slack, in the thread, with each\nthread's conversation remembered (agents.state).\n\n## Try it\n\n```bash\ncodiedev agents run --local # a sample mention; prints the reply instead of posting\n```\n\n## Connect Slack\n\n1. Create a Slack app (api.slack.com/apps). Bot token scopes: `app_mentions:read`, `chat:write`.\n2. Deploy, then `codiedev agents webhook %NAME%` for a URL.\n3. Event Subscriptions → Request URL: paste the URL (Slack's check is answered\n for you). Subscribe to the `app_mention` bot event. Install the app.\n4. Secrets:\n ```bash\n codiedev agents secrets %NAME% set SLACK_SIGNING_SECRET=… # Basic Information\n codiedev agents secrets %NAME% set SLACK_BOT_TOKEN=xoxb-… # OAuth & Permissions\n ```\n\n`SYSTEM_PROMPT` (a secret or env) changes its personality. To give it tools,\ncall your APIs or an MCP server before answering (see the mcp-research template).\n",
42
+ "agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.ts\",\n \"timeoutSec\": 120,\n \"retries\": 1\n}\n",
43
+ "agent.ts": "// %NAME% — a Slack bot. Built from the slack-bot template; see README.md.\n//\n// Slack sends each @-mention to this agent's webhook (Events API). The agent\n// checks Slack's signature, ignores Slack's retries of the same event, answers\n// in the thread with a model, and keeps each thread's conversation in state\n// so follow-up questions have context.\nimport crypto from \"node:crypto\";\nimport * as agents from \"codiedev-agents\";\n\nconst MODEL = process.env.MODEL ?? \"%MODEL%\";\nconst SYSTEM = process.env.SYSTEM_PROMPT ?? \"You are a helpful teammate in Slack. Answer briefly and concretely. Use Slack mrkdwn.\";\n\ntype SlackEvent = { type?: string; text?: string; channel?: string; ts?: string; thread_ts?: string; bot_id?: string };\ntype Payload = { type?: string; event_id?: string; event?: SlackEvent };\ntype Turn = { role: \"user\" | \"assistant\"; content: string };\n\nfunction verifySlack(req: agents.WebhookRequest): void {\n const secret = process.env.SLACK_SIGNING_SECRET;\n if (!secret) throw new Error(\"Set SLACK_SIGNING_SECRET: codiedev agents secrets %NAME% set SLACK_SIGNING_SECRET=…\");\n const h = Object.fromEntries(Object.entries(req.headers).map(([k, v]) => [k.toLowerCase(), v]));\n const ts = h[\"x-slack-request-timestamp\"];\n const sig = h[\"x-slack-signature\"] ?? \"\";\n if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error(\"Rejected: unsigned or stale Slack request\");\n const mine = \"v0=\" + crypto.createHmac(\"sha256\", secret).update(`v0:${ts}:${req.body}`).digest(\"hex\");\n if (sig.length !== mine.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(mine))) throw new Error(\"Rejected: bad Slack signature\");\n}\n\nasync function postToSlack(channel: string, thread: string, text: string) {\n const token = process.env.SLACK_BOT_TOKEN;\n if (!token) {\n console.log(`[dry run] would post in ${channel} (thread ${thread}):\\n${text}`);\n return { posted: false };\n }\n const res = await fetch(\"https://slack.com/api/chat.postMessage\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json; charset=utf-8\", Authorization: `Bearer ${token}` },\n body: JSON.stringify({ channel, thread_ts: thread, text }),\n });\n const body = (await res.json()) as { ok: boolean; error?: string; ts?: string };\n if (!body.ok) throw new Error(`Slack chat.postMessage: ${body.error}`);\n return { posted: true, ts: body.ts };\n}\n\nconst req = agents.request();\nif (req) verifySlack(req);\n\nconst payload = agents.input<Payload>({\n type: \"event_callback\",\n event_id: `demo-${Date.now()}`,\n event: { type: \"app_mention\", text: \"<@U0BOT> what can you do?\", channel: \"C0DEMO\", ts: \"1.0\" },\n});\nconst event = payload.event ?? {};\nif (payload.type !== \"event_callback\" || event.type !== \"app_mention\" || event.bot_id || !event.channel || !event.ts) {\n agents.output({ ignored: true, type: payload.type ?? null });\n} else {\n // Slack retries a delivery it thinks failed: handle each event once.\n let duplicate = false;\n try {\n await agents.state.set(`event/${payload.event_id}`, true, { ifVersion: 0, ttlSec: 86_400 });\n } catch (err) {\n if (!(err instanceof agents.StateConflict)) throw err;\n duplicate = true;\n }\n if (duplicate) {\n agents.output({ duplicate: true });\n } else {\n const thread = event.thread_ts ?? event.ts;\n const historyKey = `thread/${event.channel}/${thread}`;\n const history = await agents.state.get<Turn[]>(historyKey, []);\n const question = String(event.text ?? \"\").replace(/<@[A-Z0-9]+>/g, \"\").trim();\n const llm = await agents.openai();\n const res = await llm.chat.completions.create({\n model: MODEL,\n messages: [{ role: \"system\", content: SYSTEM }, ...history, { role: \"user\", content: question }],\n });\n const answer = res.choices[0].message.content ?? \"\";\n await agents.state.set(historyKey, [...history, { role: \"user\", content: question }, { role: \"assistant\", content: answer }].slice(-20), { ttlSec: 7 * 86_400 });\n const posted = await agents.step(\"post\", () => postToSlack(event.channel!, thread, answer));\n agents.output({ channel: event.channel, thread, answer, ...posted });\n }\n}\n",
44
+ "package.json": "{\n \"name\": \"%NAME%\",\n \"private\": true,\n \"type\": \"module\",\n \"dependencies\": {}\n}\n"
45
+ }
46
+ },
47
+ {
48
+ "id": "daily-digest",
49
+ "title": "Daily digest",
50
+ "summary": "Every morning, reads your feeds, keeps only what's new since yesterday, and posts a short digest to Slack.",
51
+ "trigger": "Schedule (every day)",
52
+ "uses": [
53
+ "schedule",
54
+ "state"
55
+ ],
56
+ "lang": "python",
57
+ "adapt": "follow our sources (changelogs, status pages, blogs) and post to our team channel",
58
+ "files": {
59
+ "README.md": "# %NAME% (daily-digest template)\n\nEvery day, reads the feeds in `feeds.json` (RSS or Atom), keeps only items it\nhasn't covered before (remembered in agents.state), and posts a digest.\n\n```bash\ncodiedev agents run --local # try it here\ncodiedev agents secrets %NAME% set SLACK_WEBHOOK_URL=https://hooks.slack.com/…\ncodiedev agents deploy # then it runs daily\n```\n\nChange the schedule with `\"every\"` in agent.json (`\"1h\"`, `\"7d\"`), or pass\n`{\"feeds\": [...]}` as input for a one-off run.\n",
60
+ "agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.py\",\n \"timeoutSec\": 300,\n \"every\": \"1d\",\n \"retries\": 2\n}\n",
61
+ "agent.py": "\"\"\"%NAME% — a daily digest of what's new in your feeds. Built from the\ndaily-digest template; see README.md.\n\nRuns every day (agent.json \"every\": \"1d\"). Remembers which items it has\nalready covered (agents.state), so each digest only has what's new.\n\"\"\"\n\nimport json\nimport os\nimport urllib.request\nimport xml.etree.ElementTree as ET\n\nimport codiedev_agents as agents\n\nMODEL = os.environ.get(\"MODEL\", \"%MODEL%\")\nMAX_ITEMS = 25\nATOM = \"{http://www.w3.org/2005/Atom}\"\n\n\ndef fetch(url: str) -> bytes:\n req = urllib.request.Request(url, headers={\"User-Agent\": \"codiedev-agent/1.0\"})\n with urllib.request.urlopen(req, timeout=20) as res:\n return res.read()\n\n\ndef items(url: str) -> list[dict]:\n \"\"\"RSS <item>s or Atom <entry>s as {id, title, link, summary}.\"\"\"\n root = ET.fromstring(fetch(url))\n out = []\n for node in root.iter():\n if node.tag == \"item\":\n link = node.findtext(\"link\") or \"\"\n out.append({\"id\": node.findtext(\"guid\") or link, \"title\": node.findtext(\"title\") or \"\", \"link\": link, \"summary\": (node.findtext(\"description\") or \"\")[:500]})\n elif node.tag == f\"{ATOM}entry\":\n link_el = node.find(f\"{ATOM}link\")\n link = link_el.get(\"href\", \"\") if link_el is not None else \"\"\n out.append({\"id\": node.findtext(f\"{ATOM}id\") or link, \"title\": node.findtext(f\"{ATOM}title\") or \"\", \"link\": link, \"summary\": (node.findtext(f\"{ATOM}summary\") or \"\")[:500]})\n return out\n\n\ndef post_to_slack(text: str) -> dict:\n url = os.environ.get(\"SLACK_WEBHOOK_URL\")\n if not url:\n print(f\"[dry run] would post to Slack:\\n{text}\")\n return {\"posted\": False}\n body = json.dumps({\"text\": text}).encode()\n with urllib.request.urlopen(urllib.request.Request(url, data=body, headers={\"Content-Type\": \"application/json\"}), timeout=20) as res:\n return {\"posted\": res.status == 200}\n\n\nfeeds = (agents.input() or {}).get(\"feeds\") or json.load(open(os.path.join(os.path.dirname(__file__), \"feeds.json\")))\nseen = set(agents.state.get(\"seen\", []))\n\nfresh = []\nfor url in feeds:\n try:\n for item in items(url):\n if item[\"id\"] and item[\"id\"] not in seen:\n fresh.append({**item, \"source\": url})\n except Exception as err: # one broken feed shouldn't stop the digest\n print(f\"skipped {url}: {err}\")\nfresh = fresh[:MAX_ITEMS]\nprint(f\"{len(fresh)} new items across {len(feeds)} feeds\")\n\nif not fresh:\n agents.output({\"new\": 0})\n raise SystemExit(0)\n\nllm = agents.openai()\nres = llm.chat.completions.create(\n model=MODEL,\n messages=[\n {\"role\": \"system\", \"content\": \"Write a short daily digest for an engineering team. Group related items, one line each with its link, most important first. Skip marketing fluff. Slack mrkdwn.\"},\n {\"role\": \"user\", \"content\": json.dumps(fresh)},\n ],\n)\ndigest = res.choices[0].message.content or \"\"\nposted = agents.step(\"post\", lambda: post_to_slack(digest))\n\n# Remember what was covered (the newest 2,000 ids), after posting.\nagents.state.set(\"seen\", (list(seen) + [i[\"id\"] for i in fresh])[-2000:])\nagents.output({\"new\": len(fresh), \"digest\": digest, **posted})\n",
62
+ "feeds.json": "[\n \"https://github.blog/changelog/feed/\",\n \"https://hnrss.org/best\",\n \"https://blog.python.org/feeds/posts/default\"\n]\n",
63
+ "requirements.txt": "# Preinstalled: openai, httpx, pydantic, mcp. Add more here.\n"
64
+ }
65
+ },
66
+ {
67
+ "id": "doc-to-json",
68
+ "title": "Doc to JSON API",
69
+ "summary": "Turns any document (an invoice, a contract, an email) into validated JSON, answered synchronously over HTTP.",
70
+ "trigger": "Webhook with ?wait (a synchronous API)",
71
+ "uses": [
72
+ "webhook reply",
73
+ "retries"
74
+ ],
75
+ "lang": "python",
76
+ "adapt": "extract the fields we need from our documents",
77
+ "files": {
78
+ "README.md": "# %NAME% (doc-to-json template)\n\nA synchronous extraction API: POST a document, get validated JSON back.\n\n```bash\ncodiedev agents run --local # sample invoice (needs pydantic locally)\ncodiedev agents deploy && codiedev agents webhook %NAME%\ncurl -X POST \"<webhook-url>?wait=60\" -H 'Content-Type: application/json' \\\n -d '{\"text\": \"Invoice #77 from Globex, total $1,250.00\"}'\n```\n\nCustom fields: `{\"text\": \"...\", \"fields\": {\"party\": \"who signed\", \"renewal_date\": \"ISO date\"}}`.\nChange the `Invoice` model for a fixed schema of your own; a reply that\ndoesn't validate fails the attempt and retries once.\n",
79
+ "agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.py\",\n \"timeoutSec\": 120,\n \"retries\": 1\n}\n",
80
+ "agent.py": "\"\"\"%NAME% — extract structured JSON from a document. Built from the\ndoc-to-json template; see README.md.\n\nInput: {\"text\": \"...\"} or {\"url\": \"https://...\"}; optionally \"fields\":\n{\"name\": \"description\", ...} to extract instead of the invoice default.\nPOST it to the webhook URL with ?wait=60 and the JSON comes back as the\nHTTP response (agents.respond).\n\"\"\"\n\nimport json\nimport os\nimport urllib.request\n\nfrom pydantic import BaseModel, Field\n\nimport codiedev_agents as agents\n\nMODEL = os.environ.get(\"MODEL\", \"%MODEL%\")\n\n\nclass LineItem(BaseModel):\n description: str\n quantity: float = 1\n amount: float\n\n\nclass Invoice(BaseModel):\n vendor: str\n invoice_number: str | None = None\n date: str | None = Field(None, description=\"ISO 8601\")\n currency: str = \"USD\"\n total: float\n line_items: list[LineItem] = []\n\n\ntask = agents.input() or {\"text\": \"ACME Corp — Invoice #1042 — 2026-10-01\\n2 × Widget @ $40.00 = $80.00\\nSupport plan $120.00\\nTotal due: $200.00 USD\"}\ntext = task.get(\"text\") or \"\"\nif task.get(\"url\"):\n with urllib.request.urlopen(task[\"url\"], timeout=30) as res:\n text = res.read().decode(errors=\"replace\")[:100_000]\nif not text.strip():\n agents.respond({\"error\": \"Send {\\\"text\\\": ...} or {\\\"url\\\": ...}\"}, status=400)\n raise SystemExit(0)\n\ncustom = task.get(\"fields\")\nschema = {\"type\": \"object\", \"properties\": {k: {\"description\": v} for k, v in custom.items()}} if custom else Invoice.model_json_schema()\n\nllm = agents.openai()\nres = llm.chat.completions.create(\n model=MODEL,\n messages=[\n {\"role\": \"system\", \"content\": f\"Extract the data from the document as JSON matching this schema. Use null for anything missing. JSON only.\\n{json.dumps(schema)}\"},\n {\"role\": \"user\", \"content\": text},\n ],\n response_format={\"type\": \"json_object\"},\n)\nraw = res.choices[0].message.content or \"{}\"\ndata = json.loads(raw[raw.find(\"{\") : raw.rfind(\"}\") + 1])\n# Validate the default shape: a malformed answer raises, and the run retries.\nresult = data if custom else Invoice.model_validate(data).model_dump()\n\nagents.respond(result)\nagents.output(result)\n",
81
+ "requirements.txt": "# Preinstalled: openai, httpx, pydantic, mcp. Add more here.\n"
82
+ }
83
+ },
84
+ {
85
+ "id": "research-fanout",
86
+ "title": "Research fan-out",
87
+ "summary": "Splits a question into sub-questions, researches them in parallel child runs, and writes one answer.",
88
+ "trigger": "On demand (CLI, API, another agent)",
89
+ "uses": [
90
+ "chaining",
91
+ "parallel runs"
92
+ ],
93
+ "lang": "python",
94
+ "adapt": "research the questions our team asks most, using our docs or APIs in the worker step",
95
+ "files": {
96
+ "README.md": "# %NAME% (research-fanout template)\n\nPlans sub-questions, runs each as a parallel child run of this same agent,\nthen writes one answer. The pattern for any \"split, work in parallel,\ncombine\" job: reviewing many files, enriching a list of accounts, comparing vendors.\n\n```bash\ncodiedev agents deploy\ncodiedev agents run %NAME% --input '{\"question\": \"How do teams cut CI time in half?\"}'\ncodiedev agents show <run-id> # lists the child runs\n```\n\nWorkers need the deployed agent (they're real runs), so try the fan-out\nafter `deploy`. Change `research()` to use a search API, your docs or an\nMCP server.\n",
97
+ "agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.py\",\n \"timeoutSec\": 900,\n \"retries\": 1\n}\n",
98
+ "agent.py": "\"\"\"%NAME% — fan a question out to parallel workers, then synthesize. Built\nfrom the research-fanout template; see README.md.\n\nOne folder, two roles: started with {\"question\": ...} it plans and fans\nout; each sub-question runs as a CHILD run of this same agent with\n{\"mode\": \"worker\"}. Children show under the parent in Codiedev.\n\"\"\"\n\nimport json\nimport os\nimport time\n\nimport codiedev_agents as agents\n\nMODEL = os.environ.get(\"MODEL\", \"%MODEL%\")\nSELF = os.environ.get(\"CODIEDEV_AGENT\", \"%NAME%\")\n\n\ndef ask(system: str, user: str, as_json: bool = False) -> str:\n llm = agents.openai()\n res = llm.chat.completions.create(\n model=MODEL,\n messages=[{\"role\": \"system\", \"content\": system}, {\"role\": \"user\", \"content\": user}],\n **({\"response_format\": {\"type\": \"json_object\"}} if as_json else {}),\n )\n return res.choices[0].message.content or \"\"\n\n\ndef research(question: str) -> str:\n \"\"\"One sub-question. Swap this for a search API, your docs or an MCP tool.\"\"\"\n return ask(\"Answer the question thoroughly but concisely. Say what you're unsure about.\", question)\n\n\ntask = agents.input() or {}\n\nif task.get(\"mode\") == \"worker\":\n agents.output({\"question\": task[\"question\"], \"answer\": research(task[\"question\"])})\n raise SystemExit(0)\n\nquestion = task.get(\"question\") or \"What should a 50-person engineering team know before adopting AI coding agents?\"\nplan = ask('Split the question into 3–5 focused, non-overlapping sub-questions. JSON: {\"subquestions\": [\"...\"]}', question, as_json=True)\nsubs = json.loads(plan[plan.find(\"{\") : plan.rfind(\"}\") + 1])[\"subquestions\"][:5]\nprint(f\"Fanning out {len(subs)} sub-questions\")\n\n# Start every worker first, then wait: they run in parallel.\nrun_ids = [agents.start(SELF, {\"mode\": \"worker\", \"question\": q}) for q in subs]\nresults: dict[str, dict] = {}\ndeadline = time.monotonic() + 600\nwhile len(results) < len(run_ids) and time.monotonic() < deadline:\n for run_id in run_ids:\n if run_id not in results:\n run = agents.get(run_id)\n if run[\"status\"] in (\"succeeded\", \"failed\", \"cancelled\"):\n results[run_id] = run\n print(f\" {run['status']}: {run_id}\")\n time.sleep(2)\n\nfindings = [r[\"output\"] for r in results.values() if r[\"status\"] == \"succeeded\" and isinstance(r.get(\"output\"), dict)]\nanswer = ask(\n \"Write one clear answer to the question from these findings. Lead with the conclusion. Note disagreements.\",\n json.dumps({\"question\": question, \"findings\": findings}),\n)\nprint(answer)\nagents.output({\"question\": question, \"answer\": answer, \"subquestions\": subs, \"workers\": len(findings)})\n",
99
+ "requirements.txt": "# Preinstalled: openai, httpx, pydantic, mcp. Add more here.\n"
100
+ }
101
+ },
102
+ {
103
+ "id": "uptime-watch",
104
+ "title": "Uptime watch",
105
+ "summary": "Checks your endpoints every 15 minutes and posts to Slack only when something goes down or comes back, with a plain-English note.",
106
+ "trigger": "Schedule (every 15 minutes)",
107
+ "uses": [
108
+ "schedule",
109
+ "state"
110
+ ],
111
+ "lang": "python",
112
+ "adapt": "watch our services' health endpoints",
113
+ "files": {
114
+ "README.md": "# %NAME% (uptime-watch template)\n\nChecks the endpoints in `checks.json` every 15 minutes. Posts to Slack only\nwhen one goes down or recovers (it remembers the last result in agents.state).\n\n```bash\ncodiedev agents run --local\ncodiedev agents secrets %NAME% set SLACK_WEBHOOK_URL=https://hooks.slack.com/…\ncodiedev agents deploy\n```\n\nEdit `checks.json` (`name`, `url`, `expect` status). Change `\"every\"` in agent.json.\n",
115
+ "agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.py\",\n \"timeoutSec\": 120,\n \"every\": \"15m\"\n}\n",
116
+ "agent.py": "\"\"\"%NAME% — watch endpoints and report changes. Built from the uptime-watch\ntemplate; see README.md.\n\nEvery run checks each endpoint in checks.json, compares with the last run\n(agents.state) and only alerts on a change: down, or back up.\n\"\"\"\n\nimport json\nimport os\nimport time\nimport urllib.error\nimport urllib.request\n\nimport codiedev_agents as agents\n\nMODEL = os.environ.get(\"MODEL\", \"%MODEL%\")\n\n\ndef check(c: dict) -> dict:\n started = time.monotonic()\n try:\n with urllib.request.urlopen(urllib.request.Request(c[\"url\"], headers={\"User-Agent\": \"codiedev-uptime/1.0\"}), timeout=15) as res:\n status = res.status\n except urllib.error.HTTPError as err:\n status = err.code\n except Exception as err: # DNS, TLS, timeout\n return {\"up\": False, \"status\": None, \"error\": str(err)[:200], \"ms\": None}\n ms = round((time.monotonic() - started) * 1000)\n return {\"up\": status == c.get(\"expect\", 200), \"status\": status, \"error\": None, \"ms\": ms}\n\n\ndef post_to_slack(text: str) -> dict:\n url = os.environ.get(\"SLACK_WEBHOOK_URL\")\n if not url:\n print(f\"[dry run] would post to Slack:\\n{text}\")\n return {\"posted\": False}\n body = json.dumps({\"text\": text}).encode()\n with urllib.request.urlopen(urllib.request.Request(url, data=body, headers={\"Content-Type\": \"application/json\"}), timeout=20):\n return {\"posted\": True}\n\n\nchecks = json.load(open(os.path.join(os.path.dirname(__file__), \"checks.json\")))\nbefore = agents.state.get(\"last\", {})\nnow = {c[\"name\"]: {**check(c), \"url\": c[\"url\"]} for c in checks}\nfor name, r in now.items():\n print(f\"{'UP ' if r['up'] else 'DOWN'} {name}: {r['status'] or r['error']} {r['ms'] or ''}ms\")\n\nchanges = [\n {\"name\": name, \"now\": r, \"before\": before.get(name)}\n for name, r in now.items()\n if name in before and before[name][\"up\"] != r[\"up\"]\n] + [{\"name\": name, \"now\": r, \"before\": None} for name, r in now.items() if name not in before and not r[\"up\"]]\n\nposted = {\"posted\": False}\nif changes:\n llm = agents.openai()\n res = llm.chat.completions.create(\n model=MODEL,\n messages=[\n {\"role\": \"system\", \"content\": \"Write a short Slack alert (mrkdwn) for these uptime changes: what went down or recovered, the status or error in plain English, and the URL. No preamble.\"},\n {\"role\": \"user\", \"content\": json.dumps(changes)},\n ],\n )\n posted = agents.step(\"alert\", lambda: post_to_slack(res.choices[0].message.content or json.dumps(changes)))\n\nagents.state.set(\"last\", now)\nagents.output({\"down\": [n for n, r in now.items() if not r[\"up\"]], \"changes\": len(changes), **posted})\n",
117
+ "checks.json": "[\n { \"name\": \"Website\", \"url\": \"https://example.com\", \"expect\": 200 },\n { \"name\": \"API health\", \"url\": \"https://httpbin.org/status/200\", \"expect\": 200 }\n]\n",
118
+ "requirements.txt": "# Preinstalled: openai, httpx, pydantic, mcp. Add more here.\n"
119
+ }
120
+ },
121
+ {
122
+ "id": "mcp-research",
123
+ "title": "MCP tool agent",
124
+ "summary": "A model with tools: it starts an MCP server, gives the model its tools, and loops until it has an answer. Ships with the fetch server so it can read the web.",
125
+ "trigger": "On demand (CLI, API, another agent)",
126
+ "uses": [
127
+ "MCP",
128
+ "tool calling"
129
+ ],
130
+ "lang": "python",
131
+ "adapt": "use the MCP server for our system (Jira, Postgres, GitHub, our own) instead of fetch",
132
+ "files": {
133
+ "README.md": "# %NAME% (mcp-research template)\n\nA tool-using agent: it starts an MCP server, gives its tools to the model\nand loops until there's an answer. Ships with `mcp-server-fetch`, so it can\nread web pages.\n\n```bash\ncodiedev agents deploy\ncodiedev agents run %NAME% --input '{\"question\": \"Summarize the changes in https://github.com/modelcontextprotocol/python-sdk/releases\"}'\n```\n\nUse another MCP server: change `SERVER` (e.g. `StdioServerParameters(command=\"npx\",\nargs=[\"-y\", \"@modelcontextprotocol/server-github\"])`), add its credentials\nwith `codiedev agents secrets %NAME% set GITHUB_PERSONAL_ACCESS_TOKEN=…`, and\npass them through `env=` if the server needs them.\n",
134
+ "agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.py\",\n \"timeoutSec\": 600,\n \"retries\": 1\n}\n",
135
+ "agent.py": "\"\"\"%NAME% — a model with MCP tools. Built from the mcp-research template;\nsee README.md.\n\nStarts an MCP server over stdio (mcp-server-fetch: read any URL), hands its\ntools to the model, and runs the tool loop until the model answers. Swap\nSERVER for any MCP server (npx -y <server> works too).\n\"\"\"\n\nimport asyncio\nimport json\nimport os\nimport sys\n\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import stdio_client\n\nimport codiedev_agents as agents\n\nMODEL = os.environ.get(\"MODEL\", \"%MODEL%\")\nMAX_TURNS = 8\nSERVER = StdioServerParameters(command=sys.executable, args=[\"-m\", \"mcp_server_fetch\"])\n\n\nasync def main() -> None:\n task = agents.input() or {}\n question = task.get(\"question\") or \"What are the top 3 stories on https://news.ycombinator.com right now? One line each.\"\n async with stdio_client(SERVER) as (read, write):\n async with ClientSession(read, write) as session:\n await session.initialize()\n tools = (await session.list_tools()).tools\n print(\"tools:\", \", \".join(t.name for t in tools))\n # (inputSchema in older MCP SDKs, input_schema in newer ones)\n spec = [\n {\"type\": \"function\", \"function\": {\"name\": t.name, \"description\": t.description or \"\", \"parameters\": getattr(t, \"inputSchema\", None) or getattr(t, \"input_schema\", None) or {\"type\": \"object\"}}}\n for t in tools\n ]\n\n llm = agents.openai()\n messages: list = [\n {\"role\": \"system\", \"content\": \"Use the tools to find what you need, then answer concisely. Cite URLs.\"},\n {\"role\": \"user\", \"content\": question},\n ]\n answer = \"\"\n for _ in range(MAX_TURNS):\n res = llm.chat.completions.create(model=MODEL, messages=messages, tools=spec)\n msg = res.choices[0].message\n messages.append(msg.model_dump(exclude_none=True))\n if not msg.tool_calls:\n answer = msg.content or \"\"\n break\n for call in msg.tool_calls:\n args = json.loads(call.function.arguments or \"{}\")\n print(f\"→ {call.function.name} {json.dumps(args)[:200]}\")\n result = await session.call_tool(call.function.name, args)\n text = \"\\n\".join(getattr(c, \"text\", \"\") for c in result.content)[:20_000]\n messages.append({\"role\": \"tool\", \"tool_call_id\": call.id, \"content\": text})\n print(answer)\n agents.output({\"question\": question, \"answer\": answer})\n\n\nasyncio.run(main())\n",
136
+ "requirements.txt": "# The MCP server this agent starts (preinstalled: openai, httpx, pydantic, mcp).\nmcp-server-fetch\n"
137
+ }
138
+ }
139
+ ];
@@ -0,0 +1,7 @@
1
+ import type { CodiedevConfig } from "../utils";
2
+ export declare const AGENTS_HELP: string;
3
+ export declare const AGENT_GUIDE = "# Building a Codiedev agent\n\nThis folder is one agent. Each time it's started, Codiedev runs it once in a\nfresh sandbox, then destroys the sandbox. Model access, tracing and secrets\nare already wired in. You write the entrypoint (`agent.py`, `agent.ts`,\n`agent.mjs` or a `.sh` script, named in agent.json).\n\n## The primitive\n\nPython:\n\n```python\nimport codiedev_agents as agents\n\ntask = agents.input() # this run's input (any JSON), or None\nllm = agents.openai() # OpenAI SDK client on the Codiedev gateway, traced\nreply = llm.chat.completions.create(model=MODEL, messages=[...])\nagents.output({\"answer\": \"...\"}) # this run's result (any JSON)\n```\n\nTypeScript / JavaScript (`codiedev-agents` and `openai` are preinstalled):\n\n```ts\nimport * as agents from \"codiedev-agents\";\n\nconst task = agents.input();\nconst llm = await agents.openai();\nconst reply = await llm.chat.completions.create({ model: MODEL, messages: [...] });\nagents.output({ answer: reply.choices[0].message.content });\n```\n\nFrom a shell script: the input is `/agent/.codiedev/input.json`; write the\nresult to `/agent/.codiedev/output.json`.\n\n- **Chain agents:** `agents.run(\"other-agent\", {...})` starts another deployed\n agent as a child run, waits, and returns it; its result is `[\"output\"]`\n (`.output` in JS). Use `agents.start(...)` + `agents.get(id)` to fan out in\n parallel. Children show under their parent in Codiedev.\n- **Tools / MCP:** call APIs directly (`httpx`, `fetch`), or start any MCP\n server over stdio: Python `mcp` and Node `@modelcontextprotocol/sdk` are\n installed, and `npx -y <server>` works. For a remote MCP server, put its\n URL and token in secrets.\n- **Personal secrets:** `\"personalSecrets\": [\"SALESFORCE_TOKEN\"]` in agent.json\n means each person sets their own (`secrets <name> set KEY=\u2026 --mine`), and a\n run someone starts uses theirs, never yours. Schedules and webhooks use the\n shared value. Teammates \"Use\" your agent instead of copying it, so they get\n your updates.\n- **Secrets:** `codiedev agents secrets <name> set ZENDESK_TOKEN=\u2026`; read them\n from the environment (`os.environ[\"ZENDESK_TOKEN\"]`, `process.env.ZENDESK_TOKEN`).\n Never put tokens in code.\n- **Dependencies:** `requirements.txt` and/or `package.json` (+ lock file). They\n install once into a cached image when they change, not on every run.\n Preinstalled: Python 3.12 with openai, mcp, httpx, pydantic; Node 22 with\n openai, @modelcontextprotocol/sdk, tsx; git, curl.\n- **Exit code:** 0 = succeeded. An exception or non-zero exit = failed, with\n the exception (or the last log line) as the error. Everything printed goes to the run's log.\n\n## The sandbox\n\nLinux x64 (Debian, glibc) with internet access. Your code is in `/agent` (the\nworking directory); `/tmp` is writable. Every run gets these environment\nvariables:\n\n| Variable | What it is |\n|---|---|\n| `OPENAI_BASE_URL`, `OPENAI_API_KEY` | The Codiedev model gateway and this agent's key. Any OpenAI-compatible tool picks them up as is. |\n| `CODIEDEV_GATEWAY_URL`, `CODIEDEV_API_KEY` | The same two, under Codiedev names |\n| `CODIEDEV_AGENT`, `CODIEDEV_RUN_ID` | This agent's name and this run's id |\n| `CODIEDEV_ATTEMPT`, `CODIEDEV_MAX_ATTEMPTS` | Which try this is, out of how many |\n| `CODIEDEV_MODEL` | The agent's default model: the first one its key allows (unset when it allows every model) |\n| your secrets | Each by its own name, e.g. `GITHUB_TOKEN` |\n\n**Models:** use a model id from your company's **Models** page, like\n`openai/gpt-5.1-codex-mini`. A model your company hasn't approved fails with\n`model_not_allowed`. Calls are traced under the agent's name and count\ntoward its `budgetUsd` (a monthly budget for the agent, not per run).\nA call counts toward a run's cost when it sends the header\n`X-Codiedev-Run-Id: $CODIEDEV_RUN_ID`. `agents.openai()` does; for any other\nclient or tool, add the header yourself (see opencode below).\n\n**Dependencies** from `package.json` install into `/agent/node_modules`, so\ntheir binaries are in `/agent/node_modules/.bin`. Python packages are importable\nas usual. A Python agent can have a `package.json` too (both install).\n\n## Outside tools (opencode, aider, Codex CLI\u2026)\n\nAny tool that speaks the OpenAI API works: point it at `OPENAI_BASE_URL` with\n`OPENAI_API_KEY`. For example, opencode as a coding agent inside a run\n(`\"opencode-ai\"` in package.json, `\"timeoutSec\": 3600` and `\"cpu\": 2,\n\"memoryMb\": 4096` in agent.json):\n\n```python\nimport json, os, subprocess\nconfig = {\n \"$schema\": \"https://opencode.ai/config.json\",\n \"autoupdate\": False, \"share\": \"disabled\", \"permission\": \"allow\",\n \"provider\": {\"codiedev\": {\n \"npm\": \"@ai-sdk/openai-compatible\",\n \"options\": {\"baseURL\": os.environ[\"OPENAI_BASE_URL\"], \"apiKey\": \"{env:OPENAI_API_KEY}\",\n # Counts its model calls toward this run's cost.\n \"headers\": {\"X-Codiedev-Run-Id\": os.environ[\"CODIEDEV_RUN_ID\"]}},\n \"models\": {MODEL: {}},\n }},\n}\njson.dump(config, open(f\"{repo_dir}/opencode.json\", \"w\"))\nsubprocess.run([\"/agent/node_modules/.bin/opencode\", \"run\", \"--model\", f\"codiedev/{MODEL}\", prompt],\n cwd=repo_dir, check=True, timeout=45 * 60)\n```\n\nKeep the tool's own time limit under the run's, so the agent can still write\nits output. Git is installed; clone with a token from secrets\n(`https://x-access-token:$GITHUB_TOKEN@github.com/owner/repo.git`).\n\n## Building blocks\n\nAll in the SDK (Python names shown; JS uses camelCase and returns promises).\n\n- **State that outlives runs:** `agents.state.get(key, default)`,\n `.set(key, value, ttl=None)`, `.delete(key)`, `.list(prefix)`. Values are any\n JSON up to 64 KB, 1,000 keys per agent, shared by all its runs. For a\n counter or a cursor that parallel runs touch, use\n `agents.state.update(key, lambda old: ...)`, or `get_versioned` +\n `set(..., if_version=n)`, which raises `StateConflict` instead of overwriting.\n- **Retries:** `\"retries\": 2` in agent.json (or `--retries` / `retries=` per run)\n tries a failed run again on the same run id after 15s, 30s, 60s\u2026 Callers\n waiting on it keep waiting. `agents.attempt()` says which try this is.\n- **Steps (durable across retries):** wrap side effects in\n `agents.step(\"post-reply\", lambda: post_reply())`. A finished step returns\n its saved result on a retry instead of running again, so a retry never\n posts twice. A multi-step job is a list of steps.\n- **Answering a webhook:** a caller that POSTs to `<webhook url>?wait=30`\n gets the run's result as the HTTP response (Slack slash commands, form\n handlers). `agents.respond(body, status=200, headers=None)` sets exactly\n what it gets; otherwise it's the output. Runs take seconds to start, so\n for senders with a 3s limit, acknowledge and post back instead.\n- **Later:** `agents.start(\"name\", input, delay=3600)` starts a run in an hour\n (up to 7 days). An agent can schedule its own next check this way.\n- **Human approval:** `agents.request_approval(\"Refund $120 to Acme?\",\n details=..., context={...})` puts a decision in Codiedev (Agents \u2192\n Approvals, or `codiedev agents decide`) and returns a `url` to post anywhere.\n End the run there; when someone decides, a new run of this agent starts\n with input `{\"approval\": {\"decision\", \"comment\", \"decidedBy\"}, \"context\": ...}`.\n Check `agents.input().get(\"approval\")` at the top to tell the two apart.\n Unanswered by `expires_in` (default 7 days): decision `\"expired\"`.\n `options=[...]` for other choices; `approvers=[emails]` to limit who decides.\n\n## Ship, watch, iterate\n\n```bash\ncodiedev agents run --local --input '{\"ticket\": 42}' # run it here first, no deploy\ncodiedev agents deploy # from this folder\ncodiedev agents run <name> --input '{\"ticket\": 42}' # streams the log, prints output\ncodiedev agents runs <name> --failed # what broke\ncodiedev agents show <run-id> --json # input, output, log, cost, children\ncodiedev agents logs <run-id> -f # follow a run that's still going\n```\n\nTo fix a failing agent: `codiedev agents runs <name> --failed`, read the run\nwith `show --json`, change the code, try it with `run --local` and the same\ninput, `deploy`, then rerun it with `codiedev agents run <name> --input-file <file>`.\nA deploy that made things worse: `codiedev agents rollback <name>` (the last\n10 versions are kept; `codiedev agents versions <name>` lists them).\n\n`run --local` runs the entrypoint on your machine with the same SDK: secrets\nfrom `.env` (never bundled), state in `.codiedev/local/state.json`\n(`--remote-state` uses the deployed agent's), model calls on your own CLI\ntoken. Python agents need `openai` installed locally; TS runs through `npx tsx`. `show` also prints the run's\nTraces id, so `codiedev traces` (test cases, evals) works on it.\n\n## Starting runs\n\n- **Schedule:** set `\"every\": \"15m\"` in agent.json and redeploy. A scheduled run\n is skipped while the previous one is still going.\n- **Webhook:** `codiedev agents webhook <name>` returns a URL. A POST to it\n starts a run with the body as input (JSON parsed, anything else as a\n string). `agents.request()` gives the exact request (method, query,\n headers, raw body) so the agent can verify the sender's signature (Slack\n `X-Slack-Signature`, GitHub `X-Hub-Signature-256`); keep the signing\n secret in secrets. Slack's URL check is answered for you.\n- **From other code:** POST `/api/agents/v1/run` with `{\"agent\", \"input\"}` and\n your CLI token, or `codiedev_agents.run()` with `CODIEDEV_TOKEN` and\n `CODIEDEV_AGENTS_URL` set.\n- **No duplicates:** send `Idempotency-Key` (header, or `idempotencyKey` in the\n body, or `--idempotency-key`). A repeat within a day returns the first run.\n\n## Sharing it\n\n`codiedev agents publish <name> --summary \"\u2026\"` shares it as a template with\nyour team (`--company` for everyone), like a skill. Teammates find it under\nAgents \u2192 Marketplace or `codiedev agents templates`; they can run yours as\nis, or install their own copy (`codiedev agents install <template> <name>`)\nwith their own secrets. Publish again to update it. Write a README.md in the\nfolder: it's what people read before installing.\n\n## Who can do what\n\nAnyone in the company can run, pull and watch any agent. Redeploying,\ndeleting, rolling back, secrets, webhooks and reading its state from outside\na run are for the agent's owner (who first deployed it) and company admins,\nsince whoever can redeploy an agent can read its secrets. Anyone can decide\nan approval unless it names approvers.\n\n## Cost and limits\n\nEach run shows its sandbox time and cost (passed through at the provider's\nrate, about $0.19 an hour at 1 core / 2 GB) and its model spend. The agent's\ngateway key has a monthly budget (`budgetUsd` in agent.json, $25 by default).\n\nInput 64 KB, output 256 KB, bundle 10 MB, runs up to 1 hour, chains up to 5\ndeep, 25 runs at once per company, retries up to 5, delays up to 7 days,\nstate 1,000 keys \u00D7 64 KB per agent.\n";
4
+ export declare function parseEvery(value: string | null | undefined): number | null | undefined;
5
+ /** "90", "90s", "30m", "2h", "1d" → seconds. */
6
+ export declare function parseDuration(value: string): number;
7
+ export declare function runAgents(args: string[], configOverride?: CodiedevConfig): Promise<void>;