codiedev 0.9.80 → 0.10.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +12 -5
  2. package/dist/backup/cache.js +1 -5
  3. package/dist/backup/match.js +0 -6
  4. package/dist/backup/run.js +3 -35
  5. package/dist/captureExclusions.js +0 -12
  6. package/dist/captureLog.js +0 -28
  7. package/dist/captureReadiness.js +0 -58
  8. package/dist/captureSignals.js +0 -11
  9. package/dist/cli.js +11 -33
  10. package/dist/cliSession.js +3 -60
  11. package/dist/codexHookTrust.js +0 -28
  12. package/dist/codexLimitScan.js +0 -107
  13. package/dist/codie.js +64 -57
  14. package/dist/codieBrandInstall.js +0 -18
  15. package/dist/codieLauncher.js +1 -72
  16. package/dist/codieRuntime.js +27 -32
  17. package/dist/codieRuntimeInstall.js +11 -43
  18. package/dist/codieUsage.js +0 -49
  19. package/dist/commands/agentTemplates.generated.js +136 -0
  20. package/dist/commands/agents.js +1088 -0
  21. package/dist/commands/agentsSdk.generated.js +7 -0
  22. package/dist/commands/ask.js +0 -3
  23. package/dist/commands/capture.js +0 -2
  24. package/dist/commands/createTicket.js +0 -10
  25. package/dist/commands/doctor.js +2 -72
  26. package/dist/commands/evals.js +3 -9
  27. package/dist/commands/inbox.js +0 -2
  28. package/dist/commands/library.js +0 -3
  29. package/dist/commands/pack.js +0 -29
  30. package/dist/commands/ping.js +0 -2
  31. package/dist/commands/pull.js +0 -9
  32. package/dist/commands/push.js +0 -12
  33. package/dist/commands/search.js +0 -4
  34. package/dist/commands/send.js +0 -5
  35. package/dist/commands/share.js +0 -4
  36. package/dist/commands/shared.js +0 -35
  37. package/dist/commands/traces.js +0 -13
  38. package/dist/commands/tracesLabel.js +0 -8
  39. package/dist/configDir.js +1 -18
  40. package/dist/connect.js +3 -75
  41. package/dist/connectFlow.js +0 -15
  42. package/dist/copilotHooks.js +0 -67
  43. package/dist/copilotInstallation.js +0 -1
  44. package/dist/cursorHooks.js +1 -11
  45. package/dist/cursorLifecycle.js +1 -7
  46. package/dist/detection.js +0 -30
  47. package/dist/harnessDirs.js +0 -19
  48. package/dist/hook.js +43 -293
  49. package/dist/inboxFormat.js +0 -24
  50. package/dist/launchContext.js +0 -50
  51. package/dist/launcher.js +0 -33
  52. package/dist/limitEvent.js +2 -34
  53. package/dist/managedPi.js +2 -50
  54. package/dist/mcp.js +0 -48
  55. package/dist/nativeSteering.js +1 -4
  56. package/dist/packs/codie.js +0 -15
  57. package/dist/packs/harnesses.js +1 -38
  58. package/dist/packs/hookGate.js +0 -33
  59. package/dist/packs/install.js +0 -20
  60. package/dist/packs/manifest.js +1 -15
  61. package/dist/packs/paths.js +0 -8
  62. package/dist/packs/shim.js +0 -8
  63. package/dist/packs/store.js +0 -12
  64. package/dist/packs/sync.js +0 -20
  65. package/dist/piHooks.js +4 -45
  66. package/dist/piRuntime.js +0 -18
  67. package/dist/repoResolver.js +0 -19
  68. package/dist/secret.js +0 -3
  69. package/dist/selfUpdate.js +2 -77
  70. package/dist/skillMarker.js +0 -14
  71. package/dist/subagents.js +0 -18
  72. package/dist/teamSkillContext.js +0 -1
  73. package/dist/transcriptModels.js +0 -8
  74. package/dist/upgradeNudge.js +1 -30
  75. package/dist/utils.js +165 -768
  76. package/dist/version.js +0 -10
  77. package/dist/versionCheck.js +0 -33
  78. package/package.json +13 -8
  79. package/dist/backup/cache.d.ts +0 -14
  80. package/dist/backup/match.d.ts +0 -2
  81. package/dist/backup/run.d.ts +0 -25
  82. package/dist/captureExclusions.d.ts +0 -30
  83. package/dist/captureLog.d.ts +0 -20
  84. package/dist/captureReadiness.d.ts +0 -65
  85. package/dist/captureSignals.d.ts +0 -47
  86. package/dist/cli.d.ts +0 -15
  87. package/dist/cliSession.d.ts +0 -108
  88. package/dist/codexHookTrust.d.ts +0 -49
  89. package/dist/codexLimitScan.d.ts +0 -64
  90. package/dist/codie.d.ts +0 -2
  91. package/dist/codieBrandInstall.d.ts +0 -11
  92. package/dist/codieLauncher.d.ts +0 -87
  93. package/dist/codieRuntime.d.ts +0 -35
  94. package/dist/codieRuntimeInstall.d.ts +0 -56
  95. package/dist/codieUsage.d.ts +0 -64
  96. package/dist/commands/ask.d.ts +0 -1
  97. package/dist/commands/capture.d.ts +0 -14
  98. package/dist/commands/createTicket.d.ts +0 -27
  99. package/dist/commands/delete.d.ts +0 -1
  100. package/dist/commands/doctor.d.ts +0 -9
  101. package/dist/commands/evals.d.ts +0 -3
  102. package/dist/commands/inbox.d.ts +0 -3
  103. package/dist/commands/library.d.ts +0 -2
  104. package/dist/commands/note.d.ts +0 -1
  105. package/dist/commands/pack.d.ts +0 -54
  106. package/dist/commands/ping.d.ts +0 -1
  107. package/dist/commands/pull.d.ts +0 -2
  108. package/dist/commands/push.d.ts +0 -33
  109. package/dist/commands/search.d.ts +0 -2
  110. package/dist/commands/send.d.ts +0 -2
  111. package/dist/commands/share.d.ts +0 -2
  112. package/dist/commands/shared.d.ts +0 -64
  113. package/dist/commands/traces.d.ts +0 -72
  114. package/dist/commands/tracesLabel.d.ts +0 -24
  115. package/dist/configDir.d.ts +0 -10
  116. package/dist/connect.d.ts +0 -1
  117. package/dist/connectFlow.d.ts +0 -34
  118. package/dist/copilotHooks.d.ts +0 -85
  119. package/dist/copilotInstallation.d.ts +0 -10
  120. package/dist/cursorHooks.d.ts +0 -13
  121. package/dist/cursorLifecycle.d.ts +0 -2
  122. package/dist/detection.d.ts +0 -30
  123. package/dist/harnessDirs.d.ts +0 -15
  124. package/dist/hook.d.ts +0 -6
  125. package/dist/inboxFormat.d.ts +0 -38
  126. package/dist/launchContext.d.ts +0 -46
  127. package/dist/launcher.d.ts +0 -25
  128. package/dist/limitEvent.d.ts +0 -15
  129. package/dist/limitEventReporter.d.ts +0 -21
  130. package/dist/managedPi.d.ts +0 -48
  131. package/dist/mcp.d.ts +0 -12
  132. package/dist/nativeSteering.d.ts +0 -5
  133. package/dist/packs/codie.d.ts +0 -45
  134. package/dist/packs/harnesses.d.ts +0 -74
  135. package/dist/packs/hookGate.d.ts +0 -53
  136. package/dist/packs/install.d.ts +0 -46
  137. package/dist/packs/manifest.d.ts +0 -52
  138. package/dist/packs/paths.d.ts +0 -17
  139. package/dist/packs/shim.d.ts +0 -26
  140. package/dist/packs/store.d.ts +0 -42
  141. package/dist/packs/sync.d.ts +0 -40
  142. package/dist/piHooks.d.ts +0 -22
  143. package/dist/piRuntime.d.ts +0 -26
  144. package/dist/repoResolver.d.ts +0 -17
  145. package/dist/secret.d.ts +0 -1
  146. package/dist/selfUpdate.d.ts +0 -64
  147. package/dist/skillMarker.d.ts +0 -6
  148. package/dist/subagents.d.ts +0 -36
  149. package/dist/teamSkillContext.d.ts +0 -9
  150. package/dist/transcriptModels.d.ts +0 -20
  151. package/dist/upgradeNudge.d.ts +0 -24
  152. package/dist/utils.d.ts +0 -396
  153. package/dist/version.d.ts +0 -1
  154. package/dist/versionCheck.d.ts +0 -68
@@ -1,10 +1,4 @@
1
1
  "use strict";
2
- // First-run install of the OpenCode engine for the Codie harness.
3
- //
4
- // Staged then renamed: npm writes into a sibling staging directory and only an
5
- // install that actually produced the engine binary gets promoted to the real
6
- // runtime path. An interrupted or half-finished download therefore never
7
- // leaves a directory that resolveCodieRuntime() would report as "ready".
8
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
3
  if (k2 === undefined) k2 = k;
10
4
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -55,11 +49,7 @@ const os = __importStar(require("os"));
55
49
  const path = __importStar(require("path"));
56
50
  const child_process_1 = require("child_process");
57
51
  const codieRuntime_1 = require("./codieRuntime");
58
- /** linkEngine's detail when npm is explicitly set not to run install scripts. */
59
- exports.IGNORE_SCRIPTS = "ignore-scripts";
60
52
  function buildRuntimeInstallArgs(stagingDir, spec) {
61
- // No --registry / --proxy: inherit whatever npm config the developer's
62
- // machine already has so an internal mirror keeps working.
63
53
  return [
64
54
  "install",
65
55
  "--prefix",
@@ -87,17 +77,12 @@ function installCodieRuntime(deps) {
87
77
  remove(staging);
88
78
  throw installFailure(result.stderr?.trim() || `npm exited with ${result.status}`);
89
79
  }
90
- // Trust the artifact, not the exit code — a misconfigured mirror can succeed
91
- // with nothing useful in the tarball.
92
80
  const stagedBinary = path.join(staging, "node_modules", "opencode-ai", "bin", "opencode.exe");
93
81
  if (!exists(stagedBinary)) {
94
82
  remove(staging);
95
83
  throw new Error(`The Codie engine install did not produce a runtime binary. ` +
96
84
  `Check that ${runtime.spec} is available from your npm registry.`);
97
85
  }
98
- // The placeholder exists even when postinstall never ran (npm 12 skips
99
- // dependency install scripts); link it here — what opencode-ai's own error
100
- // tells you to do — before promoting.
101
86
  if (!isEngine(stagedBinary)) {
102
87
  let linked = { ok: false };
103
88
  try {
@@ -108,9 +93,6 @@ function installCodieRuntime(deps) {
108
93
  }
109
94
  if (!linked.ok || !isEngine(stagedBinary)) {
110
95
  if (linked.detail === exports.IGNORE_SCRIPTS) {
111
- // npm is set to ignore scripts: keep the install (promote it) so the
112
- // printed postinstall path exists and the next run is a no-download
113
- // "unlinked" that stops with the same step, not another download.
114
96
  try {
115
97
  rename(staging, runtime.installDir);
116
98
  }
@@ -128,20 +110,22 @@ function installCodieRuntime(deps) {
128
110
  rename(staging, runtime.installDir);
129
111
  }
130
112
  finally {
131
- // No-op after a successful promote (rmSync is force: true); after a failed
132
- // one it keeps a ~132MB staging dir from being orphaned — mkdtemp mints a
133
- // fresh sibling every attempt, so leaks would accumulate.
134
113
  remove(staging);
135
114
  }
136
115
  }
137
- // Resolve the engine, downloading it once if this is the first `codie` run.
138
- function ensureCodieRuntimeBinary({ resolve, install, repair, }) {
116
+ async function ensureCodieRuntimeBinary({ resolve, install, confirmUpdate = async () => false, repair, }) {
139
117
  const runtime = resolve();
140
118
  if (runtime.kind === "ready")
141
119
  return runtime.binary;
142
- // An install whose postinstall never ran: link it in place (no download).
143
- // Re-resolve after, so a concurrent repair or a partial link falls through
144
- // to a reinstall rather than launching a missing file.
120
+ if (runtime.kind === "outdated" && !(await confirmUpdate(runtime))) {
121
+ if (runtime.linked !== false || !repair || !runtime.packageDir)
122
+ return runtime.binary;
123
+ if (repair({ kind: "unlinked", installDir: runtime.installDir, binary: runtime.binary, spec: runtime.spec, packageDir: runtime.packageDir })) {
124
+ return runtime.binary;
125
+ }
126
+ install(runtime);
127
+ return runtime.binary;
128
+ }
145
129
  if (runtime.kind === "unlinked" && repair?.(runtime)) {
146
130
  const after = resolve();
147
131
  if (after.kind === "ready")
@@ -150,7 +134,6 @@ function ensureCodieRuntimeBinary({ resolve, install, repair, }) {
150
134
  install(runtime);
151
135
  return runtime.binary;
152
136
  }
153
- /** Why an installed engine still isn't runnable, with opencode-ai's own error. Pure. */
154
137
  function linkFailure(linked, packageDir) {
155
138
  if (linked.detail === exports.IGNORE_SCRIPTS) {
156
139
  const script = packageDir ? `"${path.join(packageDir, "postinstall.mjs")}"` : "postinstall.mjs in the opencode-ai package";
@@ -162,20 +145,16 @@ function linkFailure(linked, packageDir) {
162
145
  (linked.detail ? `: ${linked.detail}` : ".") +
163
146
  ` Set CODIEDEV_OPENCODE_BINARY to an opencode binary to use a pre-provisioned engine.`);
164
147
  }
148
+ exports.IGNORE_SCRIPTS = "ignore-scripts";
165
149
  function installFailure(detail) {
166
150
  return new Error(`Could not download the Codie engine: ${detail}\n` +
167
151
  `If you are behind a proxy or an internal registry, make sure npm can reach it. ` +
168
152
  `To use a pre-provisioned engine instead, set CODIEDEV_OPENCODE_BINARY to its path.`);
169
153
  }
170
- // ── Real-world wiring ───────────────────────────────────────────────────
171
- // Kept separate from the pure logic above so tests never shell out to npm.
172
- /** A cmd.exe command line: each argument double-quoted when it has spaces or
173
- * shell metacharacters (paths like C:\Users\First Last\…). Pure. */
174
154
  function windowsCommand(command, args) {
175
155
  const quote = (a) => (/[\s"&|<>^()%!]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a);
176
156
  return [command, ...args.map(quote)].join(" ");
177
157
  }
178
- /** A real engine on disk (not opencode-ai's placeholder script). */
179
158
  function isEngineOnDisk(candidate) {
180
159
  try {
181
160
  return (0, codieRuntime_1.isEngineSize)(fs.statSync(candidate).size);
@@ -184,25 +163,17 @@ function isEngineOnDisk(candidate) {
184
163
  return false;
185
164
  }
186
165
  }
187
- /** Whether npm is explicitly configured not to run install scripts (an org
188
- * policy codie shouldn't bypass). npm 12's default script approval isn't this. */
189
166
  function npmIgnoresScripts(env = process.env) {
190
- // npm reads either case and treats anything but a false-ish value as true.
191
167
  const fromEnv = env.npm_config_ignore_scripts ?? env.NPM_CONFIG_IGNORE_SCRIPTS;
192
168
  if (fromEnv !== undefined)
193
169
  return !["", "false", "0", "null", "no"].includes(fromEnv.trim().toLowerCase());
194
170
  const opts = { encoding: "utf8", timeout: 10_000, windowsHide: true };
195
- // npm.cmd needs a shell on Windows (Node 20+ refuses to spawn .cmd without
196
- // one). One constant command string — args plus shell:true is deprecated.
197
171
  const result = process.platform === "win32"
198
172
  ? (0, child_process_1.spawnSync)("npm.cmd config get ignore-scripts", { ...opts, shell: true })
199
173
  : (0, child_process_1.spawnSync)("npm", ["config", "get", "ignore-scripts"], opts);
200
174
  return result.status === 0 && (result.stdout || "").trim() === "true";
201
175
  }
202
176
  const LINK_TIMEOUT_MS = 120_000;
203
- /** Run opencode-ai's own postinstall, which links the platform binary into
204
- * bin/opencode.exe (and picks the right build for this CPU/libc). The same
205
- * step npm would have run; skipped when npm is set to ignore scripts. */
206
177
  function linkEngineOnDisk(packageDir, { ignoreScripts = npmIgnoresScripts } = {}) {
207
178
  const script = path.join(packageDir, "postinstall.mjs");
208
179
  if (!fs.existsSync(script))
@@ -229,7 +200,6 @@ function installCodieRuntimeOnDisk(runtime, stateDir) {
229
200
  installCodieRuntime({
230
201
  runtime,
231
202
  makeStagingDir: () => {
232
- // Sibling of the final dir so the promote is a same-filesystem rename.
233
203
  fs.mkdirSync(path.dirname((0, codieRuntime_1.codieRuntimeDir)(stateDir)), { recursive: true });
234
204
  return fs.mkdtempSync(path.join(path.dirname((0, codieRuntime_1.codieRuntimeDir)(stateDir)), ".runtime-staging-"));
235
205
  },
@@ -239,8 +209,6 @@ function installCodieRuntimeOnDisk(runtime, stateDir) {
239
209
  stdio: ["ignore", "inherit", "pipe"],
240
210
  env: { ...process.env, npm_config_yes: "true" },
241
211
  };
242
- // Windows: Node 20+ refuses to spawn npm.cmd without a shell (EINVAL),
243
- // which made the engine install fail outright there. One quoted string.
244
212
  const result = process.platform === "win32"
245
213
  ? (0, child_process_1.spawnSync)(windowsCommand("npm.cmd", args), { ...opts, shell: true, windowsHide: true })
246
214
  : (0, child_process_1.spawnSync)("npm", args, opts);
@@ -1,12 +1,4 @@
1
1
  "use strict";
2
- // The Codie harness status line.
3
- //
4
- // The gateway already knows what a developer has spent, what caps them, and
5
- // what they have in flight. Nothing ever asked it. This module asks, and turns
6
- // the answer into one line the harness can paint next to the prompt.
7
- //
8
- // Every failure here is silent by design: a status poll must never interrupt,
9
- // delay, or break a coding session. No snapshot means no line.
10
2
  Object.defineProperty(exports, "__esModule", { value: true });
11
3
  exports.formatUsageLine = formatUsageLine;
12
4
  exports.usageTone = usageTone;
@@ -19,8 +11,6 @@ function cleanBaseUrl(value) {
19
11
  function money(value) {
20
12
  return `$${value.toFixed(2)}`;
21
13
  }
22
- /** Budgets are set in whole dollars far more often than not, so $150 reads
23
- * better than $150.00 next to a spend figure that does need its cents. */
24
14
  function budgetMoney(value) {
25
15
  return Number.isInteger(value) ? `$${value}` : money(value);
26
16
  }
@@ -28,16 +18,10 @@ function percent(spent, budget) {
28
18
  if (!(budget > 0))
29
19
  return null;
30
20
  const share = (spent / budget) * 100;
31
- // A first request against a large budget rounds to 0%, which reads as
32
- // "nothing spent". Say "under one percent" instead.
33
21
  if (share > 0 && share < 1)
34
22
  return "<1%";
35
23
  return `${Math.round(share)}%`;
36
24
  }
37
- /**
38
- * One line: who is spending, how much, against what, and what is running.
39
- * Returns "" when there is nothing honest to say.
40
- */
41
25
  function formatUsageLine(usage) {
42
26
  if (!usage || !usage.enabled)
43
27
  return "";
@@ -45,16 +29,11 @@ function formatUsageLine(usage) {
45
29
  const segments = [];
46
30
  if (usage.user?.name)
47
31
  segments.push(usage.user.name);
48
- // The developer's own month-to-date spend is always the headline. Only the
49
- // budget that actually caps them earns a percentage next to it.
50
32
  if (typeof budget.userBudgetUsd === "number") {
51
33
  segments.push(`${money(budget.userSpentUsd)} / ${budgetMoney(budget.userBudgetUsd)}`);
52
34
  const share = percent(budget.userSpentUsd, budget.userBudgetUsd);
53
35
  if (share)
54
36
  segments.push(share);
55
- // A personal cap does not help once the shared pool is nearly gone: say
56
- // so when the pool is running low and will stop this developer before
57
- // their own cap does.
58
37
  if (poolBindsFirst(budget) && typeof budget.companyBudgetUsd === "number"
59
38
  && budget.companyBudgetUsd > 0
60
39
  && budget.companySpentUsd / budget.companyBudgetUsd >= 0.75) {
@@ -67,8 +46,6 @@ function formatUsageLine(usage) {
67
46
  segments.push(money(budget.userSpentUsd));
68
47
  if (typeof budget.companyBudgetUsd === "number") {
69
48
  const share = percent(budget.companySpentUsd, budget.companyBudgetUsd);
70
- // Labelled, because a team percentage beside a personal dollar figure
71
- // otherwise reads as the developer's own share.
72
49
  if (share)
73
50
  segments.push(`team ${share}`);
74
51
  }
@@ -79,8 +56,6 @@ function formatUsageLine(usage) {
79
56
  }
80
57
  if (inFlight.requests > 0)
81
58
  segments.push(`${inFlight.requests} in flight`);
82
- // Stale holds cap a budget while doing no work. Naming them is the
83
- // difference between "I am blocked" and "I am blocked by nothing".
84
59
  if (inFlight.staleHeldUsd > 0)
85
60
  segments.push(`${money(inFlight.staleHeldUsd)} held stale`);
86
61
  return segments.join(" · ");
@@ -92,11 +67,6 @@ function poolBindsFirst(budget) {
92
67
  const userLeft = Math.max(0, budget.userBudgetUsd - budget.userSpentUsd);
93
68
  return budget.userBudgetUsd > 0 && poolLeft < userLeft;
94
69
  }
95
- /**
96
- * How urgently the status line should read, from the limit that will stop
97
- * this developer first: their own cap, or the shared pool when they have no
98
- * cap or the pool runs out sooner. 75% warns, 90% is critical.
99
- */
100
70
  function usageTone(usage) {
101
71
  if (!usage || !usage.enabled)
102
72
  return "normal";
@@ -117,34 +87,20 @@ function usageTone(usage) {
117
87
  return "normal";
118
88
  }
119
89
  function tokens(total) {
120
- // Rounded by arithmetic, not toFixed: 2.55 is not exactly representable, so
121
- // toFixed(1) rounds it down and the same count can read two ways.
122
90
  if (total >= 1_000_000)
123
91
  return `${Math.round(total / 100_000) / 10}M`;
124
92
  if (total >= 1_000)
125
93
  return `${Math.round(total / 1_000)}k`;
126
94
  return String(total);
127
95
  }
128
- /**
129
- * What this session just cost. Returns "" when the session billed nothing,
130
- * because a developer who spent nothing does not need to be told a number.
131
- */
132
96
  function formatSessionCostLine(usage) {
133
97
  const since = readSince(usage?.since);
134
98
  if (!since || since.requests <= 0)
135
99
  return "";
136
- // A real charge that rounds to $0.00 reads as free. Say it is under a cent
137
- // instead — the month-to-date figure is where it really lands.
138
100
  const cost = since.costUsd > 0 && since.costUsd < 0.005 ? "<$0.01" : money(since.costUsd);
139
101
  const requests = `${since.requests} ${since.requests === 1 ? "request" : "requests"}`;
140
102
  return `This session: ${cost} · ${requests} · ${tokens(since.promptTokens + since.completionTokens)} tokens`;
141
103
  }
142
- /**
143
- * A usage window is reported only when every field of it is a real number.
144
- * A partial block is worse than none: it reads as a cheap session, and the
145
- * arithmetic on a missing field throws out of a call site whose whole
146
- * contract is that it cannot break the run that just finished.
147
- */
148
104
  function readSince(value) {
149
105
  if (!value || typeof value !== "object" || Array.isArray(value))
150
106
  return undefined;
@@ -173,11 +129,6 @@ function readUser(value) {
173
129
  role: typeof role === "string" ? role : "",
174
130
  };
175
131
  }
176
- /**
177
- * Read the developer's budget snapshot. Returns null on any refusal, network
178
- * failure, timeout, or malformed answer — a status poll is never worth an
179
- * error in front of a developer who asked for none.
180
- */
181
132
  async function fetchCodieUsage({ baseUrl, key, sinceMs, fetchImpl = fetch, }) {
182
133
  const controller = new AbortController();
183
134
  const timeout = setTimeout(() => controller.abort(), USAGE_TIMEOUT_MS);
@@ -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
+ ];