@kohala/devkit 0.1.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,2246 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { createRequire } from "module";
5
+ import { Command } from "commander";
6
+ import pc10 from "picocolors";
7
+
8
+ // src/cli/init.ts
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { fileURLToPath } from "url";
12
+ import pc from "picocolors";
13
+
14
+ // src/manifest/schema.ts
15
+ import { z } from "zod";
16
+ var BILLING_PERIODS = ["day", "week", "month"];
17
+ var shapeValidatorSchema = z.object({
18
+ type: z.literal("shape"),
19
+ minBytes: z.number().int().nonnegative()
20
+ }).strict();
21
+ var freshnessValidatorSchema = z.object({
22
+ type: z.literal("freshness"),
23
+ /** Logical memory key of the asset that must be fresh. */
24
+ asset: z.string().min(1),
25
+ maxAgeHours: z.number().positive()
26
+ }).strict();
27
+ var invariantValidatorSchema = z.object({
28
+ type: z.literal("invariant"),
29
+ /** JavaScript-flavored regular expression source (no delimiters). */
30
+ pattern: z.string().min(1),
31
+ /** When false, the output must NOT match the pattern. Defaults to true. */
32
+ mustMatch: z.boolean().default(true)
33
+ }).strict();
34
+ var validatorSchema = z.discriminatedUnion("type", [
35
+ shapeValidatorSchema,
36
+ freshnessValidatorSchema,
37
+ invariantValidatorSchema
38
+ ]);
39
+ var capsSchema = z.object({
40
+ /** Hard token ceiling for a single shift (platform: agentPerRunTokenCap). */
41
+ perRunTokens: z.number().int().positive(),
42
+ /** Cumulative token ceiling per UTC day (platform: agentPerDayTokenCap). */
43
+ perDayTokens: z.number().int().positive(),
44
+ /** Billing-period token cap (platform: agentBillingCapTokens). Ignored locally. */
45
+ billingTokens: z.number().int().positive().optional(),
46
+ /** Billing period for billingTokens (platform: agentBillingCapPeriod). */
47
+ billingPeriod: z.enum(BILLING_PERIODS).optional()
48
+ }).strict().refine((caps) => caps.billingTokens === void 0 || caps.billingPeriod !== void 0, {
49
+ message: "caps.billingPeriod is required when caps.billingTokens is set",
50
+ path: ["billingPeriod"]
51
+ });
52
+ var nameSchema = z.string().min(1).max(64).regex(
53
+ /^[a-z0-9][a-z0-9_-]*$/i,
54
+ "must start with a letter or digit and contain only letters, digits, '-' and '_'"
55
+ );
56
+ var manifestSchema = z.object({
57
+ name: nameSchema,
58
+ /** The agent's mission text (platform: agentCharter). */
59
+ charter: z.string().min(1),
60
+ /**
61
+ * Tools the agent may call, e.g. ["s3.put", "http.post_json"].
62
+ * Everything not on this list is denied loudly at runtime.
63
+ */
64
+ toolAllowlist: z.array(z.string().min(1)).default([]),
65
+ /**
66
+ * "wrap" — execute the skill script directly and validate its output.
67
+ * "llm" — run a real tool-use loop against the developer's own LLM key.
68
+ */
69
+ runtimeMode: z.enum(["wrap", "llm"]),
70
+ /** Map of skill name -> script filename, e.g. {"collect": "main.py"}. */
71
+ skills: z.record(z.string().min(1), z.string().min(1)).default({}),
72
+ /** Cron expression. Used only on deploy; local runs are always manual. */
73
+ schedule: z.string().min(1).optional(),
74
+ caps: capsSchema,
75
+ validators: z.array(validatorSchema).default([])
76
+ }).strict();
77
+
78
+ // src/cli/init.ts
79
+ function templatesDir() {
80
+ const here = path.dirname(fileURLToPath(import.meta.url));
81
+ const candidates = [
82
+ path.resolve(here, "..", "..", "templates"),
83
+ path.resolve(here, "..", "..", "..", "templates")
84
+ ];
85
+ for (const candidate of candidates) {
86
+ if (fs.existsSync(path.join(candidate, "kohala.json"))) return candidate;
87
+ }
88
+ throw new Error(`Could not locate the devkit templates directory (looked in: ${candidates.join(", ")})`);
89
+ }
90
+ function copyTemplates(sourceDir, targetDir, agentName) {
91
+ fs.mkdirSync(targetDir, { recursive: true });
92
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
93
+ const sourcePath = path.join(sourceDir, entry.name);
94
+ const targetPath = path.join(targetDir, entry.name);
95
+ if (entry.isDirectory()) {
96
+ copyTemplates(sourcePath, targetPath, agentName);
97
+ } else {
98
+ const contents = fs.readFileSync(sourcePath, "utf8");
99
+ fs.writeFileSync(targetPath, contents.replaceAll("{{AGENT_NAME}}", agentName), "utf8");
100
+ }
101
+ }
102
+ }
103
+ function registerInitCommand(program2) {
104
+ program2.command("init").argument("<name>", "agent name (also the directory name)").description("Scaffold a new agent: kohala.json, a working skill, and the local SDK").action((name) => {
105
+ const nameCheck = manifestSchema.shape.name.safeParse(name);
106
+ if (!nameCheck.success) {
107
+ throw new Error(
108
+ `"${name}" is not a valid agent name: ${nameCheck.error.issues[0]?.message ?? "invalid"}`
109
+ );
110
+ }
111
+ const targetDir = path.resolve(process.cwd(), name);
112
+ if (fs.existsSync(targetDir)) {
113
+ throw new Error(`${targetDir} already exists \u2014 pick a new name or remove the directory.`);
114
+ }
115
+ copyTemplates(templatesDir(), targetDir, name);
116
+ console.log(pc.green(`Created agent "${name}" in ${targetDir}`));
117
+ console.log("");
118
+ console.log("Next steps:");
119
+ console.log(pc.cyan(` kohala validate ${name}`));
120
+ console.log(pc.cyan(` kohala run ${name} --local`));
121
+ console.log(pc.cyan(` kohala trace ${name}`));
122
+ });
123
+ }
124
+
125
+ // src/cli/validate.ts
126
+ import path3 from "path";
127
+ import pc2 from "picocolors";
128
+
129
+ // src/manifest/load.ts
130
+ import fs2 from "fs";
131
+ import path2 from "path";
132
+ var ManifestError = class extends Error {
133
+ /** One human-readable problem per line, each with a fix hint when we have one. */
134
+ problems;
135
+ constructor(message, problems = []) {
136
+ super(message);
137
+ this.name = "ManifestError";
138
+ this.problems = problems;
139
+ }
140
+ };
141
+ var FIELD_HINTS = {
142
+ name: 'use a short identifier like "my-agent" (letters, digits, "-", "_")',
143
+ charter: "write the agent's mission as a non-empty string",
144
+ toolAllowlist: 'list allowed tool names, e.g. ["s3.put", "http.post_json"]',
145
+ runtimeMode: 'must be "wrap" (script wrapper) or "llm" (tool-use loop)',
146
+ skills: 'map skill name to script filename, e.g. {"collect": "main.py"}',
147
+ schedule: 'use a cron expression like "0 9 * * *" (only used on deploy)',
148
+ caps: "set caps.perRunTokens and caps.perDayTokens as positive integers",
149
+ validators: 'each validator needs a "type" of "shape", "freshness" or "invariant"'
150
+ };
151
+ function formatIssue(issue) {
152
+ const issuePath = issue.path.length > 0 ? issue.path.join(".") : "(root)";
153
+ const topLevelField = String(issue.path[0] ?? "");
154
+ const hint = FIELD_HINTS[topLevelField];
155
+ const hintSuffix = hint ? ` (hint: ${hint})` : "";
156
+ return `${issuePath}: ${issue.message}${hintSuffix}`;
157
+ }
158
+ function formatManifestIssues(error) {
159
+ return error.issues.map(formatIssue);
160
+ }
161
+ function manifestPath(agentDir) {
162
+ return path2.join(agentDir, "kohala.json");
163
+ }
164
+ function loadManifest(agentDir) {
165
+ const filePath = manifestPath(agentDir);
166
+ if (!fs2.existsSync(filePath)) {
167
+ throw new ManifestError(
168
+ `No kohala.json found at ${filePath}`,
169
+ [`create one with: kohala init ${path2.basename(agentDir) || "<name>"}`]
170
+ );
171
+ }
172
+ let raw;
173
+ try {
174
+ raw = fs2.readFileSync(filePath, "utf8");
175
+ } catch (error) {
176
+ throw new ManifestError(`Could not read ${filePath}: ${error.message}`);
177
+ }
178
+ let parsed;
179
+ try {
180
+ parsed = JSON.parse(raw);
181
+ } catch (error) {
182
+ throw new ManifestError(`${filePath} is not valid JSON: ${error.message}`, [
183
+ "check for trailing commas or missing quotes"
184
+ ]);
185
+ }
186
+ const result = manifestSchema.safeParse(parsed);
187
+ if (!result.success) {
188
+ throw new ManifestError(`${filePath} failed validation`, formatManifestIssues(result.error));
189
+ }
190
+ return result.data;
191
+ }
192
+
193
+ // src/cli/validate.ts
194
+ function registerValidateCommand(program2) {
195
+ program2.command("validate").argument("<agent>", "agent directory (containing kohala.json)").description("Validate an agent's kohala.json and print precise errors").action((agent) => {
196
+ const agentDir = path3.resolve(process.cwd(), agent);
197
+ try {
198
+ const manifest = loadManifest(agentDir);
199
+ console.log(pc2.green(`kohala.json for "${manifest.name}" is valid.`));
200
+ console.log(
201
+ pc2.dim(
202
+ ` runtimeMode=${manifest.runtimeMode} skills=${Object.keys(manifest.skills).length} validators=${manifest.validators.length} allowlist=[${manifest.toolAllowlist.join(", ")}]`
203
+ )
204
+ );
205
+ } catch (error) {
206
+ if (error instanceof ManifestError) {
207
+ console.error(pc2.red(error.message));
208
+ for (const problem of error.problems) {
209
+ console.error(pc2.red(` \u2022 ${problem}`));
210
+ }
211
+ process.exitCode = 1;
212
+ return;
213
+ }
214
+ throw error;
215
+ }
216
+ });
217
+ }
218
+
219
+ // src/cli/run.ts
220
+ import path9 from "path";
221
+ import pc3 from "picocolors";
222
+
223
+ // src/memory/file.ts
224
+ import crypto from "crypto";
225
+ import fs4 from "fs";
226
+ import path5 from "path";
227
+
228
+ // src/memory/store.ts
229
+ var DEFAULT_CATEGORY = "agentoutput";
230
+ var DEFAULT_LIST_LIMIT = 100;
231
+
232
+ // src/util/lock.ts
233
+ import fs3 from "fs";
234
+ import path4 from "path";
235
+ var STALE_MS = 1e4;
236
+ var ACQUIRE_TIMEOUT_MS = 5e3;
237
+ var RETRY_DELAY_MS = 15;
238
+ function sleep(ms) {
239
+ const buffer = new SharedArrayBuffer(4);
240
+ Atomics.wait(new Int32Array(buffer), 0, 0, ms);
241
+ }
242
+ function withFileLock(filePath, fn) {
243
+ const lockDir = `${filePath}.lock`;
244
+ fs3.mkdirSync(path4.dirname(lockDir), { recursive: true });
245
+ const deadline = Date.now() + ACQUIRE_TIMEOUT_MS;
246
+ for (; ; ) {
247
+ try {
248
+ fs3.mkdirSync(lockDir);
249
+ break;
250
+ } catch (error) {
251
+ if (error.code !== "EEXIST") throw error;
252
+ try {
253
+ const age = Date.now() - fs3.statSync(lockDir).mtimeMs;
254
+ if (age > STALE_MS) {
255
+ fs3.rmdirSync(lockDir);
256
+ continue;
257
+ }
258
+ } catch {
259
+ continue;
260
+ }
261
+ if (Date.now() > deadline) {
262
+ throw new Error(
263
+ `Timed out waiting for lock ${lockDir} \u2014 another kohala process is holding it. If no other process is running, delete the directory and retry.`,
264
+ { cause: error }
265
+ );
266
+ }
267
+ sleep(RETRY_DELAY_MS);
268
+ }
269
+ }
270
+ try {
271
+ return fn();
272
+ } finally {
273
+ try {
274
+ fs3.rmdirSync(lockDir);
275
+ } catch {
276
+ }
277
+ }
278
+ }
279
+ function atomicWriteFile(filePath, content) {
280
+ fs3.mkdirSync(path4.dirname(filePath), { recursive: true });
281
+ const tempPath = `${filePath}.${process.pid}.tmp`;
282
+ fs3.writeFileSync(tempPath, content, "utf8");
283
+ fs3.renameSync(tempPath, filePath);
284
+ }
285
+
286
+ // src/memory/file.ts
287
+ function memoryDirFor(rootDir, agent) {
288
+ return path5.join(rootDir, ".kohala", "memory", agent);
289
+ }
290
+ var FileMemoryStore = class {
291
+ dir;
292
+ indexPath;
293
+ bodiesDir;
294
+ constructor(rootDir, agent) {
295
+ this.dir = memoryDirFor(rootDir, agent);
296
+ this.indexPath = path5.join(this.dir, "index.json");
297
+ this.bodiesDir = path5.join(this.dir, "bodies");
298
+ fs4.mkdirSync(this.bodiesDir, { recursive: true });
299
+ }
300
+ readIndex() {
301
+ if (!fs4.existsSync(this.indexPath)) {
302
+ return { records: [] };
303
+ }
304
+ const raw = fs4.readFileSync(this.indexPath, "utf8");
305
+ return JSON.parse(raw);
306
+ }
307
+ writeIndex(index) {
308
+ atomicWriteFile(this.indexPath, JSON.stringify(index, null, 2));
309
+ }
310
+ bodyPath(id) {
311
+ return path5.join(this.bodiesDir, id);
312
+ }
313
+ async put(key, body, category = DEFAULT_CATEGORY) {
314
+ return withFileLock(this.indexPath, () => {
315
+ const index = this.readIndex();
316
+ const now = (/* @__PURE__ */ new Date()).toISOString();
317
+ const existing = index.records.find((record2) => record2.active && record2.key === key);
318
+ if (existing) {
319
+ existing.category = category;
320
+ existing.size = body.byteLength;
321
+ existing.updatedAt = now;
322
+ fs4.writeFileSync(this.bodyPath(existing.id), body);
323
+ this.writeIndex(index);
324
+ return { ...existing };
325
+ }
326
+ const record = {
327
+ id: crypto.randomUUID(),
328
+ key,
329
+ category,
330
+ size: body.byteLength,
331
+ createdAt: now,
332
+ updatedAt: now,
333
+ active: true
334
+ };
335
+ fs4.writeFileSync(this.bodyPath(record.id), body);
336
+ index.records.push(record);
337
+ this.writeIndex(index);
338
+ return { ...record };
339
+ });
340
+ }
341
+ async get(keyOrId) {
342
+ const index = this.readIndex();
343
+ const record = index.records.find((entry) => entry.active && entry.key === keyOrId) ?? index.records.find((entry) => entry.active && entry.id === keyOrId);
344
+ if (!record) return null;
345
+ const filePath = this.bodyPath(record.id);
346
+ if (!fs4.existsSync(filePath)) {
347
+ throw new Error(
348
+ `Memory index lists ${record.key} (${record.id}) but its body file is missing at ${filePath}`
349
+ );
350
+ }
351
+ return { record: { ...record }, body: fs4.readFileSync(filePath) };
352
+ }
353
+ async list(prefix, limit = DEFAULT_LIST_LIMIT) {
354
+ const index = this.readIndex();
355
+ return index.records.filter((record) => record.active && (prefix === void 0 || record.key.startsWith(prefix))).sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1).slice(0, limit).map((record) => ({ ...record }));
356
+ }
357
+ async delete(keyOrId) {
358
+ return withFileLock(this.indexPath, () => {
359
+ const index = this.readIndex();
360
+ const record = index.records.find((entry) => entry.active && entry.key === keyOrId) ?? index.records.find((entry) => entry.active && entry.id === keyOrId);
361
+ if (!record) return null;
362
+ record.active = false;
363
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
364
+ const filePath = this.bodyPath(record.id);
365
+ if (fs4.existsSync(filePath)) {
366
+ fs4.unlinkSync(filePath);
367
+ }
368
+ this.writeIndex(index);
369
+ return { ...record };
370
+ });
371
+ }
372
+ async close() {
373
+ }
374
+ };
375
+
376
+ // src/memory/postgres.ts
377
+ import crypto2 from "crypto";
378
+ function rowToRecord(row) {
379
+ return {
380
+ id: row.id,
381
+ key: row.key,
382
+ category: row.category,
383
+ size: Number(row.size),
384
+ createdAt: new Date(row.created_at).toISOString(),
385
+ updatedAt: new Date(row.updated_at).toISOString(),
386
+ active: row.active
387
+ };
388
+ }
389
+ var PostgresMemoryStore = class _PostgresMemoryStore {
390
+ constructor(pool, agent) {
391
+ this.pool = pool;
392
+ this.agent = agent;
393
+ }
394
+ pool;
395
+ agent;
396
+ /** Connect, run the idempotent migration, and return a ready store. */
397
+ static async connect(url, agent) {
398
+ let pgModule;
399
+ try {
400
+ pgModule = await import("pg");
401
+ } catch {
402
+ throw new Error(
403
+ 'The postgres backend requires the optional "pg" package. Install it with: npm install pg'
404
+ );
405
+ }
406
+ const PoolCtor = pgModule.default?.Pool ?? pgModule.Pool;
407
+ const pool = new PoolCtor({ connectionString: url });
408
+ await pool.query(`
409
+ CREATE TABLE IF NOT EXISTS kohala_memory (
410
+ id UUID PRIMARY KEY,
411
+ agent TEXT NOT NULL,
412
+ key TEXT NOT NULL,
413
+ category TEXT NOT NULL,
414
+ body BYTEA,
415
+ size BIGINT NOT NULL,
416
+ created_at TIMESTAMPTZ NOT NULL,
417
+ updated_at TIMESTAMPTZ NOT NULL,
418
+ active BOOLEAN NOT NULL DEFAULT TRUE
419
+ )
420
+ `);
421
+ await pool.query(`
422
+ CREATE UNIQUE INDEX IF NOT EXISTS kohala_memory_active_key
423
+ ON kohala_memory (agent, key) WHERE active
424
+ `);
425
+ return new _PostgresMemoryStore(pool, agent);
426
+ }
427
+ async put(key, body, category = DEFAULT_CATEGORY) {
428
+ const now = /* @__PURE__ */ new Date();
429
+ const existing = await this.pool.query(
430
+ "SELECT * FROM kohala_memory WHERE agent = $1 AND key = $2 AND active",
431
+ [this.agent, key]
432
+ );
433
+ if (existing.rows.length > 0) {
434
+ const row = existing.rows[0];
435
+ const updated = await this.pool.query(
436
+ `UPDATE kohala_memory SET category = $1, body = $2, size = $3, updated_at = $4
437
+ WHERE id = $5 RETURNING *`,
438
+ [category, body, body.byteLength, now, row.id]
439
+ );
440
+ return rowToRecord(updated.rows[0]);
441
+ }
442
+ const inserted = await this.pool.query(
443
+ `INSERT INTO kohala_memory (id, agent, key, category, body, size, created_at, updated_at, active)
444
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $7, TRUE) RETURNING *`,
445
+ [crypto2.randomUUID(), this.agent, key, category, body, body.byteLength, now]
446
+ );
447
+ return rowToRecord(inserted.rows[0]);
448
+ }
449
+ async get(keyOrId) {
450
+ const byKey = await this.pool.query(
451
+ "SELECT * FROM kohala_memory WHERE agent = $1 AND key = $2 AND active",
452
+ [this.agent, keyOrId]
453
+ );
454
+ let row = byKey.rows[0];
455
+ if (!row && isUuid(keyOrId)) {
456
+ const byId = await this.pool.query(
457
+ "SELECT * FROM kohala_memory WHERE agent = $1 AND id = $2 AND active",
458
+ [this.agent, keyOrId]
459
+ );
460
+ row = byId.rows[0];
461
+ }
462
+ if (!row) return null;
463
+ return { record: rowToRecord(row), body: row.body ?? Buffer.alloc(0) };
464
+ }
465
+ async list(prefix, limit = DEFAULT_LIST_LIMIT) {
466
+ const result = prefix ? await this.pool.query(
467
+ `SELECT id, agent, key, category, NULL AS body, size, created_at, updated_at, active
468
+ FROM kohala_memory WHERE agent = $1 AND active AND key LIKE $2 || '%'
469
+ ORDER BY updated_at DESC LIMIT $3`,
470
+ [this.agent, prefix, limit]
471
+ ) : await this.pool.query(
472
+ `SELECT id, agent, key, category, NULL AS body, size, created_at, updated_at, active
473
+ FROM kohala_memory WHERE agent = $1 AND active
474
+ ORDER BY updated_at DESC LIMIT $2`,
475
+ [this.agent, limit]
476
+ );
477
+ return result.rows.map((row) => rowToRecord(row));
478
+ }
479
+ async delete(keyOrId) {
480
+ const asset = await this.get(keyOrId);
481
+ if (!asset) return null;
482
+ const updated = await this.pool.query(
483
+ `UPDATE kohala_memory SET active = FALSE, body = NULL, updated_at = $1
484
+ WHERE id = $2 RETURNING *`,
485
+ [/* @__PURE__ */ new Date(), asset.record.id]
486
+ );
487
+ return rowToRecord(updated.rows[0]);
488
+ }
489
+ async close() {
490
+ await this.pool.end();
491
+ }
492
+ };
493
+ function isUuid(value) {
494
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
495
+ }
496
+
497
+ // src/memory/create.ts
498
+ async function createMemoryStore(options) {
499
+ if (options.backend === "file") {
500
+ return new FileMemoryStore(options.rootDir ?? process.cwd(), options.agent);
501
+ }
502
+ if (!options.url) {
503
+ throw new Error(
504
+ "The postgres backend needs a connection string. Pass --url or set DATABASE_URL."
505
+ );
506
+ }
507
+ return PostgresMemoryStore.connect(options.url, options.agent);
508
+ }
509
+
510
+ // src/emulator/runner.ts
511
+ import crypto3 from "crypto";
512
+ import path8 from "path";
513
+ import { execa as execa2, ExecaError } from "execa";
514
+
515
+ // src/trace/writer.ts
516
+ import fs5 from "fs";
517
+ import path6 from "path";
518
+ function traceFilePath(rootDir, agent) {
519
+ return path6.join(rootDir, ".kohala", "trace", `${agent}.jsonl`);
520
+ }
521
+ var TraceWriter = class {
522
+ filePath;
523
+ constructor(rootDir, agent) {
524
+ this.filePath = traceFilePath(rootDir, agent);
525
+ fs5.mkdirSync(path6.dirname(this.filePath), { recursive: true });
526
+ }
527
+ /** Append one event as a JSONL line. */
528
+ append(event) {
529
+ fs5.appendFileSync(this.filePath, `${JSON.stringify(event)}
530
+ `, "utf8");
531
+ }
532
+ /** The file this writer appends to (used by `kohala trace`). */
533
+ get file() {
534
+ return this.filePath;
535
+ }
536
+ };
537
+
538
+ // src/emulator/tokens.ts
539
+ import fs6 from "fs";
540
+ import path7 from "path";
541
+ function estimateTokens(text) {
542
+ return Math.ceil(text.length / 4);
543
+ }
544
+ function utcDay(now = /* @__PURE__ */ new Date()) {
545
+ return now.toISOString().slice(0, 10);
546
+ }
547
+ function usageFilePath(rootDir, agent) {
548
+ return path7.join(rootDir, ".kohala", "usage", `${agent}.json`);
549
+ }
550
+ function readUsage(rootDir, agent) {
551
+ const filePath = usageFilePath(rootDir, agent);
552
+ if (!fs6.existsSync(filePath)) return { days: {} };
553
+ return JSON.parse(fs6.readFileSync(filePath, "utf8"));
554
+ }
555
+ function writeUsage(rootDir, agent, usage) {
556
+ atomicWriteFile(usageFilePath(rootDir, agent), JSON.stringify(usage, null, 2));
557
+ }
558
+ var CapExceededError = class extends Error {
559
+ constructor(code, message) {
560
+ super(message);
561
+ this.code = code;
562
+ this.name = "CapExceededError";
563
+ }
564
+ code;
565
+ };
566
+ var TokenMeter = class {
567
+ constructor(rootDir, agent, caps) {
568
+ this.rootDir = rootDir;
569
+ this.agent = agent;
570
+ this.caps = caps;
571
+ }
572
+ rootDir;
573
+ agent;
574
+ caps;
575
+ /** Tokens consumed by this shift so far. */
576
+ runTotal = 0;
577
+ /** Set when a cap abort happened, so the runner can report the right status. */
578
+ abortedWith = null;
579
+ /** Tokens already consumed today (UTC), across all local runs. */
580
+ dayTotal() {
581
+ const usage = readUsage(this.rootDir, this.agent);
582
+ return usage.days[utcDay()] ?? 0;
583
+ }
584
+ /**
585
+ * Admission: refuse the shift before any work if today's local runs have
586
+ * already exhausted the per-day cap — the platform refuses the same way.
587
+ */
588
+ admitRun() {
589
+ const today = this.dayTotal();
590
+ if (today >= this.caps.perDayTokens) {
591
+ const error = new CapExceededError(
592
+ "PER_DAY_TOKEN_CAP",
593
+ `PER_DAY_TOKEN_CAP: today's usage (${today} tokens) has reached the per-day cap of ${this.caps.perDayTokens}. The cap resets at UTC midnight.`
594
+ );
595
+ this.abortedWith = error;
596
+ throw error;
597
+ }
598
+ }
599
+ /**
600
+ * Pre-turn projection: abort the run if `projectedTokens` more would cross
601
+ * the per-run cap. Called before every LLM turn (wrap-mode llm.complete
602
+ * calls and llm-mode loop turns alike).
603
+ */
604
+ admitLlmTurn(projectedTokens) {
605
+ if (this.runTotal + projectedTokens > this.caps.perRunTokens) {
606
+ const error = new CapExceededError(
607
+ "PER_RUN_TOKEN_CAP",
608
+ `PER_RUN_TOKEN_CAP: this turn is projected to use ~${projectedTokens} tokens, which would push the run total past the per-run cap of ${this.caps.perRunTokens} (used so far: ${this.runTotal}).`
609
+ );
610
+ this.abortedWith = error;
611
+ throw error;
612
+ }
613
+ }
614
+ /**
615
+ * Record actual consumption and persist it into today's ledger. Returns the
616
+ * new day total. The read-modify-write is guarded by an interprocess lock so
617
+ * overlapping runs can't lose each other's updates (which would undercount
618
+ * usage and let concurrent runs slip past the day cap).
619
+ */
620
+ add(tokens) {
621
+ this.runTotal += tokens;
622
+ return withFileLock(usageFilePath(this.rootDir, this.agent), () => {
623
+ const usage = readUsage(this.rootDir, this.agent);
624
+ const day = utcDay();
625
+ usage.days[day] = (usage.days[day] ?? 0) + tokens;
626
+ writeUsage(this.rootDir, this.agent, usage);
627
+ return usage.days[day];
628
+ });
629
+ }
630
+ };
631
+
632
+ // src/emulator/validators.ts
633
+ async function evaluateValidators(validators, output, store, now = /* @__PURE__ */ new Date()) {
634
+ const results = [];
635
+ for (const validator of validators) {
636
+ switch (validator.type) {
637
+ case "shape": {
638
+ const bytes = Buffer.byteLength(output, "utf8");
639
+ const passed = bytes >= validator.minBytes;
640
+ results.push({
641
+ validator: "shape",
642
+ passed,
643
+ detail: passed ? `output is ${bytes} bytes (>= ${validator.minBytes})` : `output is ${bytes} bytes, expected at least ${validator.minBytes}`
644
+ });
645
+ break;
646
+ }
647
+ case "freshness": {
648
+ const asset = await store.get(validator.asset);
649
+ if (!asset) {
650
+ results.push({
651
+ validator: "freshness",
652
+ passed: false,
653
+ detail: `memory asset "${validator.asset}" does not exist`
654
+ });
655
+ break;
656
+ }
657
+ const ageHours = (now.getTime() - new Date(asset.record.updatedAt).getTime()) / 36e5;
658
+ const passed = ageHours <= validator.maxAgeHours;
659
+ results.push({
660
+ validator: "freshness",
661
+ passed,
662
+ detail: passed ? `asset "${validator.asset}" is ${ageHours.toFixed(2)}h old (<= ${validator.maxAgeHours}h)` : `asset "${validator.asset}" is ${ageHours.toFixed(2)}h old, must be newer than ${validator.maxAgeHours}h`
663
+ });
664
+ break;
665
+ }
666
+ case "invariant": {
667
+ let regex;
668
+ try {
669
+ regex = new RegExp(validator.pattern);
670
+ } catch (error) {
671
+ results.push({
672
+ validator: "invariant",
673
+ passed: false,
674
+ detail: `invalid regex "${validator.pattern}": ${error.message}`
675
+ });
676
+ break;
677
+ }
678
+ const matched = regex.test(output);
679
+ const passed = validator.mustMatch ? matched : !matched;
680
+ results.push({
681
+ validator: "invariant",
682
+ passed,
683
+ detail: passed ? `output ${validator.mustMatch ? "matches" : "does not match"} /${validator.pattern}/ as required` : `output ${matched ? "matches" : "does not match"} /${validator.pattern}/ but ${validator.mustMatch ? "must match" : "must NOT match"}`
684
+ });
685
+ break;
686
+ }
687
+ }
688
+ }
689
+ return results;
690
+ }
691
+
692
+ // src/sdk/rpc.ts
693
+ import http from "http";
694
+
695
+ // src/sdk/dispatch.ts
696
+ import dns from "dns/promises";
697
+ import net from "net";
698
+
699
+ // src/emulator/allowlist.ts
700
+ var ToolDeniedError = class extends Error {
701
+ constructor(tool, allowlist) {
702
+ super(
703
+ `TOOL_DENIED: "${tool}" is not in the agent's toolAllowlist [${allowlist.join(", ")}]. Add it to kohala.json if the agent should be able to use it.`
704
+ );
705
+ this.tool = tool;
706
+ this.name = "ToolDeniedError";
707
+ }
708
+ tool;
709
+ };
710
+ function isToolAllowed(allowlist, tool) {
711
+ return allowlist.includes(tool);
712
+ }
713
+ function assertToolAllowed(allowlist, tool) {
714
+ if (!isToolAllowed(allowlist, tool)) {
715
+ throw new ToolDeniedError(tool, allowlist);
716
+ }
717
+ }
718
+
719
+ // src/emulator/llm-client.ts
720
+ var NoLlmKeyError = class extends Error {
721
+ constructor() {
722
+ super(
723
+ "NO_LLM_KEY: no LLM key configured. Set ANTHROPIC_API_KEY (preferred) or GEMINI_API_KEY in your environment \u2014 the devkit never mocks completions."
724
+ );
725
+ this.name = "NoLlmKeyError";
726
+ }
727
+ };
728
+ var DEFAULT_ANTHROPIC_MODEL = "claude-3-5-haiku-latest";
729
+ var DEFAULT_GEMINI_MODEL = "gemini-2.0-flash";
730
+ function detectLlmProvider(env = process.env) {
731
+ if (env.ANTHROPIC_API_KEY) return "anthropic";
732
+ if (env.GEMINI_API_KEY) return "gemini";
733
+ return null;
734
+ }
735
+ async function completeText(prompt, model, maxOutputTokens = 1024, env = process.env) {
736
+ const provider = detectLlmProvider(env);
737
+ if (provider === "anthropic") {
738
+ return completeAnthropic(prompt, model ?? DEFAULT_ANTHROPIC_MODEL, maxOutputTokens, env);
739
+ }
740
+ if (provider === "gemini") {
741
+ return completeGemini(prompt, model ?? DEFAULT_GEMINI_MODEL, maxOutputTokens, env);
742
+ }
743
+ throw new NoLlmKeyError();
744
+ }
745
+ async function completeAnthropic(prompt, model, maxOutputTokens, env) {
746
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
747
+ method: "POST",
748
+ headers: {
749
+ "content-type": "application/json",
750
+ "x-api-key": env.ANTHROPIC_API_KEY,
751
+ "anthropic-version": "2023-06-01"
752
+ },
753
+ body: JSON.stringify({
754
+ model,
755
+ max_tokens: maxOutputTokens,
756
+ messages: [{ role: "user", content: prompt }]
757
+ })
758
+ });
759
+ if (!response.ok) {
760
+ const body = await response.text();
761
+ throw new Error(`Anthropic API error ${response.status}: ${body.slice(0, 300)}`);
762
+ }
763
+ const data = await response.json();
764
+ const text = data.content.filter((block) => block.type === "text").map((block) => block.text ?? "").join("");
765
+ return {
766
+ text,
767
+ inputTokens: data.usage.input_tokens,
768
+ outputTokens: data.usage.output_tokens,
769
+ provider: "anthropic",
770
+ model
771
+ };
772
+ }
773
+ async function completeGemini(prompt, model, maxOutputTokens, env) {
774
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
775
+ const response = await fetch(url, {
776
+ method: "POST",
777
+ headers: {
778
+ "content-type": "application/json",
779
+ "x-goog-api-key": env.GEMINI_API_KEY
780
+ },
781
+ body: JSON.stringify({
782
+ contents: [{ parts: [{ text: prompt }] }],
783
+ generationConfig: { maxOutputTokens }
784
+ })
785
+ });
786
+ if (!response.ok) {
787
+ const body = await response.text();
788
+ throw new Error(`Gemini API error ${response.status}: ${body.slice(0, 300)}`);
789
+ }
790
+ const data = await response.json();
791
+ const text = (data.candidates?.[0]?.content?.parts ?? []).map((part) => part.text ?? "").join("");
792
+ return {
793
+ text,
794
+ inputTokens: data.usageMetadata?.promptTokenCount ?? 0,
795
+ outputTokens: data.usageMetadata?.candidatesTokenCount ?? 0,
796
+ provider: "gemini",
797
+ model
798
+ };
799
+ }
800
+
801
+ // src/sdk/dispatch.ts
802
+ var ToolCallError = class extends Error {
803
+ constructor(code, message) {
804
+ super(message);
805
+ this.code = code;
806
+ this.name = "ToolCallError";
807
+ }
808
+ code;
809
+ };
810
+ function summarizeArgs(args) {
811
+ const json = JSON.stringify(args) ?? "{}";
812
+ return json.length > 200 ? `${json.slice(0, 200)}\u2026` : json;
813
+ }
814
+ function isBlockedHostname(hostname) {
815
+ const lower = hostname.toLowerCase().replace(/^\[|\]$/g, "");
816
+ if (lower === "localhost" || lower.endsWith(".localhost")) return true;
817
+ if (net.isIP(lower)) return isBlockedIp(lower);
818
+ return false;
819
+ }
820
+ function isBlockedIp(ip) {
821
+ const version2 = net.isIP(ip);
822
+ if (version2 === 4) {
823
+ const octets = ip.split(".").map(Number);
824
+ const [a = 0, b = 0] = octets;
825
+ if (a === 0 || a === 10 || a === 127) return true;
826
+ if (a === 169 && b === 254) return true;
827
+ if (a === 172 && b >= 16 && b <= 31) return true;
828
+ if (a === 192 && b === 168) return true;
829
+ if (a === 100 && b >= 64 && b <= 127) return true;
830
+ return false;
831
+ }
832
+ if (version2 === 6) {
833
+ const lower = ip.toLowerCase();
834
+ if (lower === "::" || lower === "::1") return true;
835
+ if (lower.startsWith("fe8") || lower.startsWith("fe9") || lower.startsWith("fea") || lower.startsWith("feb"))
836
+ return true;
837
+ if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
838
+ if (lower.startsWith("::ffff:")) {
839
+ const mapped = lower.slice("::ffff:".length);
840
+ return net.isIP(mapped) === 4 ? isBlockedIp(mapped) : true;
841
+ }
842
+ return false;
843
+ }
844
+ return true;
845
+ }
846
+ async function assertPublicHost(hostname) {
847
+ if (isBlockedHostname(hostname)) {
848
+ throw new ToolCallError(
849
+ "BLOCKED_HOST",
850
+ `http.post_json refuses internal/private hosts ("${hostname}") \u2014 same guard the platform applies.`
851
+ );
852
+ }
853
+ const bare = hostname.replace(/^\[|\]$/g, "");
854
+ if (net.isIP(bare)) return;
855
+ let addresses;
856
+ try {
857
+ addresses = await dns.lookup(bare, { all: true, verbatim: true });
858
+ } catch {
859
+ throw new ToolCallError("BAD_URL", `http.post_json could not resolve host "${hostname}"`);
860
+ }
861
+ for (const { address } of addresses) {
862
+ if (isBlockedIp(address)) {
863
+ throw new ToolCallError(
864
+ "BLOCKED_HOST",
865
+ `http.post_json refuses "${hostname}" \u2014 it resolves to the internal/private address ${address}.`
866
+ );
867
+ }
868
+ }
869
+ }
870
+ var MAX_REDIRECTS = 3;
871
+ var ToolDispatcher = class _ToolDispatcher {
872
+ constructor(context) {
873
+ this.context = context;
874
+ }
875
+ context;
876
+ /** All tools the emulator knows how to execute. */
877
+ static KNOWN_TOOLS = [
878
+ "s3.put",
879
+ "s3.get",
880
+ "s3.list",
881
+ "s3.delete",
882
+ "http.post_json",
883
+ "llm.complete",
884
+ "notify.send",
885
+ "metrics.record"
886
+ ];
887
+ /**
888
+ * Execute `tool` with `args`. Always writes a tool_call trace event, whether
889
+ * the call was allowed, denied, or failed.
890
+ */
891
+ async call(tool, args) {
892
+ const { manifest, trace, runId } = this.context;
893
+ const startedAt = Date.now();
894
+ const base = {
895
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
896
+ runId,
897
+ agent: manifest.name,
898
+ type: "tool_call",
899
+ tool,
900
+ argsSummary: summarizeArgs(args)
901
+ };
902
+ try {
903
+ assertToolAllowed(manifest.toolAllowlist, tool);
904
+ } catch (error) {
905
+ trace.append({
906
+ ...base,
907
+ allowed: false,
908
+ ok: false,
909
+ durationMs: Date.now() - startedAt,
910
+ error: error.message
911
+ });
912
+ throw new ToolCallError("TOOL_DENIED", error.message);
913
+ }
914
+ try {
915
+ const result = await this.execute(tool, args);
916
+ trace.append({ ...base, allowed: true, ok: true, durationMs: Date.now() - startedAt });
917
+ return result;
918
+ } catch (error) {
919
+ const message = error.message;
920
+ trace.append({
921
+ ...base,
922
+ allowed: true,
923
+ ok: false,
924
+ durationMs: Date.now() - startedAt,
925
+ error: message
926
+ });
927
+ if (error instanceof ToolCallError) throw error;
928
+ if (error instanceof CapExceededError) throw new ToolCallError(error.code, message);
929
+ throw new ToolCallError("TOOL_ERROR", message);
930
+ }
931
+ }
932
+ async execute(tool, args) {
933
+ const { store, meter, trace, manifest, runId } = this.context;
934
+ switch (tool) {
935
+ case "s3.put": {
936
+ const key = requireString(args, "key");
937
+ const body = requireString(args, "body");
938
+ const category = optionalString(args, "category");
939
+ const record = await store.put(key, Buffer.from(body, "utf8"), category);
940
+ return { record };
941
+ }
942
+ case "s3.get": {
943
+ const keyOrId = requireString(args, "keyOrId");
944
+ const asset = await store.get(keyOrId);
945
+ if (!asset) {
946
+ throw new ToolCallError("NOT_FOUND", `No active memory asset matches "${keyOrId}"`);
947
+ }
948
+ return { record: asset.record, body: asset.body.toString("utf8") };
949
+ }
950
+ case "s3.list": {
951
+ const prefix = optionalString(args, "prefix");
952
+ const limit = typeof args.limit === "number" ? args.limit : void 0;
953
+ const records = await store.list(prefix, limit);
954
+ return { records };
955
+ }
956
+ case "s3.delete": {
957
+ const keyOrId = requireString(args, "keyOrId");
958
+ const record = await store.delete(keyOrId);
959
+ if (!record) {
960
+ throw new ToolCallError("NOT_FOUND", `No active memory asset matches "${keyOrId}"`);
961
+ }
962
+ return { record };
963
+ }
964
+ case "http.post_json": {
965
+ const url = requireString(args, "url");
966
+ const headers = args.headers ?? {};
967
+ const requestBody = JSON.stringify(args.body ?? {});
968
+ let currentUrl = url;
969
+ let response;
970
+ for (let hop = 0; ; hop += 1) {
971
+ const parsed = safeParseUrl(currentUrl);
972
+ if (!parsed || parsed.protocol !== "http:" && parsed.protocol !== "https:") {
973
+ throw new ToolCallError(
974
+ "BAD_URL",
975
+ `http.post_json needs an http(s) URL, got "${currentUrl}"`
976
+ );
977
+ }
978
+ await assertPublicHost(parsed.hostname);
979
+ response = await fetch(currentUrl, {
980
+ method: "POST",
981
+ headers: { "content-type": "application/json", ...headers },
982
+ body: requestBody,
983
+ redirect: "manual"
984
+ });
985
+ if (response.status < 300 || response.status >= 400) break;
986
+ const location = response.headers.get("location");
987
+ if (!location) break;
988
+ if (hop >= MAX_REDIRECTS) {
989
+ throw new ToolCallError(
990
+ "TOO_MANY_REDIRECTS",
991
+ `http.post_json gave up after ${MAX_REDIRECTS} redirects (last: ${currentUrl})`
992
+ );
993
+ }
994
+ currentUrl = new URL(location, currentUrl).toString();
995
+ }
996
+ const text = await response.text();
997
+ let json = null;
998
+ try {
999
+ json = JSON.parse(text);
1000
+ } catch {
1001
+ }
1002
+ return { status: response.status, ok: response.ok, body: text, json };
1003
+ }
1004
+ case "llm.complete": {
1005
+ const prompt = requireString(args, "prompt");
1006
+ const model = optionalString(args, "model");
1007
+ const maxOutput = 1024;
1008
+ meter.admitLlmTurn(estimateTokens(prompt) + maxOutput);
1009
+ const completion = await completeText(prompt, model, maxOutput);
1010
+ const consumed = completion.inputTokens + completion.outputTokens;
1011
+ const dayTotal = meter.add(consumed);
1012
+ trace.append({
1013
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1014
+ runId,
1015
+ agent: manifest.name,
1016
+ type: "tokens",
1017
+ tokens: consumed,
1018
+ runTotal: meter.runTotal,
1019
+ dayTotal,
1020
+ source: `llm.complete ${completion.provider}/${completion.model}`
1021
+ });
1022
+ return {
1023
+ text: completion.text,
1024
+ model: completion.model,
1025
+ provider: completion.provider,
1026
+ inputTokens: completion.inputTokens,
1027
+ outputTokens: completion.outputTokens
1028
+ };
1029
+ }
1030
+ case "notify.send": {
1031
+ requireString(args, "channel");
1032
+ requireString(args, "message");
1033
+ return { delivered: "trace" };
1034
+ }
1035
+ case "metrics.record": {
1036
+ requireString(args, "name");
1037
+ if (typeof args.value !== "number") {
1038
+ throw new ToolCallError("BAD_ARGS", "metrics.record needs a numeric value");
1039
+ }
1040
+ return { recorded: "trace" };
1041
+ }
1042
+ default:
1043
+ throw new ToolCallError(
1044
+ "UNKNOWN_TOOL",
1045
+ `Unknown tool "${tool}". Known tools: ${_ToolDispatcher.KNOWN_TOOLS.join(", ")}`
1046
+ );
1047
+ }
1048
+ }
1049
+ };
1050
+ function requireString(args, name) {
1051
+ const value = args[name];
1052
+ if (typeof value !== "string" || value.length === 0) {
1053
+ throw new ToolCallError("BAD_ARGS", `Missing required string argument "${name}"`);
1054
+ }
1055
+ return value;
1056
+ }
1057
+ function optionalString(args, name) {
1058
+ const value = args[name];
1059
+ if (value === void 0 || value === null) return void 0;
1060
+ if (typeof value !== "string") {
1061
+ throw new ToolCallError("BAD_ARGS", `Argument "${name}" must be a string when provided`);
1062
+ }
1063
+ return value;
1064
+ }
1065
+ function safeParseUrl(url) {
1066
+ try {
1067
+ return new URL(url);
1068
+ } catch {
1069
+ return null;
1070
+ }
1071
+ }
1072
+
1073
+ // src/sdk/rpc.ts
1074
+ async function startSdkRpcServer(context) {
1075
+ const dispatcher = new ToolDispatcher(context);
1076
+ const server = http.createServer((request, response) => {
1077
+ if (request.method !== "POST") {
1078
+ response.writeHead(405, { "content-type": "application/json" });
1079
+ response.end(JSON.stringify({ ok: false, error: { code: "METHOD_NOT_ALLOWED", message: "POST only" } }));
1080
+ return;
1081
+ }
1082
+ let raw = "";
1083
+ request.on("data", (chunk) => {
1084
+ raw += chunk;
1085
+ });
1086
+ request.on("end", () => {
1087
+ void (async () => {
1088
+ let payload;
1089
+ try {
1090
+ payload = JSON.parse(raw);
1091
+ } catch {
1092
+ respond(response, { ok: false, error: { code: "BAD_JSON", message: "Request body is not valid JSON" } });
1093
+ return;
1094
+ }
1095
+ if (typeof payload.tool !== "string") {
1096
+ respond(response, { ok: false, error: { code: "BAD_REQUEST", message: 'Missing "tool" field' } });
1097
+ return;
1098
+ }
1099
+ try {
1100
+ const result = await dispatcher.call(
1101
+ payload.tool,
1102
+ payload.args ?? {}
1103
+ );
1104
+ respond(response, { ok: true, result });
1105
+ } catch (error) {
1106
+ if (error instanceof ToolCallError) {
1107
+ respond(response, { ok: false, error: { code: error.code, message: error.message } });
1108
+ return;
1109
+ }
1110
+ respond(response, {
1111
+ ok: false,
1112
+ error: { code: "INTERNAL", message: error.message }
1113
+ });
1114
+ }
1115
+ })();
1116
+ });
1117
+ });
1118
+ await new Promise((resolve) => {
1119
+ server.listen(0, "127.0.0.1", resolve);
1120
+ });
1121
+ const address = server.address();
1122
+ if (address === null || typeof address === "string") {
1123
+ throw new Error("Could not determine RPC server port");
1124
+ }
1125
+ return {
1126
+ url: `http://127.0.0.1:${address.port}`,
1127
+ close: () => new Promise((resolve, reject) => {
1128
+ server.close((error) => error ? reject(error) : resolve());
1129
+ })
1130
+ };
1131
+ }
1132
+ function respond(response, body) {
1133
+ const json = JSON.stringify(body);
1134
+ response.writeHead(200, { "content-type": "application/json" });
1135
+ response.end(json);
1136
+ }
1137
+
1138
+ // src/emulator/llm-mode.ts
1139
+ import fs7 from "fs";
1140
+ var MAX_TURNS = 16;
1141
+ var MAX_OUTPUT_TOKENS = 1024;
1142
+ var TOOL_DEFINITIONS = {
1143
+ "s3.put": {
1144
+ description: 'Store a value in agent memory under a logical key. Category defaults to "agentoutput".',
1145
+ input_schema: {
1146
+ type: "object",
1147
+ properties: {
1148
+ key: { type: "string" },
1149
+ body: { type: "string" },
1150
+ category: { type: "string" }
1151
+ },
1152
+ required: ["key", "body"]
1153
+ }
1154
+ },
1155
+ "s3.get": {
1156
+ description: "Fetch a memory asset by logical key (or record id).",
1157
+ input_schema: {
1158
+ type: "object",
1159
+ properties: { keyOrId: { type: "string" } },
1160
+ required: ["keyOrId"]
1161
+ }
1162
+ },
1163
+ "s3.list": {
1164
+ description: "List active memory assets, optionally filtered by key prefix.",
1165
+ input_schema: {
1166
+ type: "object",
1167
+ properties: { prefix: { type: "string" }, limit: { type: "number" } }
1168
+ }
1169
+ },
1170
+ "s3.delete": {
1171
+ description: "Remove and deactivate a memory asset by key or id.",
1172
+ input_schema: {
1173
+ type: "object",
1174
+ properties: { keyOrId: { type: "string" } },
1175
+ required: ["keyOrId"]
1176
+ }
1177
+ },
1178
+ "http.post_json": {
1179
+ description: "POST a JSON body to an external http(s) URL and return the response.",
1180
+ input_schema: {
1181
+ type: "object",
1182
+ properties: {
1183
+ url: { type: "string" },
1184
+ body: { type: "object" },
1185
+ headers: { type: "object" }
1186
+ },
1187
+ required: ["url"]
1188
+ }
1189
+ },
1190
+ "notify.send": {
1191
+ description: "Send a notification (locally this is recorded in the audit trace).",
1192
+ input_schema: {
1193
+ type: "object",
1194
+ properties: { channel: { type: "string" }, message: { type: "string" } },
1195
+ required: ["channel", "message"]
1196
+ }
1197
+ },
1198
+ "metrics.record": {
1199
+ description: "Record a metric point (locally this is recorded in the audit trace).",
1200
+ input_schema: {
1201
+ type: "object",
1202
+ properties: {
1203
+ name: { type: "string" },
1204
+ value: { type: "number" },
1205
+ tags: { type: "object" }
1206
+ },
1207
+ required: ["name", "value"]
1208
+ }
1209
+ }
1210
+ };
1211
+ async function runLlmShift(context, skillName, scriptPath, repairFeedback) {
1212
+ const apiKey = process.env.ANTHROPIC_API_KEY;
1213
+ if (!apiKey) {
1214
+ throw new NoLlmKeyError();
1215
+ }
1216
+ const model = process.env.KOHALA_LLM_MODEL ?? "claude-3-5-haiku-latest";
1217
+ const dispatcher = new ToolDispatcher(context);
1218
+ const { manifest, meter, trace, runId } = context;
1219
+ const tools = Object.entries(TOOL_DEFINITIONS).filter(([name]) => manifest.toolAllowlist.includes(name)).map(([name, definition]) => ({ name: toApiToolName(name), ...definition }));
1220
+ const taskContext = fs7.existsSync(scriptPath) ? fs7.readFileSync(scriptPath, "utf8") : "";
1221
+ const messages = [
1222
+ {
1223
+ role: "user",
1224
+ content: `You are running one shift of the agent skill "${skillName}".
1225
+ ` + (taskContext ? `Task instructions:
1226
+ ${taskContext}
1227
+ ` : "") + (repairFeedback ? `
1228
+ This is a repair attempt. Your previous output failed validation:
1229
+ ${repairFeedback}
1230
+ Fix the problem this time.
1231
+ ` : "") + "Use your tools as needed, then reply with a final text summary of what you did."
1232
+ }
1233
+ ];
1234
+ for (let turn = 0; turn < MAX_TURNS; turn += 1) {
1235
+ const projected = estimateTokens(JSON.stringify(messages) + manifest.charter) + MAX_OUTPUT_TOKENS;
1236
+ meter.admitLlmTurn(projected);
1237
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
1238
+ method: "POST",
1239
+ headers: {
1240
+ "content-type": "application/json",
1241
+ "x-api-key": apiKey,
1242
+ "anthropic-version": "2023-06-01"
1243
+ },
1244
+ body: JSON.stringify({
1245
+ model,
1246
+ max_tokens: MAX_OUTPUT_TOKENS,
1247
+ system: manifest.charter,
1248
+ tools,
1249
+ messages
1250
+ })
1251
+ });
1252
+ if (!response.ok) {
1253
+ const body = await response.text();
1254
+ throw new Error(`Anthropic API error ${response.status}: ${body.slice(0, 300)}`);
1255
+ }
1256
+ const data = await response.json();
1257
+ const consumed = data.usage.input_tokens + data.usage.output_tokens;
1258
+ const dayTotal = meter.add(consumed);
1259
+ trace.append({
1260
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1261
+ runId,
1262
+ agent: manifest.name,
1263
+ type: "tokens",
1264
+ tokens: consumed,
1265
+ runTotal: meter.runTotal,
1266
+ dayTotal,
1267
+ source: `llm-mode turn ${turn + 1} (${model})`
1268
+ });
1269
+ const finalText = data.content.filter((block) => block.type === "text").map((block) => block.text ?? "").join("");
1270
+ if (data.stop_reason !== "tool_use") {
1271
+ return finalText;
1272
+ }
1273
+ messages.push({ role: "assistant", content: data.content });
1274
+ const toolResults = [];
1275
+ for (const block of data.content) {
1276
+ if (block.type !== "tool_use" || !block.id || !block.name) continue;
1277
+ const toolName = fromApiToolName(block.name);
1278
+ try {
1279
+ const result = await dispatcher.call(toolName, block.input ?? {});
1280
+ toolResults.push({
1281
+ type: "tool_result",
1282
+ tool_use_id: block.id,
1283
+ content: JSON.stringify(result).slice(0, 8e3)
1284
+ });
1285
+ } catch (error) {
1286
+ const toolError = error;
1287
+ toolResults.push({
1288
+ type: "tool_result",
1289
+ tool_use_id: block.id,
1290
+ content: `${toolError.code ?? "TOOL_ERROR"}: ${toolError.message}`,
1291
+ is_error: true
1292
+ });
1293
+ }
1294
+ }
1295
+ messages.push({ role: "user", content: toolResults });
1296
+ }
1297
+ throw new Error(`llm-mode shift exceeded ${MAX_TURNS} turns without finishing`);
1298
+ }
1299
+ function toApiToolName(name) {
1300
+ return name.replace(/\./g, "__");
1301
+ }
1302
+ function fromApiToolName(name) {
1303
+ return name.replace(/__/g, ".");
1304
+ }
1305
+
1306
+ // src/emulator/python.ts
1307
+ import { execa } from "execa";
1308
+ var cachedPython = null;
1309
+ async function findPython() {
1310
+ if (cachedPython) return cachedPython;
1311
+ for (const candidate of ["python3", "python"]) {
1312
+ try {
1313
+ const result = await execa(candidate, ["--version"], { reject: false });
1314
+ if (result.exitCode === 0 && /Python 3/.test(`${result.stdout}${result.stderr}`)) {
1315
+ cachedPython = candidate;
1316
+ return candidate;
1317
+ }
1318
+ } catch {
1319
+ }
1320
+ }
1321
+ throw new Error(
1322
+ "Python 3 is required to run agent skills but was not found on your PATH. Install it from https://www.python.org/downloads/ (or via your package manager: `brew install python3`, `apt install python3`), then re-run this command."
1323
+ );
1324
+ }
1325
+
1326
+ // src/emulator/runner.ts
1327
+ var MAX_REPAIR_ATTEMPTS = 2;
1328
+ function resolveSkill(manifest, requested) {
1329
+ const entries = Object.entries(manifest.skills);
1330
+ if (entries.length === 0) {
1331
+ throw new Error(`Agent "${manifest.name}" has no skills in kohala.json`);
1332
+ }
1333
+ if (requested) {
1334
+ const script = manifest.skills[requested];
1335
+ if (!script) {
1336
+ throw new Error(
1337
+ `Skill "${requested}" not found. Available skills: ${entries.map(([name]) => name).join(", ")}`
1338
+ );
1339
+ }
1340
+ return [requested, script];
1341
+ }
1342
+ if (entries.length === 1) {
1343
+ return entries[0];
1344
+ }
1345
+ throw new Error(
1346
+ `Agent "${manifest.name}" has ${entries.length} skills \u2014 pick one with --skill <name> (${entries.map(([name]) => name).join(", ")})`
1347
+ );
1348
+ }
1349
+ async function runShift(options) {
1350
+ const { rootDir, agentDir, manifest, store } = options;
1351
+ const runId = `run_${Date.now().toString(36)}_${crypto3.randomBytes(3).toString("hex")}`;
1352
+ const trace = new TraceWriter(rootDir, manifest.name);
1353
+ const meter = new TokenMeter(rootDir, manifest.name, manifest.caps);
1354
+ const startedAt = Date.now();
1355
+ const [skillName, scriptFilename] = resolveSkill(manifest, options.skill);
1356
+ const finish = (status, output, validatorResults, detail) => {
1357
+ trace.append({
1358
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1359
+ runId,
1360
+ agent: manifest.name,
1361
+ type: "run_finished",
1362
+ status,
1363
+ totalTokens: meter.runTotal,
1364
+ durationMs: Date.now() - startedAt,
1365
+ ...detail ? { detail } : {}
1366
+ });
1367
+ return { runId, status, output, totalTokens: meter.runTotal, validatorResults, detail };
1368
+ };
1369
+ try {
1370
+ meter.admitRun();
1371
+ } catch (error) {
1372
+ if (error instanceof CapExceededError) {
1373
+ return finish("aborted_per_day_token_cap", "", [], error.message);
1374
+ }
1375
+ throw error;
1376
+ }
1377
+ trace.append({
1378
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1379
+ runId,
1380
+ agent: manifest.name,
1381
+ type: "run_started",
1382
+ runtimeMode: manifest.runtimeMode,
1383
+ skill: skillName,
1384
+ scriptFilename
1385
+ });
1386
+ const context = { manifest, store, trace, meter, runId };
1387
+ if (manifest.runtimeMode === "llm") {
1388
+ try {
1389
+ let repairReason = "";
1390
+ for (let attempt = 0; attempt <= MAX_REPAIR_ATTEMPTS; attempt += 1) {
1391
+ if (attempt > 0) {
1392
+ trace.append({
1393
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1394
+ runId,
1395
+ agent: manifest.name,
1396
+ type: "repair_attempt",
1397
+ attempt,
1398
+ maxAttempts: MAX_REPAIR_ATTEMPTS,
1399
+ reason: repairReason
1400
+ });
1401
+ }
1402
+ const output = await runLlmShift(
1403
+ context,
1404
+ skillName,
1405
+ path8.join(agentDir, "skills", scriptFilename),
1406
+ attempt > 0 ? repairReason : void 0
1407
+ );
1408
+ const validatorResults = await evaluateValidators(manifest.validators, output, store);
1409
+ recordValidatorResults(trace, runId, manifest.name, validatorResults);
1410
+ const failures = validatorResults.filter((result) => !result.passed);
1411
+ if (failures.length === 0) {
1412
+ return finish("succeeded", output, validatorResults);
1413
+ }
1414
+ repairReason = failures.map((failure) => `${failure.validator}: ${failure.detail}`).join("; ");
1415
+ if (attempt === MAX_REPAIR_ATTEMPTS) {
1416
+ return finish(
1417
+ "failed",
1418
+ output,
1419
+ validatorResults,
1420
+ `validators failed after ${MAX_REPAIR_ATTEMPTS} repair attempts: ${repairReason}`
1421
+ );
1422
+ }
1423
+ }
1424
+ throw new Error("repair loop exited without a result");
1425
+ } catch (error) {
1426
+ if (error instanceof CapExceededError || meter.abortedWith) {
1427
+ const cap = error instanceof CapExceededError ? error : meter.abortedWith;
1428
+ const status = cap.code === "PER_RUN_TOKEN_CAP" ? "aborted_per_run_token_cap" : "aborted_per_day_token_cap";
1429
+ return finish(status, "", [], cap.message);
1430
+ }
1431
+ return finish("error", "", [], error.message);
1432
+ }
1433
+ }
1434
+ const rpc = await startSdkRpcServer(context);
1435
+ try {
1436
+ let repairReason = "";
1437
+ for (let attempt = 0; attempt <= MAX_REPAIR_ATTEMPTS; attempt += 1) {
1438
+ if (attempt > 0) {
1439
+ trace.append({
1440
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1441
+ runId,
1442
+ agent: manifest.name,
1443
+ type: "repair_attempt",
1444
+ attempt,
1445
+ maxAttempts: MAX_REPAIR_ATTEMPTS,
1446
+ reason: repairReason
1447
+ });
1448
+ }
1449
+ const execution = await executeScript({
1450
+ agentDir,
1451
+ scriptFilename,
1452
+ rpcUrl: rpc.url,
1453
+ manifest,
1454
+ runId,
1455
+ repairAttempt: attempt,
1456
+ repairFeedback: repairReason
1457
+ });
1458
+ if (!execution.ok) {
1459
+ if (meter.abortedWith?.code === "PER_RUN_TOKEN_CAP") {
1460
+ return finish("aborted_per_run_token_cap", execution.stdout, [], meter.abortedWith.message);
1461
+ }
1462
+ return finish("error", execution.stdout, [], execution.errorDetail);
1463
+ }
1464
+ const validatorResults = await evaluateValidators(
1465
+ manifest.validators,
1466
+ execution.stdout,
1467
+ store
1468
+ );
1469
+ recordValidatorResults(trace, runId, manifest.name, validatorResults);
1470
+ const failures = validatorResults.filter((result) => !result.passed);
1471
+ if (failures.length === 0) {
1472
+ return finish("succeeded", execution.stdout, validatorResults);
1473
+ }
1474
+ repairReason = failures.map((failure) => `${failure.validator}: ${failure.detail}`).join("; ");
1475
+ if (attempt === MAX_REPAIR_ATTEMPTS) {
1476
+ return finish("failed", execution.stdout, validatorResults, `validators failed after ${MAX_REPAIR_ATTEMPTS} repair attempts: ${repairReason}`);
1477
+ }
1478
+ }
1479
+ throw new Error("repair loop exited without a result");
1480
+ } finally {
1481
+ await rpc.close();
1482
+ }
1483
+ }
1484
+ function recordValidatorResults(trace, runId, agent, results) {
1485
+ for (const result of results) {
1486
+ trace.append({
1487
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1488
+ runId,
1489
+ agent,
1490
+ type: "validator_result",
1491
+ validator: result.validator,
1492
+ passed: result.passed,
1493
+ detail: result.detail
1494
+ });
1495
+ }
1496
+ }
1497
+ async function executeScript(options) {
1498
+ const python = await findPython();
1499
+ const scriptPath = path8.join(options.agentDir, "skills", options.scriptFilename);
1500
+ try {
1501
+ const result = await execa2(python, [scriptPath], {
1502
+ cwd: options.agentDir,
1503
+ env: {
1504
+ KOHALA_RPC_URL: options.rpcUrl,
1505
+ KOHALA_AGENT: options.manifest.name,
1506
+ KOHALA_RUN_ID: options.runId,
1507
+ KOHALA_REPAIR_ATTEMPT: String(options.repairAttempt),
1508
+ KOHALA_VALIDATOR_FEEDBACK: options.repairFeedback
1509
+ },
1510
+ // Surface the script's stderr live so developers see their own logging.
1511
+ stderr: "inherit",
1512
+ reject: false,
1513
+ timeout: 10 * 60 * 1e3
1514
+ });
1515
+ if (result.exitCode !== 0) {
1516
+ return {
1517
+ ok: false,
1518
+ stdout: result.stdout ?? "",
1519
+ errorDetail: `script ${options.scriptFilename} exited with code ${result.exitCode}`
1520
+ };
1521
+ }
1522
+ return { ok: true, stdout: result.stdout ?? "" };
1523
+ } catch (error) {
1524
+ const message = error instanceof ExecaError ? error.message : error.message;
1525
+ return { ok: false, stdout: "", errorDetail: message };
1526
+ }
1527
+ }
1528
+
1529
+ // src/cli/run.ts
1530
+ function registerRunCommand(program2) {
1531
+ program2.command("run").argument("<agent>", "agent directory (containing kohala.json)").option("--local", "run against the local emulator (required for now)").option("--skill <name>", "which skill to run (defaults to the only skill)").option(
1532
+ "--backend <backend>",
1533
+ "memory backend for this run: file | postgres",
1534
+ "file"
1535
+ ).option("--url <url>", "postgres connection string (or set DATABASE_URL)").description("Run a shift against the local emulator").action(
1536
+ async (agent, options) => {
1537
+ if (!options.local) {
1538
+ throw new Error(
1539
+ "Hosted runs start from the platform, not the CLI. Use --local to run against the local emulator, or `kohala deploy --run` to trigger a hosted run."
1540
+ );
1541
+ }
1542
+ if (options.backend !== "file" && options.backend !== "postgres") {
1543
+ throw new Error(`Unknown backend "${options.backend}" \u2014 use "file" or "postgres".`);
1544
+ }
1545
+ const rootDir = process.cwd();
1546
+ const agentDir = path9.resolve(rootDir, agent);
1547
+ const manifest = loadManifest(agentDir);
1548
+ const store = await createMemoryStore({
1549
+ backend: options.backend,
1550
+ agent: manifest.name,
1551
+ rootDir,
1552
+ url: options.url ?? process.env.DATABASE_URL
1553
+ });
1554
+ try {
1555
+ console.log(
1556
+ pc3.cyan(`Running shift for "${manifest.name}" (${manifest.runtimeMode} mode)...`)
1557
+ );
1558
+ const result = await runShift({ rootDir, agentDir, manifest, store, skill: options.skill });
1559
+ console.log("");
1560
+ if (result.status === "succeeded") {
1561
+ console.log(pc3.green(`\u2714 Shift ${result.runId} succeeded`));
1562
+ } else {
1563
+ console.log(pc3.red(`\u2718 Shift ${result.runId} ${result.status.replace(/_/g, " ")}`));
1564
+ if (result.detail) console.log(pc3.red(` ${result.detail}`));
1565
+ }
1566
+ console.log(pc3.dim(` tokens used: ${result.totalTokens} (counted, never billed)`));
1567
+ for (const validator of result.validatorResults) {
1568
+ const mark = validator.passed ? pc3.green("passed") : pc3.red("failed");
1569
+ console.log(pc3.dim(` validator ${validator.validator}: `) + mark + pc3.dim(` \u2014 ${validator.detail}`));
1570
+ }
1571
+ if (result.output.trim() !== "") {
1572
+ console.log("");
1573
+ console.log(pc3.dim("\u2500\u2500 output \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1574
+ console.log(result.output.trim());
1575
+ }
1576
+ console.log("");
1577
+ console.log(pc3.dim(`Full audit trail: kohala trace ${agent}`));
1578
+ if (result.status !== "succeeded") {
1579
+ process.exitCode = 1;
1580
+ }
1581
+ } finally {
1582
+ await store.close();
1583
+ }
1584
+ }
1585
+ );
1586
+ }
1587
+
1588
+ // src/cli/trace.ts
1589
+ import fs9 from "fs";
1590
+ import path10 from "path";
1591
+ import pc5 from "picocolors";
1592
+
1593
+ // src/trace/reader.ts
1594
+ import fs8 from "fs";
1595
+ function readTraceFile(filePath) {
1596
+ const raw = fs8.readFileSync(filePath, "utf8");
1597
+ return parseTraceLines(raw);
1598
+ }
1599
+ function parseTraceLines(raw) {
1600
+ const events = [];
1601
+ const lines = raw.split("\n");
1602
+ for (let i = 0; i < lines.length; i += 1) {
1603
+ const line = (lines[i] ?? "").trim();
1604
+ if (line === "") continue;
1605
+ try {
1606
+ events.push(JSON.parse(line));
1607
+ } catch {
1608
+ throw new Error(`Malformed trace line ${i + 1}: ${line.slice(0, 120)}`);
1609
+ }
1610
+ }
1611
+ return events;
1612
+ }
1613
+ function followTraceFile(filePath, onEvent, pollMs = 250) {
1614
+ let offset = fs8.existsSync(filePath) ? fs8.statSync(filePath).size : 0;
1615
+ let partial = "";
1616
+ const timer = setInterval(() => {
1617
+ if (!fs8.existsSync(filePath)) return;
1618
+ const size = fs8.statSync(filePath).size;
1619
+ if (size <= offset) return;
1620
+ const stream = fs8.createReadStream(filePath, { start: offset, end: size - 1, encoding: "utf8" });
1621
+ let chunkData = "";
1622
+ stream.on("data", (chunk) => {
1623
+ chunkData += chunk;
1624
+ });
1625
+ stream.on("end", () => {
1626
+ offset = size;
1627
+ partial += chunkData;
1628
+ const lines = partial.split("\n");
1629
+ partial = lines.pop() ?? "";
1630
+ for (const line of lines) {
1631
+ const trimmed = line.trim();
1632
+ if (trimmed === "") continue;
1633
+ try {
1634
+ onEvent(JSON.parse(trimmed));
1635
+ } catch {
1636
+ throw new Error(`Malformed trace line while following: ${trimmed.slice(0, 120)}`);
1637
+ }
1638
+ }
1639
+ });
1640
+ }, pollMs);
1641
+ return () => clearInterval(timer);
1642
+ }
1643
+
1644
+ // src/trace/format.ts
1645
+ import pc4 from "picocolors";
1646
+ function shortTime(ts) {
1647
+ const date = new Date(ts);
1648
+ return date.toLocaleTimeString("en-US", { hour12: false });
1649
+ }
1650
+ function formatTraceEvent(event) {
1651
+ const time = pc4.dim(shortTime(event.ts));
1652
+ switch (event.type) {
1653
+ case "run_started":
1654
+ return `${time} ${pc4.cyan("run_started")} skill=${event.skill} (${event.scriptFilename}) mode=${event.runtimeMode} run=${event.runId}`;
1655
+ case "tool_call": {
1656
+ const verdict = event.allowed ? event.ok ? pc4.green("ok") : pc4.red("error") : pc4.red("DENIED");
1657
+ const errorSuffix = event.error ? ` ${pc4.red(event.error)}` : "";
1658
+ return `${time} ${pc4.magenta("tool_call")} ${pc4.bold(event.tool)} ${verdict} ${pc4.dim(`${event.durationMs}ms`)} ${pc4.dim(event.argsSummary)}${errorSuffix}`;
1659
+ }
1660
+ case "tokens":
1661
+ return `${time} ${pc4.dim(`tokens +${event.tokens} run=${event.runTotal} day=${event.dayTotal} (${event.source})`)}`;
1662
+ case "validator_result": {
1663
+ const verdict = event.passed ? pc4.green("passed") : pc4.red("FAILED");
1664
+ return `${time} ${pc4.yellow("validator")} ${event.validator} ${verdict} ${pc4.dim(event.detail)}`;
1665
+ }
1666
+ case "repair_attempt":
1667
+ return `${time} ${pc4.yellow(`repair_attempt ${event.attempt}/${event.maxAttempts}`)} ${pc4.dim(event.reason)}`;
1668
+ case "run_finished": {
1669
+ const color = event.status === "succeeded" ? pc4.green : event.status === "failed" ? pc4.red : pc4.red;
1670
+ const detailSuffix = event.detail ? ` ${pc4.dim(event.detail)}` : "";
1671
+ return `${time} ${color(`run_finished ${event.status}`)} tokens=${event.totalTokens} ${pc4.dim(`${event.durationMs}ms`)}${detailSuffix}`;
1672
+ }
1673
+ }
1674
+ }
1675
+
1676
+ // src/cli/trace.ts
1677
+ function registerTraceCommand(program2) {
1678
+ program2.command("trace").argument("<agent>", "agent directory or agent name").option("--follow", "keep watching for new events (like tail -f)").option("--json", "print raw JSONL events instead of the pretty view").description("Tail the audit trace for an agent").action((agent, options) => {
1679
+ const rootDir = process.cwd();
1680
+ let agentName = agent;
1681
+ const agentDir = path10.resolve(rootDir, agent);
1682
+ if (fs9.existsSync(path10.join(agentDir, "kohala.json"))) {
1683
+ agentName = loadManifest(agentDir).name;
1684
+ }
1685
+ const filePath = traceFilePath(rootDir, agentName);
1686
+ if (!fs9.existsSync(filePath)) {
1687
+ throw new Error(
1688
+ `No trace found for "${agentName}" (looked at ${filePath}). Run the agent first: kohala run ${agent} --local`
1689
+ );
1690
+ }
1691
+ const print = options.json ? (event) => console.log(JSON.stringify(event)) : (event) => console.log(formatTraceEvent(event));
1692
+ for (const event of readTraceFile(filePath)) {
1693
+ print(event);
1694
+ }
1695
+ if (options.follow) {
1696
+ console.error(pc5.dim(`\u2014 following ${filePath} (ctrl-c to stop) \u2014`));
1697
+ followTraceFile(filePath, print);
1698
+ }
1699
+ });
1700
+ }
1701
+
1702
+ // src/cli/memory-serve.ts
1703
+ import fs10 from "fs";
1704
+ import path11 from "path";
1705
+ import pc6 from "picocolors";
1706
+
1707
+ // src/mcp/server.ts
1708
+ import http2 from "http";
1709
+ import { z as z2 } from "zod";
1710
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1711
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1712
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1713
+ var SERVER_INFO = { name: "kohala-memory", version: "0.1.0" };
1714
+ function buildMemoryMcpServer(store, agent) {
1715
+ const server = new McpServer(SERVER_INFO);
1716
+ server.registerTool(
1717
+ "s3.put",
1718
+ {
1719
+ title: "Store memory asset",
1720
+ description: `Store a value in ${agent}'s persistent memory under a logical key. Category defaults to "${DEFAULT_CATEGORY}" (run results).`,
1721
+ inputSchema: {
1722
+ key: z2.string().min(1).describe("Logical key, e.g. weather/latest"),
1723
+ body: z2.string().describe("UTF-8 body to store"),
1724
+ category: z2.string().optional().describe(`Asset category (default "${DEFAULT_CATEGORY}")`)
1725
+ }
1726
+ },
1727
+ async ({ key, body, category }) => {
1728
+ const record = await store.put(key, Buffer.from(body, "utf8"), category);
1729
+ return { content: [{ type: "text", text: JSON.stringify({ record }, null, 2) }] };
1730
+ }
1731
+ );
1732
+ server.registerTool(
1733
+ "s3.get",
1734
+ {
1735
+ title: "Fetch memory asset",
1736
+ description: "Fetch an asset by logical key (resolved first) or record id.",
1737
+ inputSchema: {
1738
+ keyOrId: z2.string().min(1).describe("Logical key or record id")
1739
+ }
1740
+ },
1741
+ async ({ keyOrId }) => {
1742
+ const asset = await store.get(keyOrId);
1743
+ if (!asset) {
1744
+ return {
1745
+ isError: true,
1746
+ content: [{ type: "text", text: `No active memory asset matches "${keyOrId}"` }]
1747
+ };
1748
+ }
1749
+ return {
1750
+ content: [
1751
+ {
1752
+ type: "text",
1753
+ text: JSON.stringify(
1754
+ { record: asset.record, body: asset.body.toString("utf8") },
1755
+ null,
1756
+ 2
1757
+ )
1758
+ }
1759
+ ]
1760
+ };
1761
+ }
1762
+ );
1763
+ server.registerTool(
1764
+ "s3.list",
1765
+ {
1766
+ title: "List memory assets",
1767
+ description: "List active assets for the agent, newest first, optionally by key prefix.",
1768
+ inputSchema: {
1769
+ prefix: z2.string().optional().describe("Only keys starting with this prefix"),
1770
+ limit: z2.number().int().positive().optional().describe("Max records (default 100)")
1771
+ }
1772
+ },
1773
+ async ({ prefix, limit }) => {
1774
+ const records = await store.list(prefix, limit);
1775
+ return { content: [{ type: "text", text: JSON.stringify({ records }, null, 2) }] };
1776
+ }
1777
+ );
1778
+ server.registerTool(
1779
+ "s3.delete",
1780
+ {
1781
+ title: "Delete memory asset",
1782
+ description: "Remove an asset's body and deactivate its index entry (soft delete).",
1783
+ inputSchema: {
1784
+ keyOrId: z2.string().min(1).describe("Logical key or record id")
1785
+ }
1786
+ },
1787
+ async ({ keyOrId }) => {
1788
+ const record = await store.delete(keyOrId);
1789
+ if (!record) {
1790
+ return {
1791
+ isError: true,
1792
+ content: [{ type: "text", text: `No active memory asset matches "${keyOrId}"` }]
1793
+ };
1794
+ }
1795
+ return { content: [{ type: "text", text: JSON.stringify({ record }, null, 2) }] };
1796
+ }
1797
+ );
1798
+ server.registerResource(
1799
+ "memory-index",
1800
+ "memory://index",
1801
+ {
1802
+ title: `Memory index for ${agent}`,
1803
+ description: "All active memory assets for this agent (key, category, timestamps).",
1804
+ mimeType: "application/json"
1805
+ },
1806
+ async (uri) => {
1807
+ const records = await store.list(void 0, 1e3);
1808
+ return {
1809
+ contents: [
1810
+ { uri: uri.href, mimeType: "application/json", text: JSON.stringify({ agent, records }, null, 2) }
1811
+ ]
1812
+ };
1813
+ }
1814
+ );
1815
+ return server;
1816
+ }
1817
+ async function serveStdio(store, agent) {
1818
+ const server = buildMemoryMcpServer(store, agent);
1819
+ const transport = new StdioServerTransport();
1820
+ await server.connect(transport);
1821
+ }
1822
+ async function serveHttp(store, agent, port) {
1823
+ const httpServer = http2.createServer((request, response) => {
1824
+ void (async () => {
1825
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
1826
+ if (url.pathname !== "/mcp") {
1827
+ response.writeHead(404, { "content-type": "application/json" });
1828
+ response.end(JSON.stringify({ error: "Not found. The MCP endpoint is POST /mcp" }));
1829
+ return;
1830
+ }
1831
+ if (request.method !== "POST") {
1832
+ response.writeHead(405, { "content-type": "application/json" });
1833
+ response.end(
1834
+ JSON.stringify({ error: "Stateless MCP server: only POST /mcp is supported" })
1835
+ );
1836
+ return;
1837
+ }
1838
+ let raw = "";
1839
+ request.on("data", (chunk) => {
1840
+ raw += chunk;
1841
+ });
1842
+ request.on("end", () => {
1843
+ void (async () => {
1844
+ let body;
1845
+ try {
1846
+ body = JSON.parse(raw);
1847
+ } catch {
1848
+ response.writeHead(400, { "content-type": "application/json" });
1849
+ response.end(JSON.stringify({ error: "Request body is not valid JSON" }));
1850
+ return;
1851
+ }
1852
+ const server = buildMemoryMcpServer(store, agent);
1853
+ const transport = new StreamableHTTPServerTransport({
1854
+ sessionIdGenerator: void 0
1855
+ // stateless
1856
+ });
1857
+ response.on("close", () => {
1858
+ void transport.close();
1859
+ void server.close();
1860
+ });
1861
+ await server.connect(transport);
1862
+ await transport.handleRequest(request, response, body);
1863
+ })();
1864
+ });
1865
+ })();
1866
+ });
1867
+ await new Promise((resolve) => {
1868
+ httpServer.listen(port, "127.0.0.1", resolve);
1869
+ });
1870
+ return httpServer;
1871
+ }
1872
+
1873
+ // src/cli/memory-serve.ts
1874
+ function registerMemoryCommand(program2) {
1875
+ const memory = program2.command("memory").description("Memory server commands");
1876
+ memory.command("serve").option("--agent <name>", "agent name to scope memory to (or run from an agent dir)").option("--backend <backend>", "file | postgres", "file").option("--url <url>", "postgres connection string (or set DATABASE_URL)").option("--http", "serve over streamable HTTP instead of stdio").option("--port <port>", "HTTP port (with --http)", "8787").description("Serve agent memory over the Model Context Protocol").action(
1877
+ async (options) => {
1878
+ if (options.backend !== "file" && options.backend !== "postgres") {
1879
+ throw new Error(`Unknown backend "${options.backend}" \u2014 use "file" or "postgres".`);
1880
+ }
1881
+ let agent = options.agent;
1882
+ let rootDir = process.cwd();
1883
+ if (!agent) {
1884
+ const manifestHere = path11.join(process.cwd(), "kohala.json");
1885
+ if (fs10.existsSync(manifestHere)) {
1886
+ agent = loadManifest(process.cwd()).name;
1887
+ rootDir = path11.dirname(process.cwd());
1888
+ } else {
1889
+ throw new Error(
1890
+ "Pass --agent <name>, or run this from inside an agent directory (one containing kohala.json)."
1891
+ );
1892
+ }
1893
+ }
1894
+ const store = await createMemoryStore({
1895
+ backend: options.backend,
1896
+ agent,
1897
+ rootDir,
1898
+ url: options.url ?? process.env.DATABASE_URL
1899
+ });
1900
+ if (options.http) {
1901
+ const port = Number(options.port);
1902
+ await serveHttp(store, agent, port);
1903
+ console.error(
1904
+ pc6.green(`MCP memory server for "${agent}" listening on http://127.0.0.1:${port}/mcp`)
1905
+ );
1906
+ console.error(pc6.dim(`backend=${options.backend} \u2014 ctrl-c to stop`));
1907
+ } else {
1908
+ console.error(
1909
+ pc6.dim(`MCP memory server for "${agent}" on stdio (backend=${options.backend})`)
1910
+ );
1911
+ await serveStdio(store, agent);
1912
+ }
1913
+ }
1914
+ );
1915
+ }
1916
+
1917
+ // src/cli/login.ts
1918
+ import readline from "readline";
1919
+ import pc7 from "picocolors";
1920
+
1921
+ // src/deploy/credentials.ts
1922
+ import fs11 from "fs";
1923
+ import os from "os";
1924
+ import path12 from "path";
1925
+ function credentialsPath() {
1926
+ return path12.join(os.homedir(), ".kohala", "credentials.json");
1927
+ }
1928
+ function saveApiKey(apiKey) {
1929
+ if (!apiKey.startsWith("pk_")) {
1930
+ throw new Error('Kohala API keys start with "pk_". Check the key and try again.');
1931
+ }
1932
+ const filePath = credentialsPath();
1933
+ fs11.mkdirSync(path12.dirname(filePath), { recursive: true });
1934
+ const payload = { apiKey };
1935
+ fs11.writeFileSync(filePath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 384 });
1936
+ fs11.chmodSync(filePath, 384);
1937
+ return filePath;
1938
+ }
1939
+ function resolveApiKey(env = process.env) {
1940
+ if (env.KOHALA_API_KEY) return env.KOHALA_API_KEY;
1941
+ const filePath = credentialsPath();
1942
+ if (!fs11.existsSync(filePath)) return null;
1943
+ try {
1944
+ const parsed = JSON.parse(fs11.readFileSync(filePath, "utf8"));
1945
+ return parsed.apiKey ?? null;
1946
+ } catch {
1947
+ throw new Error(
1948
+ `${filePath} is corrupted \u2014 delete it and run "kohala login" again.`
1949
+ );
1950
+ }
1951
+ }
1952
+
1953
+ // src/cli/login.ts
1954
+ function registerLoginCommand(program2) {
1955
+ program2.command("login").option("--api-key <key>", "provide the key non-interactively").description("Save your Kohala API key for `kohala deploy`").action(async (options) => {
1956
+ let apiKey = options.apiKey;
1957
+ if (!apiKey) {
1958
+ apiKey = await promptHidden(
1959
+ "Paste your Kohala API key (starts with pk_, from kohala.ai account settings): "
1960
+ );
1961
+ }
1962
+ if (!apiKey) {
1963
+ throw new Error("No API key provided.");
1964
+ }
1965
+ const savedTo = saveApiKey(apiKey.trim());
1966
+ console.log(pc7.green(`API key saved to ${savedTo} (permissions 600).`));
1967
+ console.log(pc7.dim("Note: the KOHALA_API_KEY environment variable overrides this file."));
1968
+ });
1969
+ }
1970
+ function promptHidden(question) {
1971
+ return new Promise((resolve) => {
1972
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
1973
+ const stdin = process.stdin;
1974
+ process.stderr.write(question);
1975
+ if (stdin.isTTY && stdin.setRawMode) {
1976
+ stdin.setRawMode(true);
1977
+ let value = "";
1978
+ const onData = (chunk) => {
1979
+ const char = chunk.toString("utf8");
1980
+ if (char === "\n" || char === "\r" || char === "") {
1981
+ stdin.setRawMode?.(false);
1982
+ stdin.off("data", onData);
1983
+ rl.close();
1984
+ process.stderr.write("\n");
1985
+ resolve(value);
1986
+ } else if (char === "") {
1987
+ stdin.setRawMode?.(false);
1988
+ rl.close();
1989
+ process.exit(130);
1990
+ } else if (char === "\x7F") {
1991
+ value = value.slice(0, -1);
1992
+ } else {
1993
+ value += char;
1994
+ }
1995
+ };
1996
+ stdin.on("data", onData);
1997
+ } else {
1998
+ rl.question("", (answer) => {
1999
+ rl.close();
2000
+ resolve(answer);
2001
+ });
2002
+ }
2003
+ });
2004
+ }
2005
+
2006
+ // src/cli/deploy.ts
2007
+ import path14 from "path";
2008
+ import pc8 from "picocolors";
2009
+
2010
+ // src/deploy/client.ts
2011
+ import fs12 from "fs";
2012
+ import path13 from "path";
2013
+ var DEFAULT_BASE_URL = "https://kohala.ai";
2014
+ function buildDeployPlan(manifest, agentDir) {
2015
+ const skills = Object.entries(manifest.skills).map(([name, scriptFilename]) => {
2016
+ const scriptPath = path13.join(agentDir, "skills", scriptFilename);
2017
+ if (!fs12.existsSync(scriptPath)) {
2018
+ throw new Error(
2019
+ `Skill "${name}" points at ${scriptFilename}, but ${scriptPath} does not exist.`
2020
+ );
2021
+ }
2022
+ return {
2023
+ name,
2024
+ scriptFilename,
2025
+ description: `Skill "${name}" of agent "${manifest.name}"`,
2026
+ scriptContent: fs12.readFileSync(scriptPath, "utf8")
2027
+ };
2028
+ });
2029
+ return {
2030
+ agent: {
2031
+ name: manifest.name,
2032
+ charter: manifest.charter,
2033
+ toolAllowlist: manifest.toolAllowlist,
2034
+ runtimeMode: manifest.runtimeMode,
2035
+ ...manifest.schedule ? { schedule: manifest.schedule } : {}
2036
+ },
2037
+ skills,
2038
+ quota: {
2039
+ perRunTokens: manifest.caps.perRunTokens,
2040
+ perDayTokens: manifest.caps.perDayTokens,
2041
+ ...manifest.caps.billingTokens !== void 0 ? { billingTokens: manifest.caps.billingTokens } : {},
2042
+ ...manifest.caps.billingPeriod !== void 0 ? { billingPeriod: manifest.caps.billingPeriod } : {}
2043
+ }
2044
+ };
2045
+ }
2046
+ var DeployError = class extends Error {
2047
+ constructor(status, message) {
2048
+ super(message);
2049
+ this.status = status;
2050
+ this.name = "DeployError";
2051
+ }
2052
+ status;
2053
+ };
2054
+ var KohalaClient = class {
2055
+ constructor(apiKey, baseUrl = DEFAULT_BASE_URL) {
2056
+ this.apiKey = apiKey;
2057
+ this.baseUrl = baseUrl;
2058
+ }
2059
+ apiKey;
2060
+ baseUrl;
2061
+ async request(method, apiPath, body) {
2062
+ const response = await fetch(`${this.baseUrl}${apiPath}`, {
2063
+ method,
2064
+ headers: {
2065
+ "content-type": "application/json",
2066
+ authorization: `Bearer ${this.apiKey}`
2067
+ },
2068
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
2069
+ });
2070
+ if (response.status === 401) {
2071
+ throw new DeployError(
2072
+ 401,
2073
+ "401 Unauthorized: your API key was rejected. Check KOHALA_API_KEY or re-run `kohala login` with a fresh pk_ key from your Kohala account settings."
2074
+ );
2075
+ }
2076
+ if (response.status === 403) {
2077
+ throw new DeployError(
2078
+ 403,
2079
+ "403 Forbidden: your plan does not allow this operation. Check your plan at https://kohala.ai or contact support."
2080
+ );
2081
+ }
2082
+ if (!response.ok) {
2083
+ const text2 = await response.text();
2084
+ throw new DeployError(
2085
+ response.status,
2086
+ `Kohala API error ${response.status} on ${method} ${apiPath}: ${text2.slice(0, 500)}`
2087
+ );
2088
+ }
2089
+ const text = await response.text();
2090
+ return text ? JSON.parse(text) : null;
2091
+ }
2092
+ /** Step 1: create or update the agent (idempotent on name). */
2093
+ async upsertAgent(payload) {
2094
+ const data = await this.request("POST", "/api/v1/agents", payload);
2095
+ const id = data.id ?? data.agentId;
2096
+ if (!id) {
2097
+ throw new DeployError(500, "Platform response did not include an agent id");
2098
+ }
2099
+ return { id, created: data.created ?? false };
2100
+ }
2101
+ /** Step 2: upload one skill (script content inline). */
2102
+ async upsertSkill(agentId, payload) {
2103
+ await this.request("POST", `/api/v1/agents/${agentId}/skills`, payload);
2104
+ }
2105
+ /** Step 3: set the token caps. */
2106
+ async setQuota(agentId, payload) {
2107
+ await this.request("PUT", `/api/v1/agents/${agentId}/quota`, payload);
2108
+ }
2109
+ /** Optional step 4 (--run): trigger a manual run and return its link. */
2110
+ async triggerManualRun(agentId) {
2111
+ const data = await this.request("POST", `/api/v1/agents/${agentId}/agent-runs/manual`);
2112
+ return data.runUrl ?? data.url ?? `${this.baseUrl}/agents/${agentId}/runs/${data.id ?? ""}`;
2113
+ }
2114
+ };
2115
+
2116
+ // src/cli/deploy.ts
2117
+ function registerDeployCommand(program2) {
2118
+ program2.command("deploy").argument("<agent>", "agent directory (containing kohala.json)").option("--dry-run", "print the payloads without sending anything").option("--base-url <url>", "API base URL", DEFAULT_BASE_URL).option("--run", "trigger a manual run after deploying").description("Deploy an agent to kohala.ai (idempotent on agent name)").action(
2119
+ async (agent, options) => {
2120
+ const agentDir = path14.resolve(process.cwd(), agent);
2121
+ const manifest = loadManifest(agentDir);
2122
+ const plan = buildDeployPlan(manifest, agentDir);
2123
+ if (options.dryRun) {
2124
+ console.log(pc8.cyan("Dry run \u2014 nothing will be sent. Deploy plan:"));
2125
+ console.log("");
2126
+ console.log(pc8.bold("1. POST /api/v1/agents (idempotent on name)"));
2127
+ console.log(JSON.stringify(plan.agent, null, 2));
2128
+ for (const skill of plan.skills) {
2129
+ console.log("");
2130
+ console.log(pc8.bold(`2. POST /api/v1/agents/:id/skills \u2014 "${skill.name}"`));
2131
+ console.log(
2132
+ JSON.stringify(
2133
+ { ...skill, scriptContent: `<${Buffer.byteLength(skill.scriptContent)} bytes of ${skill.scriptFilename}>` },
2134
+ null,
2135
+ 2
2136
+ )
2137
+ );
2138
+ }
2139
+ console.log("");
2140
+ console.log(pc8.bold("3. PUT /api/v1/agents/:id/quota"));
2141
+ console.log(JSON.stringify(plan.quota, null, 2));
2142
+ if (options.run) {
2143
+ console.log("");
2144
+ console.log(pc8.bold("4. POST /api/v1/agents/:id/agent-runs/manual"));
2145
+ }
2146
+ return;
2147
+ }
2148
+ const apiKey = resolveApiKey();
2149
+ if (!apiKey) {
2150
+ throw new Error(
2151
+ "No API key configured. Run `kohala login` (or set KOHALA_API_KEY) first. Keys come from your kohala.ai account settings."
2152
+ );
2153
+ }
2154
+ const client = new KohalaClient(apiKey, options.baseUrl);
2155
+ console.log(pc8.cyan(`Deploying "${manifest.name}" to ${options.baseUrl} ...`));
2156
+ const upserted = await client.upsertAgent(plan.agent);
2157
+ console.log(
2158
+ pc8.green(` \u2714 agent ${upserted.created ? "created" : "updated"} (id ${upserted.id})`)
2159
+ );
2160
+ for (const skill of plan.skills) {
2161
+ await client.upsertSkill(upserted.id, skill);
2162
+ console.log(pc8.green(` \u2714 skill "${skill.name}" uploaded (${skill.scriptFilename})`));
2163
+ }
2164
+ await client.setQuota(upserted.id, plan.quota);
2165
+ console.log(
2166
+ pc8.green(
2167
+ ` \u2714 quota set (perRun=${plan.quota.perRunTokens}, perDay=${plan.quota.perDayTokens})`
2168
+ )
2169
+ );
2170
+ if (options.run) {
2171
+ const runUrl = await client.triggerManualRun(upserted.id);
2172
+ console.log(pc8.green(` \u2714 manual run triggered: ${runUrl}`));
2173
+ }
2174
+ console.log("");
2175
+ console.log(pc8.green(`Deployed. Deploys are additive \u2014 nothing was deleted remotely.`));
2176
+ }
2177
+ );
2178
+ }
2179
+
2180
+ // src/cli/doctor.ts
2181
+ import pc9 from "picocolors";
2182
+ function registerDoctorCommand(program2) {
2183
+ program2.command("doctor").description("Check your environment: Node, Python, LLM keys, API key, DATABASE_URL").action(async () => {
2184
+ const ok = (label, detail) => console.log(`${pc9.green("\u2714")} ${label} ${pc9.dim(detail)}`);
2185
+ const warn = (label, detail) => console.log(`${pc9.yellow("\u2022")} ${label} ${pc9.dim(detail)}`);
2186
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
2187
+ if (nodeMajor >= 20) {
2188
+ ok(`node ${process.versions.node}`, "(>= 20 required)");
2189
+ } else {
2190
+ console.log(
2191
+ `${pc9.red("\u2718")} node ${process.versions.node} \u2014 the devkit requires Node 20 or newer.`
2192
+ );
2193
+ }
2194
+ try {
2195
+ const python = await findPython();
2196
+ ok(`python (${python})`, "wrap-mode skills can run");
2197
+ } catch {
2198
+ warn("python 3 not found", "wrap-mode `kohala run --local` will not work until installed");
2199
+ }
2200
+ const provider = detectLlmProvider();
2201
+ if (provider) {
2202
+ ok(`LLM key (${provider})`, "llm.complete and llm mode available");
2203
+ } else {
2204
+ warn(
2205
+ "no ANTHROPIC_API_KEY / GEMINI_API_KEY",
2206
+ "llm.complete and llm-mode runs will fail with NO_LLM_KEY"
2207
+ );
2208
+ }
2209
+ const apiKey = resolveApiKey();
2210
+ if (apiKey) {
2211
+ const source = process.env.KOHALA_API_KEY ? "KOHALA_API_KEY env" : credentialsPath();
2212
+ ok("Kohala API key", `from ${source} \u2014 kohala deploy ready`);
2213
+ } else {
2214
+ warn("no Kohala API key", "run `kohala login` before `kohala deploy` (local dev needs no account)");
2215
+ }
2216
+ if (process.env.DATABASE_URL) {
2217
+ ok("DATABASE_URL set", "postgres memory backend available");
2218
+ } else {
2219
+ warn("DATABASE_URL not set", "memory uses the file backend (that is the default anyway)");
2220
+ }
2221
+ console.log("");
2222
+ console.log(pc9.dim("Everything local works without an account \u2014 deploy is the only step that needs one."));
2223
+ });
2224
+ }
2225
+
2226
+ // src/cli/index.ts
2227
+ var require2 = createRequire(import.meta.url);
2228
+ var { version } = require2("../../package.json");
2229
+ var program = new Command();
2230
+ program.name("kohala").description(
2231
+ "Kohala Devkit \u2014 build and run agents entirely on your own machine, then push the same agent to Kohala when you want it hosted."
2232
+ ).version(version);
2233
+ registerInitCommand(program);
2234
+ registerValidateCommand(program);
2235
+ registerRunCommand(program);
2236
+ registerTraceCommand(program);
2237
+ registerMemoryCommand(program);
2238
+ registerLoginCommand(program);
2239
+ registerDeployCommand(program);
2240
+ registerDoctorCommand(program);
2241
+ program.parseAsync(process.argv).catch((error) => {
2242
+ console.error(pc10.red(`
2243
+ ${error.message}`));
2244
+ process.exitCode = 1;
2245
+ });
2246
+ //# sourceMappingURL=index.js.map