codiedev 0.10.0-beta.5 → 0.10.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/captureSignals.js +2 -0
- package/dist/cli.js +38 -50
- package/dist/codexHookTrust.js +67 -4
- package/dist/codie.js +23 -74
- package/dist/codieLauncher.js +18 -22
- package/dist/commands/agentTemplates.generated.js +136 -0
- package/dist/commands/agents.js +1066 -0
- package/dist/commands/agentsSdk.generated.js +7 -0
- package/dist/commands/doctor.js +12 -38
- package/dist/commands/evals.js +187 -0
- package/dist/commands/pack.js +1 -1
- package/dist/commands/traces.js +79 -15
- package/dist/commands/tracesLabel.js +348 -0
- package/dist/connect.js +11 -52
- package/dist/copilotHooks.js +0 -4
- package/dist/cursorHooks.js +0 -15
- package/dist/harnessDirs.js +4 -0
- package/dist/hook.js +33 -197
- package/dist/limitEventReporter.js +1 -1
- package/dist/managedPi.js +240 -0
- package/dist/mcp.js +0 -307
- package/dist/packs/harnesses.js +40 -3
- package/dist/packs/install.js +2 -1
- package/dist/piHooks.js +216 -0
- package/dist/piRuntime.js +120 -0
- package/dist/selfUpdate.js +47 -2
- package/dist/utils.js +232 -115
- package/package.json +5 -7
- package/dist/codieStartup.js +0 -27
- package/dist/commands/promote.js +0 -53
- package/dist/commands/react.js +0 -38
- package/dist/commands/reverseTicket.js +0 -444
- package/dist/commands/security.js +0 -156
- package/dist/prUrl.js +0 -28
- package/dist/security.js +0 -417
- package/dist/securityDeepScan.js +0 -235
- package/dist/securityDoctor.js +0 -61
- package/dist/securityFeature.js +0 -6
- package/dist/securityInstall.js +0 -232
- package/dist/securityInventory.js +0 -566
- package/dist/securityPilot.js +0 -59
- package/dist/securityPolicy.js +0 -181
- package/dist/securitySession.js +0 -72
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TEMPLATES = void 0;
|
|
4
|
+
exports.TEMPLATES = [
|
|
5
|
+
{
|
|
6
|
+
"id": "support-triage",
|
|
7
|
+
"title": "Support triage",
|
|
8
|
+
"summary": "Triages each new ticket from your helpdesk, drafts a reply, and asks a person before it promises a refund.",
|
|
9
|
+
"trigger": "Webhook from Zendesk, Salesforce, Intercom or a form",
|
|
10
|
+
"uses": [
|
|
11
|
+
"webhook",
|
|
12
|
+
"approvals",
|
|
13
|
+
"steps",
|
|
14
|
+
"retries"
|
|
15
|
+
],
|
|
16
|
+
"lang": "python",
|
|
17
|
+
"adapt": "point it at our helpdesk's webhook and post replies back through its API",
|
|
18
|
+
"files": {
|
|
19
|
+
"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",
|
|
20
|
+
"agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.py\",\n \"timeoutSec\": 300,\n \"retries\": 2\n}\n",
|
|
21
|
+
"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",
|
|
22
|
+
"requirements.txt": "# Preinstalled: openai, httpx, pydantic, mcp. Add more here.\n"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "slack-bot",
|
|
27
|
+
"title": "Slack bot",
|
|
28
|
+
"summary": "Answers when someone @-mentions it in Slack, in the thread, and remembers each thread's conversation.",
|
|
29
|
+
"trigger": "Slack Events API webhook",
|
|
30
|
+
"uses": [
|
|
31
|
+
"webhook",
|
|
32
|
+
"signatures",
|
|
33
|
+
"state"
|
|
34
|
+
],
|
|
35
|
+
"lang": "ts",
|
|
36
|
+
"adapt": "give it our team's context and the tools it should use",
|
|
37
|
+
"files": {
|
|
38
|
+
"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",
|
|
39
|
+
"agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.ts\",\n \"timeoutSec\": 120,\n \"retries\": 1\n}\n",
|
|
40
|
+
"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",
|
|
41
|
+
"package.json": "{\n \"name\": \"%NAME%\",\n \"private\": true,\n \"type\": \"module\",\n \"dependencies\": {}\n}\n"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "daily-digest",
|
|
46
|
+
"title": "Daily digest",
|
|
47
|
+
"summary": "Every morning, reads your feeds, keeps only what's new since yesterday, and posts a short digest to Slack.",
|
|
48
|
+
"trigger": "Schedule (every day)",
|
|
49
|
+
"uses": [
|
|
50
|
+
"schedule",
|
|
51
|
+
"state"
|
|
52
|
+
],
|
|
53
|
+
"lang": "python",
|
|
54
|
+
"adapt": "follow our sources (changelogs, status pages, blogs) and post to our team channel",
|
|
55
|
+
"files": {
|
|
56
|
+
"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",
|
|
57
|
+
"agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.py\",\n \"timeoutSec\": 300,\n \"every\": \"1d\",\n \"retries\": 2\n}\n",
|
|
58
|
+
"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",
|
|
59
|
+
"feeds.json": "[\n \"https://github.blog/changelog/feed/\",\n \"https://hnrss.org/best\",\n \"https://blog.python.org/feeds/posts/default\"\n]\n",
|
|
60
|
+
"requirements.txt": "# Preinstalled: openai, httpx, pydantic, mcp. Add more here.\n"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "doc-to-json",
|
|
65
|
+
"title": "Doc to JSON API",
|
|
66
|
+
"summary": "Turns any document (an invoice, a contract, an email) into validated JSON, answered synchronously over HTTP.",
|
|
67
|
+
"trigger": "Webhook with ?wait (a synchronous API)",
|
|
68
|
+
"uses": [
|
|
69
|
+
"webhook reply",
|
|
70
|
+
"retries"
|
|
71
|
+
],
|
|
72
|
+
"lang": "python",
|
|
73
|
+
"adapt": "extract the fields we need from our documents",
|
|
74
|
+
"files": {
|
|
75
|
+
"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",
|
|
76
|
+
"agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.py\",\n \"timeoutSec\": 120,\n \"retries\": 1\n}\n",
|
|
77
|
+
"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",
|
|
78
|
+
"requirements.txt": "# Preinstalled: openai, httpx, pydantic, mcp. Add more here.\n"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "research-fanout",
|
|
83
|
+
"title": "Research fan-out",
|
|
84
|
+
"summary": "Splits a question into sub-questions, researches them in parallel child runs, and writes one answer.",
|
|
85
|
+
"trigger": "On demand (CLI, API, another agent)",
|
|
86
|
+
"uses": [
|
|
87
|
+
"chaining",
|
|
88
|
+
"parallel runs"
|
|
89
|
+
],
|
|
90
|
+
"lang": "python",
|
|
91
|
+
"adapt": "research the questions our team asks most, using our docs or APIs in the worker step",
|
|
92
|
+
"files": {
|
|
93
|
+
"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",
|
|
94
|
+
"agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.py\",\n \"timeoutSec\": 900,\n \"retries\": 1\n}\n",
|
|
95
|
+
"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",
|
|
96
|
+
"requirements.txt": "# Preinstalled: openai, httpx, pydantic, mcp. Add more here.\n"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "uptime-watch",
|
|
101
|
+
"title": "Uptime watch",
|
|
102
|
+
"summary": "Checks your endpoints every 15 minutes and posts to Slack only when something goes down or comes back, with a plain-English note.",
|
|
103
|
+
"trigger": "Schedule (every 15 minutes)",
|
|
104
|
+
"uses": [
|
|
105
|
+
"schedule",
|
|
106
|
+
"state"
|
|
107
|
+
],
|
|
108
|
+
"lang": "python",
|
|
109
|
+
"adapt": "watch our services' health endpoints",
|
|
110
|
+
"files": {
|
|
111
|
+
"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",
|
|
112
|
+
"agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.py\",\n \"timeoutSec\": 120,\n \"every\": \"15m\"\n}\n",
|
|
113
|
+
"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",
|
|
114
|
+
"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",
|
|
115
|
+
"requirements.txt": "# Preinstalled: openai, httpx, pydantic, mcp. Add more here.\n"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"id": "mcp-research",
|
|
120
|
+
"title": "MCP tool agent",
|
|
121
|
+
"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.",
|
|
122
|
+
"trigger": "On demand (CLI, API, another agent)",
|
|
123
|
+
"uses": [
|
|
124
|
+
"MCP",
|
|
125
|
+
"tool calling"
|
|
126
|
+
],
|
|
127
|
+
"lang": "python",
|
|
128
|
+
"adapt": "use the MCP server for our system (Jira, Postgres, GitHub, our own) instead of fetch",
|
|
129
|
+
"files": {
|
|
130
|
+
"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",
|
|
131
|
+
"agent.json": "{\n \"name\": \"%NAME%\",\n \"entrypoint\": \"agent.py\",\n \"timeoutSec\": 600,\n \"retries\": 1\n}\n",
|
|
132
|
+
"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",
|
|
133
|
+
"requirements.txt": "# The MCP server this agent starts (preinstalled: openai, httpx, pydantic, mcp).\nmcp-server-fetch\n"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
];
|