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.
Files changed (3) hide show
  1. package/README.md +12 -7
  2. package/dist/index.mjs +140 -11
  3. 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
- Opens one terminal per ticket (a tmux window if you're already in tmux, an
59
- iTerm2 or Terminal.app tab on macOS, a Windows Terminal tab on Windows), each
60
- `cd`'d into this directory. If a coding agent CLI is installed (Claude Code,
61
- Cursor, Codex) it's launched in each terminal with that ticket's full text as
62
- its first prompt — same substance a manual paste would give it. With more
63
- than one installed, you're asked once which to use and that choice is
64
- remembered (`--agent <name>` overrides it any time). Nothing is tracked
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
- info(`Opening ${refs.length} terminal${refs.length === 1 ? "" : "s"} in ${cwd}...`);
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 launch = agent ? await buildLaunch(agent, credentials, ref) : null;
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuvikon",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "description": "Tell Neuvikon which ticket your coding agent is working on.",
5
5
  "keywords": [
6
6
  "neuvikon",