@kohala/devkit 0.1.6 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @kohala/devkit
2
2
 
3
+ ## 0.2.0
4
+
5
+ - **TypeScript/JavaScript agents can now be shipped with the CLI.** `kohala
6
+ validate` and `kohala deploy` discover `.ts` / `.js` skill scripts
7
+ alongside `.py` (same extension table the platform uses) and report the
8
+ detected runtime per skill. A Node-lane skill's upload now carries
9
+ `runtimeLanguage` and the manifest's new `dependencies` (npm packages) as
10
+ `scriptDependencies`, so the platform runs its acceptance checks — compile,
11
+ security scan, npm allowlist — when the script is attached instead of
12
+ letting it fail on its first hosted run. A package outside the allowlist is
13
+ reported locally, with the platform's own message, before anything is sent
14
+ (`--allow-unknown-packages` defers to the server). A skill file with an
15
+ extension no runtime can execute is now a validation error for wrap-mode
16
+ agents. Python projects deploy byte-for-byte as before: their payload gains
17
+ no new fields.
18
+ - **Start a TypeScript agent with one command.** `kohala init my-agent
19
+ --language ts` creates a typed `main.ts`, a local `_tools.ts` adapter with
20
+ the same API as the hosted Node SDK, strict TypeScript configuration, and
21
+ declared npm packages. The generated project validates, runs locally through
22
+ the Node lane, and produces a complete dry-run deploy plan without edits.
23
+
24
+ ## 0.1.7
25
+
26
+ - **BUG-006 fixed: `kohala deploy --run` no longer misreports every 409 as
27
+ "agent not enabled".** The platform returns two distinct 409s for a manual
28
+ run: agent disabled, and `nothing_to_run` (no script bound to an active
29
+ cron schedule — the case a fresh schedule-less scaffold always hits, even
30
+ after enabling the agent). The CLI now tells them apart and gives the
31
+ right fix for each: enable the agent in the dashboard, or add a
32
+ `schedule` to kohala.json so deploy binds the scripts. Verified live that
33
+ enabling an agent survives redeploys (the upsert does not reset it).
34
+ DEPLOY.md documents both cases.
35
+
3
36
  ## 0.1.6
4
37
 
5
38
  - **BUG-077 fixed: a deployed agent with a manifest schedule can now actually
package/CONTRIBUTING.md CHANGED
@@ -24,6 +24,15 @@ npm test # vitest (unit + CLI e2e)
24
24
  npm run build # tsup -> dist/
25
25
  ```
26
26
 
27
+ ## Updating dependencies
28
+
29
+ `devkit/package-lock.json` is committed so CI and local development install the
30
+ same dependency tree. When changing dependencies, run `npm install` from the
31
+ `devkit` directory and commit both `package.json` and `package-lock.json`.
32
+ Use `npm ci` when you only need to install the committed dependency tree.
33
+ CI pins npm 11.6.0 to avoid an npm 10 peer-resolution crash; use the same npm
34
+ version when intentionally regenerating the lockfile.
35
+
27
36
  ## Ground rules
28
37
 
29
38
  - **Errors fail loudly.** No silent fallbacks, no mock LLM responses, no
package/README.md CHANGED
@@ -54,6 +54,11 @@ skill scripts).
54
54
  }
55
55
  ```
56
56
 
57
+ - Skill scripts can be Python (`main.py`) or TypeScript/JavaScript
58
+ (`main.ts`) — the extension picks the runtime, and a TypeScript agent
59
+ declares its npm packages with `"dependencies": ["zod"]` (allowlisted
60
+ packages only). `kohala run --local` executes Python skills; TypeScript
61
+ skills run hosted. See [MANIFEST.md](docs/MANIFEST.md#script-languages).
57
62
  - `runtimeMode: "wrap"` executes your script directly and validates its
58
63
  output (stdout). Use `llm.complete` from the script SDK to call an LLM
59
64
  mid-script (needs `ANTHROPIC_API_KEY` or `GEMINI_API_KEY`).
package/dist/cli/index.js CHANGED
@@ -13,6 +13,45 @@ import pc from "picocolors";
13
13
 
14
14
  // src/manifest/schema.ts
15
15
  import { z } from "zod";
16
+
17
+ // src/manifest/language.ts
18
+ var RUNTIME_LANGUAGES = ["python", "node"];
19
+ var ENTRYPOINT_EXTENSIONS = {
20
+ python: [".py"],
21
+ node: [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"]
22
+ };
23
+ var SCRIPT_FILE_EXTENSIONS = Object.values(ENTRYPOINT_EXTENSIONS).flat();
24
+ var LANGUAGE_LABEL = {
25
+ python: "Python",
26
+ node: "TypeScript/JavaScript"
27
+ };
28
+ function languageForEntrypointFile(filename) {
29
+ const lower = (filename || "").toLowerCase();
30
+ for (const language of RUNTIME_LANGUAGES) {
31
+ if (ENTRYPOINT_EXTENSIONS[language].some((ext) => lower.endsWith(ext))) {
32
+ return language;
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ function skillLanguages(skills) {
38
+ return Object.entries(skills).map(([name, scriptFilename]) => ({
39
+ name,
40
+ scriptFilename,
41
+ language: languageForEntrypointFile(scriptFilename) ?? "python"
42
+ }));
43
+ }
44
+ function summarizeLanguages(skills) {
45
+ const languages = new Set(skillLanguages(skills).map((skill) => skill.language));
46
+ if (languages.size === 0) return "none";
47
+ if (languages.size > 1) return "mixed";
48
+ return [...languages][0];
49
+ }
50
+ function hasNodeSkill(skills) {
51
+ return skillLanguages(skills).some((skill) => skill.language === "node");
52
+ }
53
+
54
+ // src/manifest/schema.ts
16
55
  var BILLING_PERIODS = ["day", "week", "month"];
17
56
  var shapeValidatorSchema = z.object({
18
57
  type: z.literal("shape"),
@@ -53,6 +92,8 @@ var nameSchema = z.string().min(1).max(64).regex(
53
92
  /^[a-z0-9][a-z0-9_-]*$/i,
54
93
  "must start with a letter or digit and contain only letters, digits, '-' and '_'"
55
94
  );
95
+ var skillScriptSchema = z.string().min(1);
96
+ var dependenciesSchema = z.array(z.string().min(1).max(214)).max(50).default([]);
56
97
  var manifestSchema = z.object({
57
98
  name: nameSchema,
58
99
  /** The agent's mission text (platform: agentCharter). */
@@ -67,13 +108,43 @@ var manifestSchema = z.object({
67
108
  * "llm" — run a real tool-use loop against the developer's own LLM key.
68
109
  */
69
110
  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({}),
111
+ /**
112
+ * Map of skill name -> script filename in `skills/`, e.g.
113
+ * {"collect": "main.py"} or {"collect": "main.ts"}. The file extension
114
+ * decides which runtime the platform executes the script in.
115
+ */
116
+ skills: z.record(z.string().min(1), skillScriptSchema).default({}),
117
+ /**
118
+ * npm packages the TypeScript/JavaScript skills import beyond Node's
119
+ * standard library. Only packages on the platform's allowlist are
120
+ * installed — `kohala validate` and `kohala deploy` refuse anything else
121
+ * offline, before the deploy request. Python skills never use this: pip
122
+ * packages are not installable from the CLI deploy path.
123
+ */
124
+ dependencies: dependenciesSchema,
72
125
  /** Cron expression. Used only on deploy; local runs are always manual. */
73
126
  schedule: z.string().min(1).optional(),
74
127
  caps: capsSchema,
75
128
  validators: z.array(validatorSchema).default([])
76
129
  }).strict();
130
+ function crossFieldProblems(manifest) {
131
+ const problems = [];
132
+ if (manifest.runtimeMode === "wrap") {
133
+ for (const [name, filename] of Object.entries(manifest.skills)) {
134
+ if (languageForEntrypointFile(filename) === null) {
135
+ problems.push(
136
+ `skills.${name}: "${filename}" must be a script the platform can run in wrap mode \u2014 one of: ${SCRIPT_FILE_EXTENSIONS.join(", ")}`
137
+ );
138
+ }
139
+ }
140
+ }
141
+ if (manifest.dependencies.length > 0 && !hasNodeSkill(manifest.skills)) {
142
+ problems.push(
143
+ `dependencies: declares npm package(s) (${manifest.dependencies.join(", ")}) but no skill is a TypeScript/JavaScript entrypoint \u2014 only those install npm packages (hint: use a ${ENTRYPOINT_EXTENSIONS.node.join(" / ")} skill script, or remove "dependencies")`
144
+ );
145
+ }
146
+ return problems;
147
+ }
77
148
 
78
149
  // src/cli/init.ts
79
150
  function templatesDir() {
@@ -83,9 +154,11 @@ function templatesDir() {
83
154
  path.resolve(here, "..", "..", "..", "templates")
84
155
  ];
85
156
  for (const candidate of candidates) {
86
- if (fs.existsSync(path.join(candidate, "kohala.json"))) return candidate;
157
+ if (fs.existsSync(path.join(candidate, "python", "kohala.json"))) return candidate;
87
158
  }
88
- throw new Error(`Could not locate the devkit templates directory (looked in: ${candidates.join(", ")})`);
159
+ throw new Error(
160
+ `Could not locate the devkit templates directory (looked in: ${candidates.join(", ")})`
161
+ );
89
162
  }
90
163
  function copyTemplates(sourceDir, targetDir, agentName) {
91
164
  fs.mkdirSync(targetDir, { recursive: true });
@@ -101,7 +174,7 @@ function copyTemplates(sourceDir, targetDir, agentName) {
101
174
  }
102
175
  }
103
176
  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) => {
177
+ program2.command("init").argument("<name>", "agent name (also the directory name)").option("--language <language>", "scaffold language: python or ts", "python").description("Scaffold a new agent: kohala.json, a working skill, and the local SDK").action((name, options) => {
105
178
  const nameCheck = manifestSchema.shape.name.safeParse(name);
106
179
  if (!nameCheck.success) {
107
180
  throw new Error(
@@ -112,7 +185,12 @@ function registerInitCommand(program2) {
112
185
  if (fs.existsSync(targetDir)) {
113
186
  throw new Error(`${targetDir} already exists \u2014 pick a new name or remove the directory.`);
114
187
  }
115
- copyTemplates(templatesDir(), targetDir, name);
188
+ const language = options.language.toLowerCase();
189
+ const template = language === "ts" || language === "typescript" ? "typescript" : language === "py" || language === "python" ? "python" : null;
190
+ if (!template) {
191
+ throw new Error(`Unsupported language "${options.language}". Choose "python" or "ts".`);
192
+ }
193
+ copyTemplates(path.join(templatesDir(), template), targetDir, name);
116
194
  console.log(pc.green(`Created agent "${name}" in ${targetDir}`));
117
195
  console.log("");
118
196
  console.log("Next steps:");
@@ -143,7 +221,8 @@ var FIELD_HINTS = {
143
221
  charter: "write the agent's mission as a non-empty string",
144
222
  toolAllowlist: 'list allowed tool names, e.g. ["s3.put", "http.post_json"]',
145
223
  runtimeMode: 'must be "wrap" (script wrapper) or "llm" (tool-use loop)',
146
- skills: 'map skill name to script filename, e.g. {"collect": "main.py"}',
224
+ skills: 'map skill name to script filename, e.g. {"collect": "main.py"} or {"collect": "main.ts"}',
225
+ dependencies: 'list npm packages the TypeScript/JavaScript skills import, e.g. ["zod"]',
147
226
  schedule: 'use a cron expression like "0 9 * * *" (only used on deploy)',
148
227
  caps: "set caps.perRunTokens and caps.perDayTokens as positive integers",
149
228
  validators: 'each validator needs a "type" of "shape", "freshness" or "invariant"'
@@ -187,6 +266,10 @@ function loadManifest(agentDir) {
187
266
  if (!result.success) {
188
267
  throw new ManifestError(`${filePath} failed validation`, formatManifestIssues(result.error));
189
268
  }
269
+ const crossField = crossFieldProblems(result.data);
270
+ if (crossField.length > 0) {
271
+ throw new ManifestError(`${filePath} failed validation`, crossField);
272
+ }
190
273
  return result.data;
191
274
  }
192
275
 
@@ -399,49 +482,112 @@ function unknownToolIds(tools) {
399
482
  return tools.filter((t) => !KNOWN_TOOL_IDS.has(t));
400
483
  }
401
484
 
485
+ // src/manifest/npm-packages.ts
486
+ var CLI_FRAMEWORK = "custom";
487
+ var ALLOWED_NPM_PACKAGES = [
488
+ "zod",
489
+ "openai",
490
+ "@anthropic-ai/sdk",
491
+ "ai",
492
+ "@ai-sdk/openai",
493
+ "@ai-sdk/anthropic",
494
+ "date-fns",
495
+ "cheerio",
496
+ "js-yaml"
497
+ ];
498
+ function normalizeNpmName(name) {
499
+ return name.trim().toLowerCase();
500
+ }
501
+ function rejectedNpmPackages(dependencies) {
502
+ const allowed = new Set(ALLOWED_NPM_PACKAGES.map(normalizeNpmName));
503
+ const seen = /* @__PURE__ */ new Set();
504
+ const rejected = [];
505
+ for (const dependency of dependencies) {
506
+ const normalized = normalizeNpmName(dependency);
507
+ if (!normalized || allowed.has(normalized) || seen.has(normalized)) continue;
508
+ seen.add(normalized);
509
+ rejected.push(dependency.trim());
510
+ }
511
+ return rejected;
512
+ }
513
+ function npmAllowlistMessage(rejected) {
514
+ return `unsupported npm dependencies for framework "${CLI_FRAMEWORK}": ${rejected.join(", ")}. Supported packages: ${ALLOWED_NPM_PACKAGES.map(normalizeNpmName).sort().join(", ")}`;
515
+ }
516
+
402
517
  // src/cli/validate.ts
403
518
  function registerValidateCommand(program2) {
404
519
  program2.command("validate").argument("<agent>", "agent directory (containing kohala.json)").option(
405
520
  "--allow-unknown-tools",
406
521
  "do not fail on tool ids missing from the bundled catalog snapshot"
407
- ).description("Validate an agent's kohala.json and print precise errors").action((agent, options) => {
408
- const agentDir = path3.resolve(process.cwd(), agent);
409
- try {
410
- const manifest = loadManifest(agentDir);
411
- const unknown = unknownToolIds(manifest.toolAllowlist);
412
- if (unknown.length > 0) {
413
- const msg = `toolAllowlist contains tool id(s) not in the Kohala tool catalog: ` + unknown.join(", ");
414
- if (options.allowUnknownTools) {
415
- console.warn(pc2.yellow(`warning: ${msg}`));
416
- } else {
417
- console.error(pc2.red(msg));
418
- console.error(
522
+ ).option(
523
+ "--allow-unknown-packages",
524
+ "do not fail on npm packages missing from the bundled allowlist snapshot"
525
+ ).description("Validate an agent's kohala.json and print precise errors").action(
526
+ (agent, options) => {
527
+ const agentDir = path3.resolve(process.cwd(), agent);
528
+ try {
529
+ const manifest = loadManifest(agentDir);
530
+ const unknown = unknownToolIds(manifest.toolAllowlist);
531
+ if (unknown.length > 0) {
532
+ const msg = `toolAllowlist contains tool id(s) not in the Kohala tool catalog: ` + unknown.join(", ");
533
+ if (options.allowUnknownTools) {
534
+ console.warn(pc2.yellow(`warning: ${msg}`));
535
+ } else {
536
+ console.error(pc2.red(msg));
537
+ console.error(
538
+ pc2.dim(
539
+ " If these are new platform tools the bundled snapshot doesn't know yet, re-run with --allow-unknown-tools (the deploy endpoint will still verify against the live catalog)."
540
+ )
541
+ );
542
+ process.exitCode = 1;
543
+ return;
544
+ }
545
+ }
546
+ const rejected = rejectedNpmPackages(manifest.dependencies);
547
+ if (rejected.length > 0) {
548
+ const msg = npmAllowlistMessage(rejected);
549
+ if (options.allowUnknownPackages) {
550
+ console.warn(pc2.yellow(`warning: ${msg}`));
551
+ } else {
552
+ console.error(pc2.red(msg));
553
+ console.error(
554
+ pc2.dim(
555
+ " The platform refuses these when the script is attached, so deploy would fail. Remove them, or re-run with --allow-unknown-packages if the bundled snapshot is stale (the deploy endpoint still enforces the live allowlist)."
556
+ )
557
+ );
558
+ process.exitCode = 1;
559
+ return;
560
+ }
561
+ }
562
+ console.log(pc2.green(`kohala.json for "${manifest.name}" is valid.`));
563
+ console.log(
564
+ pc2.dim(
565
+ ` runtimeMode=${manifest.runtimeMode} language=${summarizeLanguages(manifest.skills)} skills=${Object.keys(manifest.skills).length} validators=${manifest.validators.length} allowlist=[${manifest.toolAllowlist.join(", ")}]`
566
+ )
567
+ );
568
+ for (const skill of skillLanguages(manifest.skills)) {
569
+ console.log(
419
570
  pc2.dim(
420
- " If these are new platform tools the bundled snapshot doesn't know yet, re-run with --allow-unknown-tools (the deploy endpoint will still verify against the live catalog)."
571
+ ` skill "${skill.name}" \u2192 skills/${skill.scriptFilename} (${LANGUAGE_LABEL[skill.language]})`
421
572
  )
422
573
  );
574
+ }
575
+ if (manifest.dependencies.length > 0) {
576
+ console.log(pc2.dim(` npm packages: ${manifest.dependencies.join(", ")}`));
577
+ }
578
+ } catch (error) {
579
+ if (error instanceof ManifestError) {
580
+ console.error(pc2.red(error.message));
581
+ for (const problem of error.problems) {
582
+ console.error(pc2.red(` \u2022 ${problem}`));
583
+ }
423
584
  process.exitCode = 1;
424
585
  return;
425
586
  }
587
+ throw error;
426
588
  }
427
- console.log(pc2.green(`kohala.json for "${manifest.name}" is valid.`));
428
- console.log(
429
- pc2.dim(
430
- ` runtimeMode=${manifest.runtimeMode} skills=${Object.keys(manifest.skills).length} validators=${manifest.validators.length} allowlist=[${manifest.toolAllowlist.join(", ")}]`
431
- )
432
- );
433
- } catch (error) {
434
- if (error instanceof ManifestError) {
435
- console.error(pc2.red(error.message));
436
- for (const problem of error.problems) {
437
- console.error(pc2.red(` \u2022 ${problem}`));
438
- }
439
- process.exitCode = 1;
440
- return;
441
- }
442
- throw error;
443
589
  }
444
- });
590
+ );
445
591
  }
446
592
 
447
593
  // src/cli/run.ts
@@ -1556,6 +1702,7 @@ async function findPython() {
1556
1702
  }
1557
1703
 
1558
1704
  // src/emulator/runner.ts
1705
+ import { fileURLToPath as fileURLToPath2 } from "url";
1559
1706
  var MAX_REPAIR_ATTEMPTS = 2;
1560
1707
  function resolveSkill(manifest, requested) {
1561
1708
  const entries = Object.entries(manifest.skills);
@@ -1689,7 +1836,12 @@ async function runShift(options) {
1689
1836
  });
1690
1837
  if (!execution.ok) {
1691
1838
  if (meter.abortedWith?.code === "PER_RUN_TOKEN_CAP") {
1692
- return finish("aborted_per_run_token_cap", execution.stdout, [], meter.abortedWith.message);
1839
+ return finish(
1840
+ "aborted_per_run_token_cap",
1841
+ execution.stdout,
1842
+ [],
1843
+ meter.abortedWith.message
1844
+ );
1693
1845
  }
1694
1846
  return finish("error", execution.stdout, [], execution.errorDetail);
1695
1847
  }
@@ -1705,7 +1857,12 @@ async function runShift(options) {
1705
1857
  }
1706
1858
  repairReason = failures.map((failure) => `${failure.validator}: ${failure.detail}`).join("; ");
1707
1859
  if (attempt === MAX_REPAIR_ATTEMPTS) {
1708
- return finish("failed", execution.stdout, validatorResults, `validators failed after ${MAX_REPAIR_ATTEMPTS} repair attempts: ${repairReason}`);
1860
+ return finish(
1861
+ "failed",
1862
+ execution.stdout,
1863
+ validatorResults,
1864
+ `validators failed after ${MAX_REPAIR_ATTEMPTS} repair attempts: ${repairReason}`
1865
+ );
1709
1866
  }
1710
1867
  }
1711
1868
  throw new Error("repair loop exited without a result");
@@ -1727,10 +1884,29 @@ function recordValidatorResults(trace, runId, agent, results) {
1727
1884
  }
1728
1885
  }
1729
1886
  async function executeScript(options) {
1730
- const python = await findPython();
1731
1887
  const scriptPath = path8.join(options.agentDir, "skills", options.scriptFilename);
1888
+ const extension = path8.extname(options.scriptFilename).toLowerCase();
1889
+ let command;
1890
+ let args;
1891
+ if (extension === ".py") {
1892
+ command = await findPython();
1893
+ args = [scriptPath];
1894
+ } else if (extension === ".ts" || extension === ".mts" || extension === ".cts") {
1895
+ command = process.execPath;
1896
+ const tsxCli = fileURLToPath2(import.meta.resolve("tsx/cli"));
1897
+ args = [tsxCli, scriptPath];
1898
+ } else if (extension === ".js" || extension === ".mjs" || extension === ".cjs") {
1899
+ command = process.execPath;
1900
+ args = [scriptPath];
1901
+ } else {
1902
+ return {
1903
+ ok: false,
1904
+ stdout: "",
1905
+ errorDetail: `Unsupported skill script extension "${extension || "(none)"}". Use .py, .ts, .mts, .cts, .js, .mjs, or .cjs.`
1906
+ };
1907
+ }
1732
1908
  try {
1733
- const result = await execa2(python, [scriptPath], {
1909
+ const result = await execa2(command, args, {
1734
1910
  cwd: options.agentDir,
1735
1911
  env: {
1736
1912
  KOHALA_RPC_URL: options.rpcUrl,
@@ -1760,11 +1936,7 @@ async function executeScript(options) {
1760
1936
 
1761
1937
  // src/cli/run.ts
1762
1938
  function registerRunCommand(program2) {
1763
- 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(
1764
- "--backend <backend>",
1765
- "memory backend for this run: file | postgres",
1766
- "file"
1767
- ).option("--url <url>", "postgres connection string (or set DATABASE_URL)").description("Run a shift against the local emulator").action(
1939
+ 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("--backend <backend>", "memory backend for this run: file | postgres", "file").option("--url <url>", "postgres connection string (or set DATABASE_URL)").description("Run a shift against the local emulator").action(
1768
1940
  async (agent, options) => {
1769
1941
  if (!options.local) {
1770
1942
  throw new Error(
@@ -1787,7 +1959,13 @@ function registerRunCommand(program2) {
1787
1959
  console.log(
1788
1960
  pc3.cyan(`Running shift for "${manifest.name}" (${manifest.runtimeMode} mode)...`)
1789
1961
  );
1790
- const result = await runShift({ rootDir, agentDir, manifest, store, skill: options.skill });
1962
+ const result = await runShift({
1963
+ rootDir,
1964
+ agentDir,
1965
+ manifest,
1966
+ store,
1967
+ skill: options.skill
1968
+ });
1791
1969
  console.log("");
1792
1970
  if (result.status === "succeeded") {
1793
1971
  console.log(pc3.green(`\u2714 Shift ${result.runId} succeeded`));
@@ -1798,7 +1976,9 @@ function registerRunCommand(program2) {
1798
1976
  console.log(pc3.dim(` tokens used: ${result.totalTokens} (counted, never billed)`));
1799
1977
  for (const validator of result.validatorResults) {
1800
1978
  const mark = validator.passed ? pc3.green("passed") : pc3.red("failed");
1801
- console.log(pc3.dim(` validator ${validator.validator}: `) + mark + pc3.dim(` \u2014 ${validator.detail}`));
1979
+ console.log(
1980
+ pc3.dim(` validator ${validator.validator}: `) + mark + pc3.dim(` \u2014 ${validator.detail}`)
1981
+ );
1802
1982
  }
1803
1983
  if (result.output.trim() !== "") {
1804
1984
  console.log("");
@@ -2243,6 +2423,9 @@ import pc8 from "picocolors";
2243
2423
  import fs12 from "fs";
2244
2424
  import path13 from "path";
2245
2425
  var DEFAULT_BASE_URL = "https://kohala.ai";
2426
+ function skillPayloadLanguage(payload) {
2427
+ return payload.runtimeLanguage ?? "python";
2428
+ }
2246
2429
  function buildDeployPlan(manifest, agentDir) {
2247
2430
  const skills = Object.entries(manifest.skills).map(([name, scriptFilename]) => {
2248
2431
  const scriptPath = path13.join(agentDir, "skills", scriptFilename);
@@ -2251,11 +2434,16 @@ function buildDeployPlan(manifest, agentDir) {
2251
2434
  `Skill "${name}" points at ${scriptFilename}, but ${scriptPath} does not exist.`
2252
2435
  );
2253
2436
  }
2437
+ const language = languageForEntrypointFile(scriptFilename) ?? "python";
2254
2438
  return {
2255
2439
  name,
2256
2440
  scriptFilename,
2257
2441
  description: `Skill "${name}" of agent "${manifest.name}"`,
2258
- code: fs12.readFileSync(scriptPath, "utf8")
2442
+ code: fs12.readFileSync(scriptPath, "utf8"),
2443
+ ...language === "node" ? {
2444
+ runtimeLanguage: "node",
2445
+ ...manifest.dependencies.length > 0 ? { scriptDependencies: manifest.dependencies } : {}
2446
+ } : {}
2259
2447
  };
2260
2448
  });
2261
2449
  if (manifest.schedule && skills.length === 0) {
@@ -2280,6 +2468,17 @@ function buildDeployPlan(manifest, agentDir) {
2280
2468
  }
2281
2469
  };
2282
2470
  }
2471
+ function readableApiError(body) {
2472
+ const raw = (body ?? "").trim();
2473
+ try {
2474
+ const parsed = JSON.parse(raw);
2475
+ const parts = [parsed.error, parsed.message].filter((part) => typeof part === "string" && part.trim() !== "").map((part) => part.trim());
2476
+ const unique = parts.filter((part, index) => parts.indexOf(part) === index);
2477
+ if (unique.length > 0) return unique.join(" \u2014 ").slice(0, 1e3);
2478
+ } catch {
2479
+ }
2480
+ return raw.slice(0, 500);
2481
+ }
2283
2482
  var DeployError = class extends Error {
2284
2483
  constructor(status, message) {
2285
2484
  super(message);
@@ -2288,6 +2487,12 @@ var DeployError = class extends Error {
2288
2487
  }
2289
2488
  status;
2290
2489
  };
2490
+ function classifyManualRun409(error) {
2491
+ if (error.status !== 409) return "other";
2492
+ if (/nothing_to_run|would execute nothing/i.test(error.message)) return "nothing_to_run";
2493
+ if (/not enabled/i.test(error.message)) return "disabled";
2494
+ return "other";
2495
+ }
2291
2496
  var KohalaClient = class {
2292
2497
  constructor(apiKey, baseUrl = DEFAULT_BASE_URL) {
2293
2498
  this.apiKey = apiKey;
@@ -2320,7 +2525,7 @@ var KohalaClient = class {
2320
2525
  const text2 = await response.text();
2321
2526
  throw new DeployError(
2322
2527
  response.status,
2323
- `Kohala API error ${response.status} on ${method} ${apiPath}: ${text2.slice(0, 500)}`
2528
+ `Kohala API error ${response.status} on ${method} ${apiPath}: ${readableApiError(text2)}`
2324
2529
  );
2325
2530
  }
2326
2531
  const text = await response.text();
@@ -2387,10 +2592,25 @@ var KohalaClient = class {
2387
2592
 
2388
2593
  // src/cli/deploy.ts
2389
2594
  function registerDeployCommand(program2) {
2390
- 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(
2595
+ 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").option(
2596
+ "--allow-unknown-packages",
2597
+ "do not fail on npm packages missing from the bundled allowlist snapshot"
2598
+ ).description("Deploy an agent to kohala.ai (idempotent on agent name)").action(
2391
2599
  async (agent, options) => {
2392
2600
  const agentDir = path14.resolve(process.cwd(), agent);
2393
2601
  const manifest = loadManifest(agentDir);
2602
+ const rejected = rejectedNpmPackages(manifest.dependencies);
2603
+ if (rejected.length > 0) {
2604
+ const message = npmAllowlistMessage(rejected);
2605
+ if (options.allowUnknownPackages) {
2606
+ console.warn(pc8.yellow(`warning: ${message}`));
2607
+ } else {
2608
+ throw new Error(
2609
+ `${message}
2610
+ The platform refuses these when the script is attached, so this deploy would fail. Remove them, or re-run with --allow-unknown-packages if the bundled snapshot is stale (the deploy endpoint still enforces the live allowlist).`
2611
+ );
2612
+ }
2613
+ }
2394
2614
  const plan = buildDeployPlan(manifest, agentDir);
2395
2615
  if (options.dryRun) {
2396
2616
  console.log(pc8.cyan("Dry run \u2014 nothing will be sent. Deploy plan:"));
@@ -2399,7 +2619,11 @@ function registerDeployCommand(program2) {
2399
2619
  console.log(JSON.stringify(plan.agent, null, 2));
2400
2620
  for (const skill of plan.skills) {
2401
2621
  console.log("");
2402
- console.log(pc8.bold(`2. POST /api/v1/agents/:id/skills \u2014 "${skill.name}"`));
2622
+ console.log(
2623
+ pc8.bold(
2624
+ `2. POST /api/v1/agents/:id/skills \u2014 "${skill.name}" (${LANGUAGE_LABEL[skillPayloadLanguage(skill)]})`
2625
+ )
2626
+ );
2403
2627
  console.log(
2404
2628
  JSON.stringify(
2405
2629
  { ...skill, code: `<${Buffer.byteLength(skill.code)} bytes of ${skill.scriptFilename}>` },
@@ -2453,7 +2677,12 @@ function registerDeployCommand(program2) {
2453
2677
  );
2454
2678
  for (const skill of plan.skills) {
2455
2679
  await client.upsertSkill(upserted.id, skill);
2456
- console.log(pc8.green(` \u2714 skill "${skill.name}" uploaded (${skill.scriptFilename})`));
2680
+ const packages = skill.scriptDependencies?.length ? `, npm: ${skill.scriptDependencies.join(", ")}` : "";
2681
+ console.log(
2682
+ pc8.green(
2683
+ ` \u2714 skill "${skill.name}" uploaded (${skill.scriptFilename}, ${LANGUAGE_LABEL[skillPayloadLanguage(skill)]}${packages})`
2684
+ )
2685
+ );
2457
2686
  }
2458
2687
  if (plan.agent.agentScheduleCron) {
2459
2688
  const filenames = plan.skills.map((skill) => skill.scriptFilename);
@@ -2476,16 +2705,33 @@ function registerDeployCommand(program2) {
2476
2705
  console.log(pc8.green(` \u2714 manual run triggered: ${runUrl}`));
2477
2706
  } catch (error) {
2478
2707
  if (error instanceof DeployError && error.status === 409) {
2479
- console.error(
2480
- pc8.yellow(
2481
- ` \u2716 manual run not started: the agent is not enabled on the platform yet.`
2482
- )
2483
- );
2484
- console.error(
2485
- pc8.dim(
2486
- ` Deploy itself succeeded. Enable "${manifest.name}" in your kohala.ai dashboard, then re-run \`kohala deploy ${agent} --run\` (or trigger a run from the dashboard).`
2487
- )
2488
- );
2708
+ const reason = classifyManualRun409(error);
2709
+ if (reason === "nothing_to_run") {
2710
+ console.error(
2711
+ pc8.yellow(
2712
+ ` \u2716 manual run not started: no script is bound to an active schedule, so a run would execute nothing.`
2713
+ )
2714
+ );
2715
+ console.error(
2716
+ pc8.dim(
2717
+ ` Deploy itself succeeded. The platform only runs scripts bound to a cron schedule: add e.g. "schedule": "0 9 * * *" to kohala.json, then re-run \`kohala deploy ${agent} --run\` \u2014 deploy binds every skill script to it.`
2718
+ )
2719
+ );
2720
+ } else if (reason === "disabled") {
2721
+ console.error(
2722
+ pc8.yellow(
2723
+ ` \u2716 manual run not started: the agent is not enabled on the platform yet.`
2724
+ )
2725
+ );
2726
+ console.error(
2727
+ pc8.dim(
2728
+ ` Deploy itself succeeded. Enable "${manifest.name}" in your kohala.ai dashboard, then re-run \`kohala deploy ${agent} --run\` (or trigger a run from the dashboard). Enabling survives redeploys.`
2729
+ )
2730
+ );
2731
+ } else {
2732
+ console.error(pc8.yellow(` \u2716 manual run not started (409).`));
2733
+ console.error(pc8.dim(` ${error.message}`));
2734
+ }
2489
2735
  process.exitCode = 1;
2490
2736
  return;
2491
2737
  }