@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 +12 -1
- package/dist/cli.js +122 -33
- package/dist/cli.js.map +4 -4
- package/dist/index.js +125 -36
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,18 @@ login | setup | daemon
|
|
|
26
26
|
|
|
27
27
|
## Worker providers
|
|
28
28
|
|
|
29
|
-
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
642
|
-
import
|
|
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 =
|
|
812
|
+
const workerDir = path7.join(runDirectory(run.id), "workers", name);
|
|
724
813
|
mkdirSync3(workerDir, { recursive: true });
|
|
725
|
-
const worktreePath =
|
|
814
|
+
const worktreePath = path7.join(worktreesDir, run.id, name);
|
|
726
815
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
727
|
-
if (
|
|
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 =
|
|
731
|
-
const stderrPath =
|
|
732
|
-
const heartbeatPath =
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
1033
|
-
import
|
|
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
|
|
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 =
|
|
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 (
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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] &&
|
|
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);
|