neuvikon 0.2.18 → 0.2.20
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 -7
- package/dist/index.mjs +140 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,13 +55,18 @@ pbpaste | npx neuvikon start
|
|
|
55
55
|
npx neuvikon start --tickets EWWQ-1,EWWQ-2,EWWQ-3 --spawn
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
If [Herdr](https://herdr.dev) is installed, every ticket in the batch lands as
|
|
59
|
+
its own tab in one Herdr workspace instead of separate OS windows — Herdr's
|
|
60
|
+
own UI then shows each agent's live working/blocked/done state. Run it from
|
|
61
|
+
inside a Herdr-managed pane and the batch adds its tabs to that same
|
|
62
|
+
workspace instead of opening a new one. Otherwise it
|
|
63
|
+
opens one terminal per ticket the usual way (a tmux window if you're already
|
|
64
|
+
in tmux, an iTerm2 or Terminal.app tab on macOS, a Windows Terminal tab on
|
|
65
|
+
Windows), each `cd`'d into this directory. Either way, if a coding agent CLI
|
|
66
|
+
is installed (Claude Code, Cursor, Codex) it's launched with that ticket's
|
|
67
|
+
full text as its first prompt — same substance a manual paste would give it.
|
|
68
|
+
With more than one installed, you're asked once which to use and that choice
|
|
69
|
+
is remembered (`--agent <name>` overrides it any time). Nothing is tracked
|
|
65
70
|
automatically, even then: a session only scopes safely once an agent is
|
|
66
71
|
actually running, so the launched agent runs `npx neuvikon start --ticket
|
|
67
72
|
EWWQ-1` itself once it's up, via its rules file, same as a manual paste
|
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { delimiter, dirname, join } from "node:path";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
5
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
//#region src/api.ts
|
|
8
8
|
var ApiError = class extends Error {
|
|
@@ -1262,6 +1262,124 @@ async function resolveAgentCli(flags) {
|
|
|
1262
1262
|
return picked;
|
|
1263
1263
|
}
|
|
1264
1264
|
//#endregion
|
|
1265
|
+
//#region src/herdr.ts
|
|
1266
|
+
/** Herdr's `--kind` values for the agents we already know how to pick between. */
|
|
1267
|
+
const HERDR_KIND = {
|
|
1268
|
+
"claude-code": "claude",
|
|
1269
|
+
codex: "codex",
|
|
1270
|
+
cursor: "cursor"
|
|
1271
|
+
};
|
|
1272
|
+
function herdrAvailable() {
|
|
1273
|
+
return isOnPath("herdr");
|
|
1274
|
+
}
|
|
1275
|
+
/** Every mutating Herdr socket-API command prints one JSON object; a failure prints to stderr instead. */
|
|
1276
|
+
function runHerdr(args) {
|
|
1277
|
+
try {
|
|
1278
|
+
const output = execFileSync("herdr", args, {
|
|
1279
|
+
encoding: "utf8",
|
|
1280
|
+
stdio: [
|
|
1281
|
+
"ignore",
|
|
1282
|
+
"pipe",
|
|
1283
|
+
"ignore"
|
|
1284
|
+
]
|
|
1285
|
+
});
|
|
1286
|
+
return JSON.parse(output);
|
|
1287
|
+
} catch {
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Herdr's `agent start` requires a name matching `[a-z][a-z0-9_-]{0,31}`,
|
|
1293
|
+
* unique among live agents. A ticket ref (`NS-60`) lowercases straight into
|
|
1294
|
+
* that shape, and reusing the ref as the name is what makes `herdr agent
|
|
1295
|
+
* get NS-60`-style lookups obvious later.
|
|
1296
|
+
*/
|
|
1297
|
+
function agentName(ref) {
|
|
1298
|
+
return ref.toLowerCase();
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* One Herdr workspace per `--spawn` call, one tab per ticket, so a whole
|
|
1302
|
+
* batch shows up together in Herdr's own UI — with its live
|
|
1303
|
+
* working/blocked/done state per agent — instead of scattered across OS
|
|
1304
|
+
* windows. The first successful ref seeds the workspace; every later ref in
|
|
1305
|
+
* the same batch adds a tab to it.
|
|
1306
|
+
*
|
|
1307
|
+
* Except when `--spawn` is itself run from inside a Herdr-managed pane
|
|
1308
|
+
* (`HERDR_WORKSPACE_ID` set, per Herdr's own caller-context injection): then
|
|
1309
|
+
* the batch reuses that workspace instead of creating a new one that yanks
|
|
1310
|
+
* focus away from wherever the user already was.
|
|
1311
|
+
*/
|
|
1312
|
+
var HerdrBatch = class {
|
|
1313
|
+
workspaceId = process.env.HERDR_WORKSPACE_ID || null;
|
|
1314
|
+
openPane(ref, cwd) {
|
|
1315
|
+
if (!this.workspaceId) {
|
|
1316
|
+
const created = runHerdr([
|
|
1317
|
+
"workspace",
|
|
1318
|
+
"create",
|
|
1319
|
+
"--cwd",
|
|
1320
|
+
cwd,
|
|
1321
|
+
"--label",
|
|
1322
|
+
ref,
|
|
1323
|
+
"--focus"
|
|
1324
|
+
]);
|
|
1325
|
+
if (!created) return null;
|
|
1326
|
+
this.workspaceId = created.result.workspace.workspace_id;
|
|
1327
|
+
return created.result.root_pane.pane_id;
|
|
1328
|
+
}
|
|
1329
|
+
return runHerdr([
|
|
1330
|
+
"tab",
|
|
1331
|
+
"create",
|
|
1332
|
+
"--workspace",
|
|
1333
|
+
this.workspaceId,
|
|
1334
|
+
"--cwd",
|
|
1335
|
+
cwd,
|
|
1336
|
+
"--label",
|
|
1337
|
+
ref,
|
|
1338
|
+
"--focus"
|
|
1339
|
+
])?.result.root_pane.pane_id ?? null;
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* "agent" — a coding agent is live in its own Herdr pane with the ticket
|
|
1343
|
+
* text confirmed delivered as its first prompt. "manual" — the pane exists
|
|
1344
|
+
* (either as a plain shell with the ticket echoed, or a started agent whose
|
|
1345
|
+
* prompt delivery couldn't be confirmed) but the user has to paste or type
|
|
1346
|
+
* into it themselves. `null` only means Herdr itself couldn't be reached,
|
|
1347
|
+
* and the caller should fall back to opening a native OS terminal instead.
|
|
1348
|
+
*/
|
|
1349
|
+
spawn(ref, cwd, agent, ticketText) {
|
|
1350
|
+
const paneId = this.openPane(ref, cwd);
|
|
1351
|
+
if (!paneId) return null;
|
|
1352
|
+
if (agent && ticketText) {
|
|
1353
|
+
if (runHerdr([
|
|
1354
|
+
"agent",
|
|
1355
|
+
"start",
|
|
1356
|
+
agentName(ref),
|
|
1357
|
+
"--kind",
|
|
1358
|
+
HERDR_KIND[agent.id],
|
|
1359
|
+
"--pane",
|
|
1360
|
+
paneId
|
|
1361
|
+
])) return runHerdr([
|
|
1362
|
+
"agent",
|
|
1363
|
+
"prompt",
|
|
1364
|
+
agentName(ref),
|
|
1365
|
+
ticketText,
|
|
1366
|
+
"--wait",
|
|
1367
|
+
"--until",
|
|
1368
|
+
"working",
|
|
1369
|
+
"--timeout",
|
|
1370
|
+
"8000"
|
|
1371
|
+
]) ? "agent" : "manual";
|
|
1372
|
+
}
|
|
1373
|
+
runHerdr([
|
|
1374
|
+
"pane",
|
|
1375
|
+
"run",
|
|
1376
|
+
paneId,
|
|
1377
|
+
`echo 'Ticket: ${ref}' && echo 'Paste it into your agent, or run: npx neuvikon start --ticket ${ref}'`
|
|
1378
|
+
]);
|
|
1379
|
+
return "manual";
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
//#endregion
|
|
1265
1383
|
//#region src/terminal.ts
|
|
1266
1384
|
function shQuote(value) {
|
|
1267
1385
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -1416,14 +1534,6 @@ async function fetchTicketText(credentials, ref) {
|
|
|
1416
1534
|
return null;
|
|
1417
1535
|
}
|
|
1418
1536
|
}
|
|
1419
|
-
async function buildLaunch(agent, credentials, ref) {
|
|
1420
|
-
const text = await fetchTicketText(credentials, ref);
|
|
1421
|
-
if (!text) return null;
|
|
1422
|
-
return {
|
|
1423
|
-
binary: agent.binary,
|
|
1424
|
-
args: agent.buildArgs(text)
|
|
1425
|
-
};
|
|
1426
|
-
}
|
|
1427
1537
|
/**
|
|
1428
1538
|
* Opens one terminal per ref and stops there — it deliberately never calls
|
|
1429
1539
|
* `start` itself. A session file written outside of a running agent has no
|
|
@@ -1434,13 +1544,32 @@ async function buildLaunch(agent, credentials, ref) {
|
|
|
1434
1544
|
* as its first prompt, but this never calls `start` itself — the agent does,
|
|
1435
1545
|
* once it's actually running, via the rules file, exactly as if the ticket
|
|
1436
1546
|
* had been pasted in by hand.
|
|
1547
|
+
*
|
|
1548
|
+
* Herdr, when installed, wins over a native OS terminal: it's a multiplexer
|
|
1549
|
+
* built for exactly this (multiple coding agents, one UI, live
|
|
1550
|
+
* working/blocked/done state), so a whole `--tickets` batch lands as one
|
|
1551
|
+
* Herdr workspace with one tab per ticket instead of scattered app windows.
|
|
1437
1552
|
*/
|
|
1438
1553
|
async function spawnMode(flags, credentials, refs) {
|
|
1439
1554
|
const cwd = process.cwd();
|
|
1440
1555
|
const agent = await resolveAgentCli(flags);
|
|
1441
|
-
|
|
1556
|
+
const herdr = herdrAvailable() ? new HerdrBatch() : null;
|
|
1557
|
+
info(`Opening ${refs.length} terminal${refs.length === 1 ? "" : "s"}${herdr ? " in Herdr" : ` in ${cwd}`}...`);
|
|
1442
1558
|
for (const ref of refs) {
|
|
1443
|
-
const
|
|
1559
|
+
const ticketText = agent ? await fetchTicketText(credentials, ref) : null;
|
|
1560
|
+
const herdrOutcome = herdr?.spawn(ref, cwd, agent, ticketText);
|
|
1561
|
+
if (herdrOutcome === "agent") {
|
|
1562
|
+
success(`${bold(ref)} — ${agent.label} starting in Herdr`);
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
if (herdrOutcome === "manual") {
|
|
1566
|
+
info(`${bold(ref)} — opened in Herdr, paste the ticket manually: ${dim(`npx neuvikon start --ticket ${ref}`)}`);
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
const launch = agent && ticketText ? {
|
|
1570
|
+
binary: agent.binary,
|
|
1571
|
+
args: agent.buildArgs(ticketText)
|
|
1572
|
+
} : null;
|
|
1444
1573
|
const startingLabel = launch && agent ? agent.label : null;
|
|
1445
1574
|
if (spawnTerminal({
|
|
1446
1575
|
ref,
|