@kudo-dev/sdk 0.0.0-stage → 0.2.0

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.
@@ -0,0 +1,867 @@
1
+ // src/agent/start.ts
2
+ import { existsSync as existsSync4 } from "fs";
3
+ import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
4
+ import { fileURLToPath as fileURLToPath4 } from "url";
5
+
6
+ // src/agent/agent.ts
7
+ import {
8
+ query
9
+ } from "@anthropic-ai/claude-agent-sdk";
10
+
11
+ // src/agent/log.ts
12
+ import { randomBytes } from "crypto";
13
+ var SECRETS = [
14
+ "PRIVATE_KEY",
15
+ "ANTHROPIC_API_KEY",
16
+ "TWITTER_API_KEY",
17
+ "TWITTER_API_SECRET_KEY",
18
+ "TWITTER_ACCESS_TOKEN",
19
+ "TWITTER_ACCESS_TOKEN_SECRET"
20
+ ];
21
+ var MAX_OUTPUT = 1500;
22
+ var MAX_COMMAND = 400;
23
+ var debugEnabled = (env = process.env) => env.LOG_LEVEL?.trim().toLowerCase() === "debug";
24
+ function redact(text, env = process.env) {
25
+ let out = text;
26
+ for (const name of SECRETS) {
27
+ const value = env[name]?.trim();
28
+ if (!value || value.length < 8) continue;
29
+ const forms = /* @__PURE__ */ new Set([value, value.replace(/^0x/i, "")]);
30
+ for (const form of forms) if (form.length >= 8) out = out.split(form).join(`[${name}]`);
31
+ }
32
+ return out;
33
+ }
34
+ function clip(text, max) {
35
+ const flat = text.trim();
36
+ return flat.length <= max ? flat : `${flat.slice(0, max)}\u2026 (${flat.length - max} more chars)`;
37
+ }
38
+ var time = () => (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
39
+ function runLog(label) {
40
+ const tag = `[${label} ${randomBytes(3).toString("hex")}]`;
41
+ const line = (message2) => `${time()} ${tag} ${redact(message2)}`;
42
+ return {
43
+ info: (message2) => console.log(line(message2)),
44
+ error: (message2) => console.error(line(message2)),
45
+ debug: (message2) => {
46
+ if (debugEnabled()) console.log(line(message2));
47
+ }
48
+ };
49
+ }
50
+ var limits = { MAX_OUTPUT, MAX_COMMAND };
51
+
52
+ // src/agent/agent.ts
53
+ var RESILIENCE_ENV = {
54
+ API_TIMEOUT_MS: "120000",
55
+ CLAUDE_CODE_MAX_RETRIES: "2",
56
+ CLAUDE_ENABLE_STREAM_WATCHDOG: "1",
57
+ CLAUDE_STREAM_IDLE_TIMEOUT_MS: "300000"
58
+ };
59
+ function agentOptions(session, log2) {
60
+ return {
61
+ model: session.model,
62
+ systemPrompt: session.systemPrompt,
63
+ cwd: session.cwd,
64
+ // Skills come only from the plugin assembled for this agent (skills.ts),
65
+ // and no project or user settings are read, so nothing else on the machine
66
+ // changes what the agent knows.
67
+ settingSources: [],
68
+ plugins: [{ type: "local", path: session.skills.path }],
69
+ skills: session.skills.names,
70
+ // The shell is the whole tool surface.
71
+ //
72
+ // `allowedTools` is an auto-approve list, not a filter: on its own it still
73
+ // leaves every other built-in tool, and any connector on the API key's
74
+ // account, advertised to the model. Denying them explicitly takes the tool
75
+ // list from 23 entries to Bash and Skill, so what the agent can do matches
76
+ // what this file says it can do — which is the claim the TEE attestation
77
+ // is supposed to stand behind.
78
+ allowedTools: ["Bash"],
79
+ disallowedTools: [
80
+ "Read",
81
+ "Write",
82
+ "Edit",
83
+ "WebFetch",
84
+ "WebSearch",
85
+ "mcp__*",
86
+ // connectors inherited from the account behind ANTHROPIC_API_KEY
87
+ "Task",
88
+ "Artifact",
89
+ "ArtifactComments",
90
+ "ArtifactData",
91
+ "CronCreate",
92
+ "CronDelete",
93
+ "CronList",
94
+ "DesignSync",
95
+ "EnterWorktree",
96
+ "ExitWorktree",
97
+ "ListAgents",
98
+ "Monitor",
99
+ "NotebookEdit",
100
+ "PushNotification",
101
+ "RemoteTrigger",
102
+ "ReportFindings",
103
+ "ScheduleWakeup",
104
+ "SendMessage",
105
+ "TaskStop",
106
+ "ToolSearch",
107
+ "Workflow"
108
+ ],
109
+ // Nobody is at the keyboard, so deny anything not pre-approved.
110
+ permissionMode: "dontAsk",
111
+ maxTurns: 40,
112
+ env: { ...process.env, ...RESILIENCE_ENV },
113
+ // Claude Code's own diagnostics: API errors, retries, a failed plugin load.
114
+ stderr: (data) => {
115
+ for (const line of data.split("\n")) if (line.trim()) log2.error(`stderr: ${clip(line, limits.MAX_OUTPUT)}`);
116
+ }
117
+ };
118
+ }
119
+ function proseOf(message2) {
120
+ return message2.message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
121
+ }
122
+ var AgentRunError = class extends Error {
123
+ };
124
+ function resultText(content) {
125
+ if (typeof content === "string") return content;
126
+ if (!Array.isArray(content)) return "";
127
+ return content.map((block) => block && typeof block === "object" && "text" in block ? String(block.text) : "").join("");
128
+ }
129
+ function logMessage(message2, log2, commands) {
130
+ if (message2.type === "system" && message2.subtype === "init") {
131
+ log2.debug(`session ${message2.session_id}, model ${message2.model}, tools ${message2.tools.join(", ")}`);
132
+ return;
133
+ }
134
+ if (message2.type === "assistant") {
135
+ for (const block of message2.message.content) {
136
+ if (block.type === "text" && block.text.trim()) log2.debug(`says: ${clip(block.text, limits.MAX_OUTPUT)}`);
137
+ if (block.type !== "tool_use") continue;
138
+ const input = block.input ?? {};
139
+ const what = block.name === "Bash" ? `$ ${clip(String(input.command ?? ""), limits.MAX_COMMAND)}` : block.name === "Skill" ? `skill ${String(input.skill ?? input.command ?? "")}` : `${block.name} ${clip(JSON.stringify(input), limits.MAX_COMMAND)}`;
140
+ commands.set(block.id, what);
141
+ log2.info(what);
142
+ }
143
+ return;
144
+ }
145
+ if (message2.type === "user" && Array.isArray(message2.message.content)) {
146
+ for (const block of message2.message.content) {
147
+ if (typeof block !== "object" || block.type !== "tool_result") continue;
148
+ const output = clip(resultText(block.content), limits.MAX_OUTPUT);
149
+ const what = commands.get(block.tool_use_id) ?? block.tool_use_id;
150
+ if (block.is_error) log2.error(`failed: ${what}
151
+ ${output}`);
152
+ else log2.debug(`output of ${what}
153
+ ${output}`);
154
+ }
155
+ }
156
+ }
157
+ function finish(result, log2) {
158
+ const stats = `${(result.duration_ms / 1e3).toFixed(1)}s, ${result.num_turns} turns, $${result.total_cost_usd.toFixed(4)}`;
159
+ for (const denial of result.permission_denials ?? []) {
160
+ log2.error(`denied tool ${denial.tool_name}: ${clip(JSON.stringify(denial.tool_input), limits.MAX_COMMAND)}`);
161
+ }
162
+ if (result.subtype === "success") {
163
+ log2.info(`done (${stats})`);
164
+ return void 0;
165
+ }
166
+ const errors = result.errors?.length ? `: ${result.errors.join("; ")}` : "";
167
+ log2.error(`ended ${result.subtype} (${stats})${errors}`);
168
+ return new AgentRunError(`Agent run ended: ${result.subtype}${errors}`);
169
+ }
170
+ async function runAgent(prompt, session, label = "run") {
171
+ const log2 = runLog(label);
172
+ const options = agentOptions(session, log2);
173
+ const said = [];
174
+ const commands = /* @__PURE__ */ new Map();
175
+ log2.info(`started: ${clip(prompt.split("\n")[0] ?? "", 120)}`);
176
+ try {
177
+ for await (const message2 of query({ prompt, options })) {
178
+ logMessage(message2, log2, commands);
179
+ if (message2.type === "assistant") said.push(proseOf(message2));
180
+ if (message2.type === "result") {
181
+ const error = finish(message2, log2);
182
+ if (error) throw error;
183
+ return said.join("").trim();
184
+ }
185
+ }
186
+ } catch (error) {
187
+ if (!(error instanceof AgentRunError)) {
188
+ log2.error(`crashed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
189
+ }
190
+ throw error;
191
+ }
192
+ log2.error("ended without a result");
193
+ throw new AgentRunError("Agent run ended without a result.");
194
+ }
195
+
196
+ // src/agent/agent-id.ts
197
+ import { mkdtemp, readFile } from "fs/promises";
198
+ import { tmpdir } from "os";
199
+ import { join } from "path";
200
+ import { createPublicClient, getAbiItem, http } from "viem";
201
+ import { privateKeyToAccount } from "viem/accounts";
202
+
203
+ // src/abi/registries.ts
204
+ var identityRegistryAbi = [
205
+ {
206
+ type: "function",
207
+ name: "register",
208
+ inputs: [
209
+ { name: "agentURI", type: "string" },
210
+ {
211
+ name: "metadata",
212
+ type: "tuple[]",
213
+ components: [
214
+ { name: "metadataKey", type: "string" },
215
+ { name: "metadataValue", type: "bytes" }
216
+ ]
217
+ }
218
+ ],
219
+ outputs: [{ name: "agentId", type: "uint256" }],
220
+ stateMutability: "nonpayable"
221
+ },
222
+ {
223
+ type: "function",
224
+ name: "setMetadata",
225
+ inputs: [
226
+ { name: "agentId", type: "uint256" },
227
+ { name: "key", type: "string" },
228
+ { name: "value", type: "bytes" }
229
+ ],
230
+ outputs: [],
231
+ stateMutability: "nonpayable"
232
+ },
233
+ {
234
+ type: "function",
235
+ name: "getMetadata",
236
+ inputs: [
237
+ { name: "agentId", type: "uint256" },
238
+ { name: "key", type: "string" }
239
+ ],
240
+ outputs: [{ name: "", type: "bytes" }],
241
+ stateMutability: "view"
242
+ },
243
+ {
244
+ type: "function",
245
+ name: "ownerOf",
246
+ inputs: [{ name: "tokenId", type: "uint256" }],
247
+ outputs: [{ name: "", type: "address" }],
248
+ stateMutability: "view"
249
+ },
250
+ {
251
+ type: "function",
252
+ name: "balanceOf",
253
+ inputs: [{ name: "owner", type: "address" }],
254
+ outputs: [{ name: "", type: "uint256" }],
255
+ stateMutability: "view"
256
+ },
257
+ {
258
+ type: "function",
259
+ name: "tokenURI",
260
+ inputs: [{ name: "tokenId", type: "uint256" }],
261
+ outputs: [{ name: "", type: "string" }],
262
+ stateMutability: "view"
263
+ },
264
+ {
265
+ type: "event",
266
+ name: "Registered",
267
+ inputs: [
268
+ { name: "agentId", type: "uint256", indexed: true },
269
+ { name: "agentURI", type: "string", indexed: false },
270
+ { name: "owner", type: "address", indexed: true }
271
+ ],
272
+ anonymous: false
273
+ },
274
+ {
275
+ type: "event",
276
+ name: "Transfer",
277
+ inputs: [
278
+ { name: "from", type: "address", indexed: true },
279
+ { name: "to", type: "address", indexed: true },
280
+ { name: "tokenId", type: "uint256", indexed: true }
281
+ ],
282
+ anonymous: false
283
+ }
284
+ ];
285
+
286
+ // src/networks.ts
287
+ import { defineChain } from "viem";
288
+ var robinhood = defineChain({
289
+ id: 4663,
290
+ name: "Robinhood Chain",
291
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
292
+ rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },
293
+ blockExplorers: {
294
+ default: { name: "Blockscout", url: "https://robinhoodchain.blockscout.com" }
295
+ },
296
+ contracts: {
297
+ multicall3: { address: "0xca11bde05977b3631167028862be2a173976ca11" }
298
+ }
299
+ });
300
+ var networks = {
301
+ /** Robinhood Chain mainnet, where app.kudo.archi reads and writes. */
302
+ robinhood: {
303
+ key: "robinhood",
304
+ chain: robinhood,
305
+ kudo: "0x1930D0FEaB14b024353bF22adE03Db989aD640C7",
306
+ usdg: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
307
+ identityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
308
+ reputationRegistry: "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63",
309
+ statuses: ["MINTED", "IN_PROGRESS", "COMPLETED", "FAILED"],
310
+ covenantUrl: "https://app.kudo.archi/share/{id}",
311
+ explorerUrl: "https://robinhoodchain.blockscout.com",
312
+ maxLogRange: 10000000n
313
+ }
314
+ };
315
+
316
+ // src/agent/agent-id.ts
317
+ var ID = /^\d+$/;
318
+ async function findRegisteredAgentId(client, registry, owner, maxLogRange = networks.robinhood.maxLogRange) {
319
+ const balance = await client.readContract({
320
+ address: registry,
321
+ abi: identityRegistryAbi,
322
+ functionName: "balanceOf",
323
+ args: [owner]
324
+ });
325
+ if (balance === 0n) return void 0;
326
+ const registered = getAbiItem({ abi: identityRegistryAbi, name: "Registered" });
327
+ let to = await client.getBlockNumber();
328
+ for (; ; ) {
329
+ const from = to >= maxLogRange ? to - maxLogRange + 1n : 0n;
330
+ const logs = await client.getLogs({ address: registry, event: registered, args: { owner }, fromBlock: from, toBlock: to });
331
+ for (const log2 of logs.reverse()) {
332
+ const id = log2.args.agentId;
333
+ if (id === void 0) continue;
334
+ const current = await client.readContract({
335
+ address: registry,
336
+ abi: identityRegistryAbi,
337
+ functionName: "ownerOf",
338
+ args: [id]
339
+ });
340
+ if (current.toLowerCase() === owner.toLowerCase()) return id;
341
+ }
342
+ if (from === 0n) return void 0;
343
+ to = from - 1n;
344
+ }
345
+ }
346
+ async function resolveAgentId(env = process.env) {
347
+ delete env.AGENT_ID;
348
+ const owner = privateKeyToAccount(env.PRIVATE_KEY.trim()).address;
349
+ const client = createPublicClient({ transport: http(env.RPC_URL.trim()) });
350
+ const id = await findRegisteredAgentId(client, env.IDENTITY_REGISTRY.trim(), owner);
351
+ if (id === void 0) {
352
+ console.log(`no ERC-8004 identity registered by ${owner} \u2014 the agent registers one at its first run`);
353
+ return;
354
+ }
355
+ env.AGENT_ID = id.toString();
356
+ console.log(`AGENT_ID ${env.AGENT_ID}, registered by ${owner}`);
357
+ }
358
+ async function prepareAgentIdFile(env = process.env) {
359
+ const file = join(await mkdtemp(join(tmpdir(), "kudo-agent-id-")), "agent-id");
360
+ env.KUDO_AGENT_ID_FILE = file;
361
+ return file;
362
+ }
363
+ async function syncAgentId(env = process.env) {
364
+ const file = env.KUDO_AGENT_ID_FILE;
365
+ if (!file) return void 0;
366
+ let recorded;
367
+ try {
368
+ recorded = (await readFile(file, "utf8")).trim();
369
+ } catch {
370
+ return void 0;
371
+ }
372
+ if (!ID.test(recorded)) {
373
+ if (recorded) console.error(`Ignoring ${file}: "${recorded.slice(0, 40)}" is not an agent id.`);
374
+ return void 0;
375
+ }
376
+ const previous = env.AGENT_ID?.trim();
377
+ env.AGENT_ID = recorded;
378
+ return previous === recorded ? void 0 : recorded;
379
+ }
380
+
381
+ // src/agent/config.ts
382
+ var REQUIRED = [
383
+ "ANTHROPIC_API_KEY",
384
+ "PRIVATE_KEY",
385
+ "RPC_URL",
386
+ "KUDO",
387
+ "IDENTITY_REGISTRY"
388
+ ];
389
+ var TWITTER = [
390
+ "TWITTER_API_KEY",
391
+ "TWITTER_API_SECRET_KEY",
392
+ "TWITTER_ACCESS_TOKEN",
393
+ "TWITTER_ACCESS_TOKEN_SECRET",
394
+ "TWITTER_HANDLE"
395
+ ];
396
+ var unset = (keys) => keys.filter((key) => !process.env[key]?.trim());
397
+ function requireEnvironment() {
398
+ const missing = unset(REQUIRED);
399
+ if (missing.length === 0) return;
400
+ console.error(`Missing required environment variables: ${missing.join(", ")}`);
401
+ console.error("See .env.example in @kudo-dev/sdk.");
402
+ process.exit(1);
403
+ }
404
+ function warnIfTwitterIncomplete() {
405
+ const missing = unset(TWITTER);
406
+ if (missing.length === 0 || missing.length === TWITTER.length) return;
407
+ console.error(
408
+ `Warning: X credentials are partially set (missing ${missing.join(", ")}). Posting will fail with 403 Unsupported Authentication.`
409
+ );
410
+ }
411
+
412
+ // src/agent/jobs.ts
413
+ var DEFAULT_JOBS = { "settle-due": { interval: 300 } };
414
+ var MIN_INTERVAL_SECONDS = 10;
415
+ var log = (job, message2) => console.log(`${(/* @__PURE__ */ new Date()).toISOString().slice(11, 19)} ${job}: ${redact(message2)}`);
416
+ function spec(name, seconds, prompt, source) {
417
+ if (!name || !Number.isFinite(seconds) || seconds < MIN_INTERVAL_SECONDS) {
418
+ throw new Error(`${source}: "${name}:${seconds}" is not <name>:<seconds> with seconds >= ${MIN_INTERVAL_SECONDS}.`);
419
+ }
420
+ return prompt === void 0 ? { name, intervalSeconds: seconds } : { name, intervalSeconds: seconds, prompt };
421
+ }
422
+ function resolveJobs(own, env = process.env) {
423
+ const raw = env.JOBS?.trim();
424
+ if (raw) {
425
+ return raw.split(",").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
426
+ const [name = "", seconds = ""] = entry.split(":").map((part) => part.trim());
427
+ return spec(name, Number(seconds), own?.[name]?.prompt, "JOBS");
428
+ });
429
+ }
430
+ if (env.TICK_INTERVAL_SECONDS?.trim() === "0") return [];
431
+ return Object.entries(own ?? DEFAULT_JOBS).map(([name, job]) => spec(name, Number(job?.interval), job?.prompt, "jobs"));
432
+ }
433
+ var lock = Promise.resolve();
434
+ function exclusive(work) {
435
+ const next = lock.then(work, work);
436
+ lock = next.then(
437
+ () => void 0,
438
+ () => void 0
439
+ );
440
+ return next;
441
+ }
442
+ function whenRunsIdle() {
443
+ return lock;
444
+ }
445
+ function startJob(job, run) {
446
+ let running = true;
447
+ let inFlight = Promise.resolve();
448
+ async function once() {
449
+ try {
450
+ const answer = await exclusive(() => run(job));
451
+ log(job.name, answer || "(no response)");
452
+ } catch (error) {
453
+ log(job.name, `failed: ${error instanceof Error ? error.message : String(error)}`);
454
+ }
455
+ }
456
+ async function loop() {
457
+ log(job.name, `every ${job.intervalSeconds}s`);
458
+ while (running) {
459
+ inFlight = once();
460
+ await inFlight;
461
+ const until = Date.now() + job.intervalSeconds * 1e3;
462
+ while (running && Date.now() < until) {
463
+ await new Promise((r) => setTimeout(r, Math.min(1e3, until - Date.now())));
464
+ }
465
+ }
466
+ log(job.name, "stopped");
467
+ }
468
+ const finished = loop();
469
+ return {
470
+ stop() {
471
+ running = false;
472
+ },
473
+ async whenIdle() {
474
+ await inFlight;
475
+ await finished;
476
+ }
477
+ };
478
+ }
479
+ function startJobs(jobs, run) {
480
+ if (jobs.length === 0) {
481
+ console.log("no scheduled jobs \u2014 something else owns this agent's work");
482
+ return { stop() {
483
+ }, async whenIdle() {
484
+ } };
485
+ }
486
+ const started = jobs.map((job) => startJob(job, run));
487
+ return {
488
+ stop: () => started.forEach((job) => job.stop()),
489
+ whenIdle: async () => {
490
+ await Promise.all(started.map((job) => job.whenIdle()));
491
+ }
492
+ };
493
+ }
494
+
495
+ // src/agent/prompts.ts
496
+ import { existsSync as existsSync2 } from "fs";
497
+ import { readFile as readFile2, readdir } from "fs/promises";
498
+ import { join as join3 } from "path";
499
+ import { fileURLToPath as fileURLToPath2 } from "url";
500
+
501
+ // src/agent/paths.ts
502
+ import { existsSync, readFileSync } from "fs";
503
+ import { dirname, join as join2 } from "path";
504
+ import { fileURLToPath } from "url";
505
+ function findPackageDir() {
506
+ let dir = dirname(fileURLToPath(import.meta.url));
507
+ for (; ; ) {
508
+ const manifest = join2(dir, "package.json");
509
+ if (existsSync(manifest)) {
510
+ const { name } = JSON.parse(readFileSync(manifest, "utf8"));
511
+ if (name === "@kudo-dev/sdk") return dir;
512
+ }
513
+ const parent = dirname(dir);
514
+ if (parent === dir) throw new Error("Cannot find the @kudo-dev/sdk package directory.");
515
+ dir = parent;
516
+ }
517
+ }
518
+ var PACKAGE_DIR = findPackageDir();
519
+ var PROMPTS_DIR = join2(PACKAGE_DIR, "prompts");
520
+ var SKILLS_DIR = join2(PACKAGE_DIR, "skills");
521
+
522
+ // src/agent/prompts.ts
523
+ var SAFE_NAME = /^[a-z0-9][a-z0-9-]*$/i;
524
+ var LOOKS_LIKE_PATH = /^(\.{1,2}\/|\/)\S*$|^\S+\.(md|txt)$/i;
525
+ async function read(source) {
526
+ if (source instanceof URL) return readFile2(fileURLToPath2(source), "utf8");
527
+ if (!source.includes("\n") && existsSync2(source)) return readFile2(source, "utf8");
528
+ if (!source.includes("\n") && LOOKS_LIKE_PATH.test(source.trim())) {
529
+ throw new Error(`Prompt file ${source} does not exist.`);
530
+ }
531
+ return source;
532
+ }
533
+ async function loadSystemPrompt(character) {
534
+ const common = await readFile2(join3(PROMPTS_DIR, "common.md"), "utf8");
535
+ return `${common.trim()}
536
+
537
+ ---
538
+
539
+ ${(await read(character)).trim()}`;
540
+ }
541
+ async function bundledPrompts() {
542
+ return (await readdir(PROMPTS_DIR)).filter((f) => f.endsWith(".txt")).map((f) => f.replace(/\.txt$/, "")).sort();
543
+ }
544
+ async function loadPrompt(name, own) {
545
+ if (own !== void 0) return (await read(own)).trim();
546
+ if (!SAFE_NAME.test(name)) {
547
+ throw new Error(`Invalid prompt name "${name}" \u2014 use letters, digits and dashes.`);
548
+ }
549
+ try {
550
+ return (await readFile2(join3(PROMPTS_DIR, `${name}.txt`), "utf8")).trim();
551
+ } catch {
552
+ throw new Error(
553
+ `Job "${name}" has no prompt and none is bundled under that name. Give it a \`prompt\`, or use one of: ${(await bundledPrompts()).join(", ")}.`
554
+ );
555
+ }
556
+ }
557
+
558
+ // src/agent/server/index.ts
559
+ import { createServer } from "http";
560
+
561
+ // src/agent/server/http.ts
562
+ var MAX_BODY_BYTES = 64 * 1024;
563
+ function sendJson(response, status, body) {
564
+ const payload = JSON.stringify(body);
565
+ response.writeHead(status, {
566
+ "Content-Type": "application/json",
567
+ "Content-Length": Buffer.byteLength(payload)
568
+ });
569
+ response.end(payload);
570
+ }
571
+ async function readBody(request) {
572
+ const chunks = [];
573
+ let size = 0;
574
+ for await (const chunk of request) {
575
+ size += chunk.length;
576
+ if (size > MAX_BODY_BYTES) throw new Error("Request body too large.");
577
+ chunks.push(chunk);
578
+ }
579
+ return Buffer.concat(chunks).toString("utf8");
580
+ }
581
+ function allowLongRun(request, response) {
582
+ request.setTimeout(0);
583
+ response.setTimeout(0);
584
+ }
585
+
586
+ // src/agent/server/handlers.ts
587
+ function health(_request, response) {
588
+ sendJson(response, 200, { ok: true });
589
+ }
590
+ function identity(_request, response) {
591
+ sendJson(response, 200, {
592
+ name: process.env.AGENT_NAME?.trim() || null,
593
+ agentId: process.env.AGENT_ID?.trim() || null,
594
+ teeId: process.env.TEE_ID?.trim() || null,
595
+ attestation: process.env.TEE_TRUST_URL?.trim() || null,
596
+ // False for a hand-set TEE_ID: the report exists, but describes another app.
597
+ attested: process.env.TEE_ID_SOURCE?.trim() === "dstack"
598
+ });
599
+ }
600
+ function notFound(_request, response) {
601
+ sendJson(response, 404, { error: "Not found. Try POST /message, GET /identity or GET /health." });
602
+ }
603
+ async function message(request, response, run) {
604
+ let body;
605
+ try {
606
+ body = JSON.parse(await readBody(request));
607
+ } catch (error) {
608
+ return sendJson(response, 400, {
609
+ error: error instanceof Error ? error.message : "Invalid JSON body."
610
+ });
611
+ }
612
+ const text = body?.message;
613
+ if (typeof text !== "string" || !text.trim()) {
614
+ return sendJson(response, 400, { error: 'Body must be {"message": "<text>"}.' });
615
+ }
616
+ try {
617
+ allowLongRun(request, response);
618
+ sendJson(response, 200, { response: redact(await run(text.trim())) });
619
+ } catch (error) {
620
+ const reason = redact(error instanceof Error ? error.message : String(error));
621
+ console.error(`POST /message failed: ${reason}`);
622
+ sendJson(response, error instanceof AgentRunError ? 502 : 500, { error: reason });
623
+ }
624
+ }
625
+
626
+ // src/agent/server/index.ts
627
+ var DEFAULT_PORT = 3e3;
628
+ function startServer(run, port) {
629
+ const listenOn = port ?? Number(process.env.PORT?.trim() || DEFAULT_PORT);
630
+ const routes = [
631
+ { method: "GET", path: "/health", handle: health },
632
+ { method: "GET", path: "/identity", handle: identity },
633
+ {
634
+ method: "POST",
635
+ path: "/message",
636
+ handle: (request, response) => message(request, response, run)
637
+ }
638
+ ];
639
+ function route(request, response) {
640
+ const match = routes.find(
641
+ (candidate) => candidate.method === request.method && candidate.path === request.url
642
+ );
643
+ void (match ? match.handle(request, response) : notFound(request, response));
644
+ }
645
+ const server = createServer(route);
646
+ server.listen(listenOn, () => console.log(`listening on :${listenOn}`));
647
+ return {
648
+ port: listenOn,
649
+ close: () => server.close()
650
+ };
651
+ }
652
+
653
+ // src/agent/skills.ts
654
+ import { existsSync as existsSync3 } from "fs";
655
+ import { mkdir, mkdtemp as mkdtemp2, readdir as readdir2, symlink, writeFile } from "fs/promises";
656
+ import { tmpdir as tmpdir2 } from "os";
657
+ import { basename, isAbsolute, join as join4, resolve } from "path";
658
+ import { fileURLToPath as fileURLToPath3 } from "url";
659
+ var DEFAULT_SKILLS = ["kudo", "kudo-runtime", "twitter"];
660
+ var PLUGIN_NAME = "kudo";
661
+ async function bundledSkills() {
662
+ const entries = await readdir2(SKILLS_DIR, { withFileTypes: true });
663
+ return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name).sort();
664
+ }
665
+ function resolveSkill(ref) {
666
+ const isPath = ref instanceof URL || isAbsolute(ref) || ref.startsWith(".");
667
+ const dir = ref instanceof URL ? fileURLToPath3(ref) : isPath ? resolve(ref) : join4(SKILLS_DIR, ref);
668
+ if (!existsSync3(join4(dir, "SKILL.md"))) {
669
+ throw new Error(
670
+ isPath ? `Skill folder ${dir} has no SKILL.md.` : `No bundled skill "${ref}". Bundled: see ${SKILLS_DIR}, or pass a path to your own skill folder.`
671
+ );
672
+ }
673
+ return { name: basename(dir), dir };
674
+ }
675
+ async function assembleSkills(extra = []) {
676
+ const skills = /* @__PURE__ */ new Map();
677
+ for (const ref of [...DEFAULT_SKILLS, ...extra]) {
678
+ const { name, dir } = resolveSkill(ref);
679
+ const existing = skills.get(name);
680
+ if (existing && existing !== dir) throw new Error(`Two skills are named "${name}": ${existing} and ${dir}.`);
681
+ skills.set(name, dir);
682
+ }
683
+ const root = await mkdtemp2(join4(tmpdir2(), "kudo-agent-skills-"));
684
+ await mkdir(join4(root, ".claude-plugin"));
685
+ await writeFile(
686
+ join4(root, ".claude-plugin", "plugin.json"),
687
+ JSON.stringify({ name: PLUGIN_NAME, description: "Kudo agent skills", version: "0.0.0" })
688
+ );
689
+ await mkdir(join4(root, "skills"));
690
+ for (const [name, dir] of skills) await symlink(dir, join4(root, "skills", name), "dir");
691
+ return { path: root, names: [...skills.keys()].map((name) => `${PLUGIN_NAME}:${name}`) };
692
+ }
693
+
694
+ // src/errors.ts
695
+ import { BaseError, ContractFunctionRevertedError } from "viem";
696
+ var KudoError = class extends Error {
697
+ code;
698
+ revert;
699
+ constructor(code, message2, options = {}) {
700
+ super(message2, { cause: options.cause });
701
+ this.name = "KudoError";
702
+ this.code = code;
703
+ this.revert = options.revert;
704
+ }
705
+ };
706
+
707
+ // src/dstack.ts
708
+ var DSTACK_SOCKET = "/var/run/dstack.sock";
709
+ async function call(path, body, options) {
710
+ const { request } = await import("http");
711
+ const socketPath = options.socket ?? DSTACK_SOCKET;
712
+ return new Promise((resolve3, reject) => {
713
+ const payload = body === void 0 ? void 0 : JSON.stringify(body);
714
+ const req = request(
715
+ {
716
+ socketPath,
717
+ path,
718
+ method: payload ? "POST" : "GET",
719
+ headers: payload ? { "content-type": "application/json", "content-length": Buffer.byteLength(payload) } : {},
720
+ timeout: options.timeoutMs ?? 5e3
721
+ },
722
+ (res) => {
723
+ const chunks = [];
724
+ res.on("data", (c) => chunks.push(c));
725
+ res.on("end", () => {
726
+ if (res.statusCode !== 200) return reject(new KudoError("NOT_FOUND", `dstack ${path} returned ${res.statusCode}.`));
727
+ try {
728
+ resolve3(JSON.parse(Buffer.concat(chunks).toString("utf8")));
729
+ } catch {
730
+ reject(new KudoError("NOT_FOUND", `dstack ${path} did not return JSON.`));
731
+ }
732
+ });
733
+ }
734
+ );
735
+ req.on("timeout", () => req.destroy(new Error("timed out")));
736
+ req.on(
737
+ "error",
738
+ (e) => reject(new KudoError("NOT_FOUND", `Cannot reach dstack at ${socketPath} (${e.message}). Is this running in a Phala TEE?`))
739
+ );
740
+ req.end(payload);
741
+ });
742
+ }
743
+ async function dstackAppId(options = {}) {
744
+ const { app_id } = await call("/Info", void 0, options);
745
+ if (!app_id) throw new KudoError("NOT_FOUND", "dstack Info returned no app_id.");
746
+ return app_id.replace(/^0x/i, "").toLowerCase();
747
+ }
748
+
749
+ // src/agent/tee.ts
750
+ var TRUST_CENTER = "https://trust.phala.com/app";
751
+ var INFO_TIMEOUT_MS = 2e3;
752
+ function bareHex(appId) {
753
+ return appId.trim().replace(/^0x/i, "").toLowerCase();
754
+ }
755
+ async function appIdFromDstack() {
756
+ try {
757
+ return await dstackAppId({ timeoutMs: INFO_TIMEOUT_MS });
758
+ } catch (error) {
759
+ const reason = error instanceof Error ? error.message : String(error);
760
+ console.log(`no dstack socket (${reason}) \u2014 looking at the environment instead`);
761
+ return void 0;
762
+ }
763
+ }
764
+ async function resolveTeeIdentity() {
765
+ const attested = await appIdFromDstack();
766
+ const appId = attested || process.env.DSTACK_APP_ID?.trim() || process.env.TEE_ID?.trim();
767
+ if (!appId) return void 0;
768
+ const teeId = bareHex(appId);
769
+ return {
770
+ teeId,
771
+ trustCenterUrl: `${TRUST_CENTER}/${teeId}`,
772
+ source: attested ? "dstack" : "environment"
773
+ };
774
+ }
775
+
776
+ // src/agent/start.ts
777
+ var SHUTDOWN_GRACE_MS = Number(process.env.SHUTDOWN_GRACE_SECONDS?.trim() || 120) * 1e3;
778
+ async function announceTee() {
779
+ const tee = await resolveTeeIdentity();
780
+ if (!tee) {
781
+ console.log("no TEE app id \u2014 running without attestation");
782
+ return;
783
+ }
784
+ process.env.TEE_ID = tee.teeId;
785
+ process.env.TEE_TRUST_URL = tee.trustCenterUrl;
786
+ process.env.TEE_ID_SOURCE = tee.source;
787
+ if (tee.source === "dstack") {
788
+ console.log(`attestation: ${tee.trustCenterUrl}`);
789
+ return;
790
+ }
791
+ console.log(`TEE_ID set by hand: ${tee.trustCenterUrl}`);
792
+ console.log(" that report is NOT this process \u2014 local stand-in, attests nothing");
793
+ }
794
+ function defaultCwd(source) {
795
+ if (source instanceof URL) return dirname2(fileURLToPath4(source));
796
+ if (!source.includes("\n") && existsSync4(source)) return dirname2(resolve2(source));
797
+ return process.cwd();
798
+ }
799
+ async function createSession(options) {
800
+ const skills = await assembleSkills(options.skills);
801
+ return {
802
+ systemPrompt: await loadSystemPrompt(options.systemPrompt),
803
+ skills,
804
+ cwd: resolve2(options.cwd ?? defaultCwd(options.systemPrompt)),
805
+ model: options.model ?? "claude-opus-5"
806
+ };
807
+ }
808
+ async function startAgent(options) {
809
+ requireEnvironment();
810
+ warnIfTwitterIncomplete();
811
+ await resolveAgentId();
812
+ await announceTee();
813
+ process.env.KUDO_SPONSORED_SEND = join5(SKILLS_DIR, "kudo", "sponsored-send.mjs");
814
+ const session = await createSession(options);
815
+ console.log(`skills: ${session.skills.names.join(", ")}`);
816
+ const jobs = resolveJobs(options.jobs);
817
+ for (const job of jobs) await loadPrompt(job.name, job.prompt);
818
+ await prepareAgentIdFile();
819
+ const beforeRun = async () => {
820
+ const recorded = await syncAgentId();
821
+ if (recorded) console.log(`AGENT_ID ${recorded}, as the agent recorded it after registering`);
822
+ };
823
+ const run = (prompt) => exclusive(async () => {
824
+ await beforeRun();
825
+ return runAgent(prompt, session, "message");
826
+ });
827
+ const runJob = async (job) => {
828
+ await beforeRun();
829
+ return runAgent(await loadPrompt(job.name, job.prompt), session, job.name);
830
+ };
831
+ const server = startServer(run, options.port);
832
+ const scheduled = startJobs(jobs, runJob);
833
+ let shuttingDown = false;
834
+ function shutdown() {
835
+ if (shuttingDown) {
836
+ console.log("second signal \u2014 exiting now");
837
+ process.exit(130);
838
+ }
839
+ shuttingDown = true;
840
+ console.log(`shutting down (up to ${SHUTDOWN_GRACE_MS / 1e3}s for the current run)`);
841
+ server.close();
842
+ scheduled.stop();
843
+ const deadline = new Promise(
844
+ (resolveTimeout) => setTimeout(() => resolveTimeout("timeout"), SHUTDOWN_GRACE_MS).unref()
845
+ );
846
+ const idle = Promise.all([scheduled.whenIdle(), whenRunsIdle()]);
847
+ void Promise.race([idle, deadline]).then((outcome) => {
848
+ if (outcome === "timeout") console.error("run did not finish in time \u2014 exiting anyway");
849
+ process.exit(0);
850
+ });
851
+ }
852
+ process.on("SIGTERM", shutdown);
853
+ process.on("SIGINT", shutdown);
854
+ }
855
+ export {
856
+ AgentRunError,
857
+ DEFAULT_SKILLS,
858
+ bundledPrompts,
859
+ bundledSkills,
860
+ createSession,
861
+ loadPrompt,
862
+ loadSystemPrompt,
863
+ resolveJobs,
864
+ runAgent,
865
+ startAgent
866
+ };
867
+ //# sourceMappingURL=index.js.map