neuvikon 0.2.18 → 0.2.19

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 +10 -7
  2. package/dist/index.mjs +127 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -55,13 +55,16 @@ 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. Otherwise it
61
+ opens one terminal per ticket the usual way (a tmux window if you're already
62
+ in tmux, an iTerm2 or Terminal.app tab on macOS, a Windows Terminal tab on
63
+ Windows), each `cd`'d into this directory. Either way, if a coding agent CLI
64
+ is installed (Claude Code, Cursor, Codex) it's launched with that ticket's
65
+ full text as its first prompt — same substance a manual paste would give it.
66
+ With more than one installed, you're asked once which to use and that choice
67
+ is remembered (`--agent <name>` overrides it any time). Nothing is tracked
65
68
  automatically, even then: a session only scopes safely once an agent is
66
69
  actually running, so the launched agent runs `npx neuvikon start --ticket
67
70
  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,116 @@ 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
+ var HerdrBatch = class {
1308
+ workspaceId = null;
1309
+ openPane(ref, cwd) {
1310
+ if (!this.workspaceId) {
1311
+ const created = runHerdr([
1312
+ "workspace",
1313
+ "create",
1314
+ "--cwd",
1315
+ cwd,
1316
+ "--label",
1317
+ ref,
1318
+ "--focus"
1319
+ ]);
1320
+ if (!created) return null;
1321
+ this.workspaceId = created.result.workspace.workspace_id;
1322
+ return created.result.root_pane.pane_id;
1323
+ }
1324
+ return runHerdr([
1325
+ "tab",
1326
+ "create",
1327
+ "--workspace",
1328
+ this.workspaceId,
1329
+ "--cwd",
1330
+ cwd,
1331
+ "--label",
1332
+ ref,
1333
+ "--focus"
1334
+ ])?.result.root_pane.pane_id ?? null;
1335
+ }
1336
+ /**
1337
+ * True once this ref has a live Herdr pane — even if starting an agent in
1338
+ * it failed, in which case the pane still exists with the same
1339
+ * print-and-wait fallback a plain spawned terminal gets. False only means
1340
+ * Herdr itself couldn't be reached, and the caller should fall back to
1341
+ * opening a native OS terminal for this ref instead.
1342
+ */
1343
+ spawn(ref, cwd, agent, ticketText) {
1344
+ const paneId = this.openPane(ref, cwd);
1345
+ if (!paneId) return false;
1346
+ if (agent && ticketText) {
1347
+ if (runHerdr([
1348
+ "agent",
1349
+ "start",
1350
+ agentName(ref),
1351
+ "--kind",
1352
+ HERDR_KIND[agent.id],
1353
+ "--pane",
1354
+ paneId
1355
+ ])) {
1356
+ runHerdr([
1357
+ "agent",
1358
+ "prompt",
1359
+ agentName(ref),
1360
+ ticketText
1361
+ ]);
1362
+ return true;
1363
+ }
1364
+ }
1365
+ runHerdr([
1366
+ "pane",
1367
+ "run",
1368
+ paneId,
1369
+ `echo 'Ticket: ${ref}' && echo 'Paste it into your agent, or run: npx neuvikon start --ticket ${ref}'`
1370
+ ]);
1371
+ return true;
1372
+ }
1373
+ };
1374
+ //#endregion
1265
1375
  //#region src/terminal.ts
1266
1376
  function shQuote(value) {
1267
1377
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -1416,14 +1526,6 @@ async function fetchTicketText(credentials, ref) {
1416
1526
  return null;
1417
1527
  }
1418
1528
  }
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
1529
  /**
1428
1530
  * Opens one terminal per ref and stops there — it deliberately never calls
1429
1531
  * `start` itself. A session file written outside of a running agent has no
@@ -1434,13 +1536,27 @@ async function buildLaunch(agent, credentials, ref) {
1434
1536
  * as its first prompt, but this never calls `start` itself — the agent does,
1435
1537
  * once it's actually running, via the rules file, exactly as if the ticket
1436
1538
  * had been pasted in by hand.
1539
+ *
1540
+ * Herdr, when installed, wins over a native OS terminal: it's a multiplexer
1541
+ * built for exactly this (multiple coding agents, one UI, live
1542
+ * working/blocked/done state), so a whole `--tickets` batch lands as one
1543
+ * Herdr workspace with one tab per ticket instead of scattered app windows.
1437
1544
  */
1438
1545
  async function spawnMode(flags, credentials, refs) {
1439
1546
  const cwd = process.cwd();
1440
1547
  const agent = await resolveAgentCli(flags);
1441
- info(`Opening ${refs.length} terminal${refs.length === 1 ? "" : "s"} in ${cwd}...`);
1548
+ const herdr = herdrAvailable() ? new HerdrBatch() : null;
1549
+ info(`Opening ${refs.length} terminal${refs.length === 1 ? "" : "s"}${herdr ? " in Herdr" : ` in ${cwd}`}...`);
1442
1550
  for (const ref of refs) {
1443
- const launch = agent ? await buildLaunch(agent, credentials, ref) : null;
1551
+ const ticketText = agent ? await fetchTicketText(credentials, ref) : null;
1552
+ if (herdr?.spawn(ref, cwd, agent, ticketText)) {
1553
+ success(`${bold(ref)} — ${agent && ticketText ? `${agent.label} starting in Herdr` : "opened in Herdr"}`);
1554
+ continue;
1555
+ }
1556
+ const launch = agent && ticketText ? {
1557
+ binary: agent.binary,
1558
+ args: agent.buildArgs(ticketText)
1559
+ } : null;
1444
1560
  const startingLabel = launch && agent ? agent.label : null;
1445
1561
  if (spawnTerminal({
1446
1562
  ref,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuvikon",
3
- "version": "0.2.18",
3
+ "version": "0.2.19",
4
4
  "description": "Tell Neuvikon which ticket your coding agent is working on.",
5
5
  "keywords": [
6
6
  "neuvikon",