@kynver-app/runtime 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +26,18 @@ login | setup | daemon
26
26
 
27
27
  ## Worker providers
28
28
 
29
- Default provider: `claude` (`workerProvider` in `~/.kynver/config.json`). Provider interface lives in `src/providers/`.
29
+ Set once in `kynver setup --provider claude|cursor` (stored in `~/.kynver/config.json` as `workerProvider`).
30
+
31
+ | Provider | CLI | Auth |
32
+ | --- | --- | --- |
33
+ | `claude` (default) | `claude` on PATH | `claude login` (OAuth). `ANTHROPIC_API_KEY` is stripped from worker env so OAuth wins. |
34
+ | `cursor` | `agent` on PATH ([Cursor Agent CLI](https://cursor.com/docs/cli/headless)) | `agent login` (OAuth) **or** `CURSOR_API_KEY` for headless runners. |
35
+
36
+ Override per invocation: `kynver worker start ... --provider cursor`
37
+
38
+ Install Cursor CLI (Windows PowerShell): `irm 'https://cursor.com/install?win32=true' | iex`
39
+
40
+ Default Cursor model: `composer-2.5`. Override with `--model` on dispatch/worker start.
30
41
 
31
42
  ## Tests
32
43
 
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { mkdirSync as mkdirSync5 } from "node:fs";
5
- import path12 from "node:path";
5
+ import path13 from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
8
  // src/config.ts
@@ -243,12 +243,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
243
243
  var DEFAULT_MAX_USED_PERCENT = 80;
244
244
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
245
245
  function observeRunnerDiskGate(input = {}) {
246
- const path13 = input.diskPath?.trim() || "/";
246
+ const path14 = input.diskPath?.trim() || "/";
247
247
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
248
248
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
249
249
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
250
250
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
251
- const stats = statfsSync(path13);
251
+ const stats = statfsSync(path14);
252
252
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
253
253
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
254
254
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -268,7 +268,7 @@ function observeRunnerDiskGate(input = {}) {
268
268
  }
269
269
  return {
270
270
  ok,
271
- path: path13,
271
+ path: path14,
272
272
  freeBytes,
273
273
  totalBytes,
274
274
  usedPercent,
@@ -379,7 +379,8 @@ function parseClaudeStream(file) {
379
379
  for (const line of lines) {
380
380
  const event = safeJson(line);
381
381
  if (!event) continue;
382
- const ts = event.timestamp || event.ts;
382
+ const tsMs = event.timestamp_ms;
383
+ const ts = event.timestamp || event.ts || (tsMs ? new Date(tsMs).toISOString() : void 0);
383
384
  if (ts) {
384
385
  result.firstEventAt ||= ts;
385
386
  result.lastEventAt = ts;
@@ -638,8 +639,8 @@ function observeRunnerResourceGate(input) {
638
639
  }
639
640
 
640
641
  // src/supervisor.ts
641
- import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "node:fs";
642
- import path6 from "node:path";
642
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3 } from "node:fs";
643
+ import path7 from "node:path";
643
644
 
644
645
  // src/prompt.ts
645
646
  function buildPrompt(input) {
@@ -698,9 +699,97 @@ var claudeProvider = {
698
699
  }
699
700
  };
700
701
 
702
+ // src/providers/cursor.ts
703
+ import { closeSync as closeSync2, existsSync as existsSync6, openSync as openSync2, readdirSync as readdirSync2 } from "node:fs";
704
+ import { spawn as spawn2 } from "node:child_process";
705
+ import path6 from "node:path";
706
+ var DEFAULT_CURSOR_MODEL = "composer-2.5";
707
+ function latestVersionDir(versionsRoot) {
708
+ if (!existsSync6(versionsRoot)) return null;
709
+ const versions = readdirSync2(versionsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^\d{4}\.\d/.test(entry.name)).map((entry) => entry.name).sort((a, b) => b.localeCompare(a));
710
+ return versions[0] ? path6.join(versionsRoot, versions[0]) : null;
711
+ }
712
+ function resolveBundledCursor(versionDir) {
713
+ const nodeExe = path6.join(versionDir, "node.exe");
714
+ const indexJs = path6.join(versionDir, "index.js");
715
+ if (!existsSync6(nodeExe) || !existsSync6(indexJs)) return null;
716
+ return { executable: nodeExe, prefixArgs: [indexJs], shell: false, detached: true };
717
+ }
718
+ function resolveWindowsCursorSpawn(agentBin) {
719
+ const agentRoot = path6.dirname(agentBin);
720
+ const direct = resolveBundledCursor(agentRoot);
721
+ if (direct) return direct;
722
+ const versionDir = latestVersionDir(path6.join(agentRoot, "versions"));
723
+ return versionDir ? resolveBundledCursor(versionDir) : null;
724
+ }
725
+ function resolveCursorSpawn(agentBin) {
726
+ if (process.platform === "win32" && /\.(cmd|bat)$/i.test(agentBin)) {
727
+ const bundled = resolveWindowsCursorSpawn(agentBin);
728
+ if (bundled) return bundled;
729
+ return { executable: agentBin, prefixArgs: [], shell: true, detached: false };
730
+ }
731
+ return { executable: agentBin, prefixArgs: [], shell: false, detached: true };
732
+ }
733
+ function resolveAgentBin() {
734
+ const configured = process.env.KYNVER_CURSOR_AGENT_BIN?.trim() || process.env.CURSOR_AGENT_BIN?.trim();
735
+ if (configured) return configured;
736
+ if (process.platform === "win32") {
737
+ const localAgent = path6.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
738
+ if (existsSync6(localAgent)) return localAgent;
739
+ }
740
+ return "agent";
741
+ }
742
+ var cursorProvider = {
743
+ name: "cursor",
744
+ start(opts) {
745
+ const model = opts.model || DEFAULT_CURSOR_MODEL;
746
+ const stdoutFd = openSync2(opts.stdoutPath, "a");
747
+ const stderrFd = openSync2(opts.stderrPath, "a");
748
+ const agentBin = resolveAgentBin();
749
+ const spawnTarget = resolveCursorSpawn(agentBin);
750
+ const child = spawn2(
751
+ spawnTarget.executable,
752
+ [
753
+ ...spawnTarget.prefixArgs,
754
+ "-p",
755
+ "--force",
756
+ "--trust",
757
+ "--workspace",
758
+ opts.worktreePath,
759
+ "--output-format",
760
+ "stream-json",
761
+ "--stream-partial-output",
762
+ "--model",
763
+ model,
764
+ opts.prompt
765
+ ],
766
+ {
767
+ cwd: opts.worktreePath,
768
+ detached: spawnTarget.detached,
769
+ shell: spawnTarget.shell,
770
+ stdio: ["ignore", stdoutFd, stderrFd],
771
+ env: {
772
+ ...process.env,
773
+ ...spawnTarget.prefixArgs.length > 0 ? { CURSOR_INVOKED_AS: path6.basename(agentBin) } : {}
774
+ }
775
+ }
776
+ );
777
+ closeSync2(stdoutFd);
778
+ closeSync2(stderrFd);
779
+ if (!child.pid) {
780
+ throw new Error(
781
+ `failed to spawn Cursor agent worker (is \`${agentBin}\` on PATH? run \`agent login\` or set CURSOR_API_KEY)`
782
+ );
783
+ }
784
+ child.unref();
785
+ return { pid: child.pid, model };
786
+ }
787
+ };
788
+
701
789
  // src/providers/registry.ts
702
790
  var BUILTIN = {
703
- claude: claudeProvider
791
+ claude: claudeProvider,
792
+ cursor: cursorProvider
704
793
  };
705
794
  var overrideProvider = null;
706
795
  function resolveWorkerProvider(name) {
@@ -720,16 +809,16 @@ function spawnWorkerProcess(run, opts) {
720
809
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
721
810
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
722
811
  const { worktreesDir } = getPaths();
723
- const workerDir = path6.join(runDirectory(run.id), "workers", name);
812
+ const workerDir = path7.join(runDirectory(run.id), "workers", name);
724
813
  mkdirSync3(workerDir, { recursive: true });
725
- const worktreePath = path6.join(worktreesDir, run.id, name);
814
+ const worktreePath = path7.join(worktreesDir, run.id, name);
726
815
  const branch = opts.branch || `agent/${run.id}/${name}`;
727
- if (existsSync6(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
816
+ if (existsSync7(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
728
817
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
729
818
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
730
- const stdoutPath = path6.join(workerDir, "stdout.jsonl");
731
- const stderrPath = path6.join(workerDir, "stderr.log");
732
- const heartbeatPath = path6.join(workerDir, "heartbeat.jsonl");
819
+ const stdoutPath = path7.join(workerDir, "stdout.jsonl");
820
+ const stderrPath = path7.join(workerDir, "stderr.log");
821
+ const heartbeatPath = path7.join(workerDir, "heartbeat.jsonl");
733
822
  const prompt = buildPrompt({
734
823
  task: opts.task,
735
824
  ownedPaths: opts.ownedPaths || [],
@@ -778,7 +867,7 @@ function spawnWorkerProcess(run, opts) {
778
867
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
779
868
  };
780
869
  saveWorker(run.id, worker);
781
- run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path6.join(workerDir, "worker.json") } };
870
+ run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path7.join(workerDir, "worker.json") } };
782
871
  run.status = "running";
783
872
  saveRun(run);
784
873
  return worker;
@@ -974,7 +1063,7 @@ async function dispatchRun(args) {
974
1063
  }
975
1064
 
976
1065
  // src/sweep.ts
977
- import path7 from "node:path";
1066
+ import path8 from "node:path";
978
1067
  async function sweepRun(args) {
979
1068
  const pipeline = args.pipeline === true || args.pipeline === "true";
980
1069
  try {
@@ -986,7 +1075,7 @@ async function sweepRun(args) {
986
1075
  const releasedLocalOrphans = [];
987
1076
  for (const name of Object.keys(run.workers || {})) {
988
1077
  const worker = readJson(
989
- path7.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1078
+ path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
990
1079
  null
991
1080
  );
992
1081
  if (!worker || !worker.dispatched || !worker.taskId) continue;
@@ -1029,11 +1118,11 @@ async function sweepRun(args) {
1029
1118
  }
1030
1119
 
1031
1120
  // src/worktree.ts
1032
- import { existsSync as existsSync7, mkdirSync as mkdirSync4 } from "node:fs";
1033
- import path9 from "node:path";
1121
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4 } from "node:fs";
1122
+ import path10 from "node:path";
1034
1123
 
1035
1124
  // src/validate.ts
1036
- import path8 from "node:path";
1125
+ import path9 from "node:path";
1037
1126
  var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
1038
1127
  function validateRunId(runId) {
1039
1128
  const trimmed = runId.trim();
@@ -1041,7 +1130,7 @@ function validateRunId(runId) {
1041
1130
  return trimmed;
1042
1131
  }
1043
1132
  function validateRepo(repo) {
1044
- const resolved = path8.resolve(repo);
1133
+ const resolved = path9.resolve(repo);
1045
1134
  if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
1046
1135
  return resolved;
1047
1136
  }
@@ -1052,7 +1141,7 @@ function createRun(args) {
1052
1141
  ensureGitRepo(repo);
1053
1142
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
1054
1143
  const dir = runDirectory(id);
1055
- if (existsSync7(dir)) failExists(`run already exists: ${id}`);
1144
+ if (existsSync8(dir)) failExists(`run already exists: ${id}`);
1056
1145
  mkdirSync4(dir, { recursive: true });
1057
1146
  const base = String(args.base || "origin/main");
1058
1147
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -1066,12 +1155,12 @@ function createRun(args) {
1066
1155
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1067
1156
  workers: {}
1068
1157
  };
1069
- writeJson(path9.join(dir, "run.json"), run);
1158
+ writeJson(path10.join(dir, "run.json"), run);
1070
1159
  console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
1071
1160
  }
1072
1161
  function listRuns() {
1073
1162
  const { runsDir } = getPaths();
1074
- const rows = listRunIds(runsDir).map((id) => readJson(path9.join(runDirectory(id), "run.json"), null)).filter(Boolean).map((run) => ({
1163
+ const rows = listRunIds(runsDir).map((id) => readJson(path10.join(runDirectory(id), "run.json"), null)).filter(Boolean).map((run) => ({
1075
1164
  id: run.id,
1076
1165
  name: run.name,
1077
1166
  status: run.status,
@@ -1086,7 +1175,7 @@ function failExists(message) {
1086
1175
  }
1087
1176
 
1088
1177
  // src/worker-ops.ts
1089
- import path10 from "node:path";
1178
+ import path11 from "node:path";
1090
1179
  async function tryCompleteWorker(args) {
1091
1180
  const worker = loadWorker(String(args.run), String(args.name));
1092
1181
  const status = computeWorkerStatus(worker);
@@ -1179,7 +1268,7 @@ async function completeWorker(args) {
1179
1268
  function workerStatus(args) {
1180
1269
  const worker = loadWorker(String(args.run), String(args.name));
1181
1270
  const status = computeWorkerStatus(worker);
1182
- writeJson(path10.join(worker.workerDir, "last-status.json"), status);
1271
+ writeJson(path11.join(worker.workerDir, "last-status.json"), status);
1183
1272
  console.log(JSON.stringify(status, null, 2));
1184
1273
  }
1185
1274
  function runStatus(args) {
@@ -1187,7 +1276,7 @@ function runStatus(args) {
1187
1276
  const names = Object.keys(run.workers || {});
1188
1277
  const workers = names.map((name) => {
1189
1278
  const worker = readJson(
1190
- path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1279
+ path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1191
1280
  null
1192
1281
  );
1193
1282
  if (!worker) {
@@ -1219,7 +1308,7 @@ function runStatus(args) {
1219
1308
  needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
1220
1309
  workers
1221
1310
  };
1222
- writeJson(path10.join(runDirectory(run.id), "last-board.json"), board);
1311
+ writeJson(path11.join(runDirectory(run.id), "last-board.json"), board);
1223
1312
  console.log(JSON.stringify(board, null, 2));
1224
1313
  }
1225
1314
  function tailWorker(args) {
@@ -1252,7 +1341,7 @@ function stopWorker(args) {
1252
1341
  }
1253
1342
 
1254
1343
  // src/pipeline-tick.ts
1255
- import path11 from "node:path";
1344
+ import path12 from "node:path";
1256
1345
 
1257
1346
  // src/workspace-runtime-config.ts
1258
1347
  async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
@@ -1281,7 +1370,7 @@ async function completeFinishedWorkers(runId, args) {
1281
1370
  const outcomes = [];
1282
1371
  for (const name of Object.keys(run.workers || {})) {
1283
1372
  const worker = readJson(
1284
- path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1373
+ path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1285
1374
  null
1286
1375
  );
1287
1376
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -1414,14 +1503,14 @@ function usage(code = 0) {
1414
1503
  [
1415
1504
  "Usage:",
1416
1505
  " kynver login --api-key KEY",
1417
- " kynver setup [--api-base-url URL] [--agent-os-id ID] [--agent-os-slug SLUG] [--repo PATH] [--max-workers N]",
1506
+ " kynver setup [--api-base-url URL] [--agent-os-id ID] [--agent-os-slug SLUG] [--repo PATH] [--max-workers N] [--provider claude|cursor]",
1418
1507
  " kynver daemon --run RUN_ID --agent-os-id AOS_ID [--execute] [--interval-ms MS]",
1419
1508
  " kynver run create --repo /path/repo [--name name] [--base origin/main]",
1420
1509
  " kynver run list",
1421
1510
  " kynver run status --run RUN_ID",
1422
1511
  " kynver run dispatch --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--execute] [--lane any|implementation|review|landing] [--max-starts 1] [--lease-ms MS] [--owned path[,path]] [--model claude-opus-4-7] [--disk-path /]",
1423
1512
  " kynver run sweep --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--grace-ms MS]",
1424
- ' kynver worker start --run RUN_ID --name worker --task "..." [--owned path[,path]] [--model claude-opus-4-7] [--agent-os-id AOS_ID] [--task-id TASK_ID]',
1513
+ ' kynver worker start --run RUN_ID --name worker --task "..." [--owned path[,path]] [--model MODEL] [--provider claude|cursor] [--agent-os-id AOS_ID] [--task-id TASK_ID]',
1425
1514
  " kynver worker status --run RUN_ID --name worker",
1426
1515
  " kynver worker tail --run RUN_ID --name worker [--lines 40] [--raw]",
1427
1516
  " kynver worker stop --run RUN_ID --name worker",
@@ -1460,7 +1549,7 @@ async function main(argv = process.argv.slice(2)) {
1460
1549
  if (scope === "worker" && action === "complete") return void await completeWorker(args);
1461
1550
  unknownCommand(scope, action);
1462
1551
  }
1463
- var isCliEntry = process.argv[1] && path12.resolve(process.argv[1]) === path12.resolve(fileURLToPath(import.meta.url));
1552
+ var isCliEntry = process.argv[1] && path13.resolve(process.argv[1]) === path13.resolve(fileURLToPath(import.meta.url));
1464
1553
  if (isCliEntry) {
1465
1554
  void main().catch((error) => {
1466
1555
  console.error(error);