neuvikon 0.2.19 → 0.2.21

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
@@ -57,7 +57,9 @@ npx neuvikon start --tickets EWWQ-1,EWWQ-2,EWWQ-3 --spawn
57
57
 
58
58
  If [Herdr](https://herdr.dev) is installed, every ticket in the batch lands as
59
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
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
61
63
  opens one terminal per ticket the usual way (a tmux window if you're already
62
64
  in tmux, an iTerm2 or Terminal.app tab on macOS, a Windows Terminal tab on
63
65
  Windows), each `cd`'d into this directory. Either way, if a coding agent CLI
package/dist/index.mjs CHANGED
@@ -560,16 +560,26 @@ async function postUsage(credentials, session, delta) {
560
560
  * Switching tickets used to stop the outgoing session bare, which dropped every
561
561
  * token it had earned since its last turn ended — and a ticket that was started
562
562
  * and left inside a single turn recorded exactly nothing.
563
+ *
564
+ * Returns whether the server actually confirmed the stop, so a caller can tell
565
+ * the difference between "closed" and "we gave up trying to tell the server" —
566
+ * a repeat `stop` for an already-stopped session is a safe no-op on the server
567
+ * side, so it's fine for a caller to retry this on failure.
563
568
  */
564
569
  async function stopSession(credentials, session, transcript) {
565
570
  const usage = pendingUsage(session, transcript);
566
- await post(credentials.url, "/api/agent/events", {
567
- token: credentials.token,
568
- event: "stop",
569
- sessionKey: session.sessionKey,
570
- tokensIn: usage.tokensIn,
571
- tokensOut: usage.tokensOut
572
- }).catch(() => void 0);
571
+ try {
572
+ await post(credentials.url, "/api/agent/events", {
573
+ token: credentials.token,
574
+ event: "stop",
575
+ sessionKey: session.sessionKey,
576
+ tokensIn: usage.tokensIn,
577
+ tokensOut: usage.tokensOut
578
+ });
579
+ return true;
580
+ } catch {
581
+ return false;
582
+ }
573
583
  }
574
584
  /**
575
585
  * Called from an agent hook on every turn, so it is deliberately silent and
@@ -1303,9 +1313,14 @@ function agentName(ref) {
1303
1313
  * working/blocked/done state per agent — instead of scattered across OS
1304
1314
  * windows. The first successful ref seeds the workspace; every later ref in
1305
1315
  * the same batch adds a tab to it.
1316
+ *
1317
+ * Except when `--spawn` is itself run from inside a Herdr-managed pane
1318
+ * (`HERDR_WORKSPACE_ID` set, per Herdr's own caller-context injection): then
1319
+ * the batch reuses that workspace instead of creating a new one that yanks
1320
+ * focus away from wherever the user already was.
1306
1321
  */
1307
1322
  var HerdrBatch = class {
1308
- workspaceId = null;
1323
+ workspaceId = process.env.HERDR_WORKSPACE_ID || null;
1309
1324
  openPane(ref, cwd) {
1310
1325
  if (!this.workspaceId) {
1311
1326
  const created = runHerdr([
@@ -1334,15 +1349,16 @@ var HerdrBatch = class {
1334
1349
  ])?.result.root_pane.pane_id ?? null;
1335
1350
  }
1336
1351
  /**
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.
1352
+ * "agent" — a coding agent is live in its own Herdr pane with the ticket
1353
+ * text confirmed delivered as its first prompt. "manual" — the pane exists
1354
+ * (either as a plain shell with the ticket echoed, or a started agent whose
1355
+ * prompt delivery couldn't be confirmed) but the user has to paste or type
1356
+ * into it themselves. `null` only means Herdr itself couldn't be reached,
1357
+ * and the caller should fall back to opening a native OS terminal instead.
1342
1358
  */
1343
1359
  spawn(ref, cwd, agent, ticketText) {
1344
1360
  const paneId = this.openPane(ref, cwd);
1345
- if (!paneId) return false;
1361
+ if (!paneId) return null;
1346
1362
  if (agent && ticketText) {
1347
1363
  if (runHerdr([
1348
1364
  "agent",
@@ -1352,15 +1368,17 @@ var HerdrBatch = class {
1352
1368
  HERDR_KIND[agent.id],
1353
1369
  "--pane",
1354
1370
  paneId
1355
- ])) {
1356
- runHerdr([
1357
- "agent",
1358
- "prompt",
1359
- agentName(ref),
1360
- ticketText
1361
- ]);
1362
- return true;
1363
- }
1371
+ ])) return runHerdr([
1372
+ "agent",
1373
+ "prompt",
1374
+ agentName(ref),
1375
+ ticketText,
1376
+ "--wait",
1377
+ "--until",
1378
+ "working",
1379
+ "--timeout",
1380
+ "8000"
1381
+ ]) ? "agent" : "manual";
1364
1382
  }
1365
1383
  runHerdr([
1366
1384
  "pane",
@@ -1368,7 +1386,7 @@ var HerdrBatch = class {
1368
1386
  paneId,
1369
1387
  `echo 'Ticket: ${ref}' && echo 'Paste it into your agent, or run: npx neuvikon start --ticket ${ref}'`
1370
1388
  ]);
1371
- return true;
1389
+ return "manual";
1372
1390
  }
1373
1391
  };
1374
1392
  //#endregion
@@ -1549,8 +1567,13 @@ async function spawnMode(flags, credentials, refs) {
1549
1567
  info(`Opening ${refs.length} terminal${refs.length === 1 ? "" : "s"}${herdr ? " in Herdr" : ` in ${cwd}`}...`);
1550
1568
  for (const ref of refs) {
1551
1569
  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"}`);
1570
+ const herdrOutcome = herdr?.spawn(ref, cwd, agent, ticketText);
1571
+ if (herdrOutcome === "agent") {
1572
+ success(`${bold(ref)} — ${agent.label} starting in Herdr`);
1573
+ continue;
1574
+ }
1575
+ if (herdrOutcome === "manual") {
1576
+ info(`${bold(ref)} — opened in Herdr, paste the ticket manually: ${dim(`npx neuvikon start --ticket ${ref}`)}`);
1554
1577
  continue;
1555
1578
  }
1556
1579
  const launch = agent && ticketText ? {
@@ -1614,7 +1637,9 @@ async function start(flags, positional) {
1614
1637
  }
1615
1638
  throw error;
1616
1639
  }
1617
- if (existing) await stopSession(credentials, existing, transcript);
1640
+ if (existing) {
1641
+ if (!await stopSession(credentials, existing, transcript)) warn(`Switched off ${existing.ticket} locally, but Neuvikon didn't confirm the stop — it may still show in progress.`);
1642
+ }
1618
1643
  writeSession({
1619
1644
  sessionKey: result.sessionKey,
1620
1645
  ticket: ref,
@@ -1650,7 +1675,15 @@ async function stop(flags) {
1650
1675
  return;
1651
1676
  }
1652
1677
  const credentials = readCredentials();
1653
- if (credentials) await stopSession(credentials, session, flags.get("transcript") || claudeTranscriptPath());
1678
+ if (!credentials) {
1679
+ clearSession();
1680
+ success(`Stopped tracking ${session.ticket}`);
1681
+ return;
1682
+ }
1683
+ if (!await stopSession(credentials, session, flags.get("transcript") || claudeTranscriptPath())) {
1684
+ warn(`Stopped tracking ${session.ticket} locally, but Neuvikon didn't confirm it — the ticket may still show in progress.`);
1685
+ return;
1686
+ }
1654
1687
  clearSession();
1655
1688
  success(`Stopped tracking ${session.ticket}`);
1656
1689
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuvikon",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "description": "Tell Neuvikon which ticket your coding agent is working on.",
5
5
  "keywords": [
6
6
  "neuvikon",