neuvikon 0.2.8 → 0.2.10

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 +9 -0
  2. package/dist/index.mjs +299 -191
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -57,6 +57,9 @@ into `.claude/settings.json` for you, whenever that project already has one:
57
57
  {
58
58
  "hooks": {
59
59
  "Stop": [{ "hooks": [{ "type": "command", "command": "npx neuvikon report" }] }],
60
+ "Notification": [
61
+ { "hooks": [{ "type": "command", "command": "npx neuvikon report --heartbeat" }] }
62
+ ],
60
63
  "SessionEnd": [{ "hooks": [{ "type": "command", "command": "npx neuvikon stop" }] }]
61
64
  }
62
65
  }
@@ -66,6 +69,12 @@ into `.claude/settings.json` for you, whenever that project already has one:
66
69
  turn adds that turn rather than re-counting the conversation. It is silent and
67
70
  always exits 0 — a token count is never worth interrupting someone's work.
68
71
 
72
+ `Notification` fires when Claude Code is blocked mid-turn — a permission
73
+ prompt, a question, waiting on you — exactly when `Stop` doesn't fire yet. The
74
+ `--heartbeat` flag tells `report` to still check in even when there's no new
75
+ token usage to send, so a session that's genuinely just waiting on you doesn't
76
+ read as stopped on the board.
77
+
69
78
  If `.claude/settings.json` doesn't exist yet, or another agent is the one
70
79
  being used, add the block above by hand.
71
80
 
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, rmSync, writeFileSync } from "node:fs";
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 { dirname, join } from "node:path";
5
5
  import { spawn } from "node:child_process";
@@ -209,6 +209,133 @@ async function readStdin() {
209
209
  return Buffer.concat(chunks).toString("utf8");
210
210
  }
211
211
  //#endregion
212
+ //#region src/usage.ts
213
+ /**
214
+ * Sums token usage out of a JSONL agent transcript, starting from `offset`.
215
+ *
216
+ * Reading incrementally is what makes a per-turn hook safe to call repeatedly:
217
+ * each call reports only what was appended since the last one. Anything that
218
+ * doesn't parse is skipped rather than fatal — a transcript being appended to
219
+ * while we read it will have a torn final line, and that is normal, not an
220
+ * error worth failing a user's turn over.
221
+ */
222
+ /**
223
+ * Where the transcript currently ends, so a session that starts mid-conversation
224
+ * can mark its starting point instead of counting what came before it.
225
+ */
226
+ function transcriptSize(path) {
227
+ let fd;
228
+ try {
229
+ fd = openSync(path, "r");
230
+ } catch {
231
+ return null;
232
+ }
233
+ try {
234
+ return fstatSync(fd).size;
235
+ } catch {
236
+ return null;
237
+ } finally {
238
+ closeSync(fd);
239
+ }
240
+ }
241
+ function readUsageSince(path, offset, since) {
242
+ let fd;
243
+ try {
244
+ fd = openSync(path, "r");
245
+ } catch {
246
+ return null;
247
+ }
248
+ try {
249
+ const size = fstatSync(fd).size;
250
+ const from = offset > size ? 0 : offset;
251
+ if (size === from) return {
252
+ tokensIn: 0,
253
+ tokensOut: 0,
254
+ offset: size,
255
+ lastMessageId: since
256
+ };
257
+ const buffer = Buffer.alloc(size - from);
258
+ readSync(fd, buffer, 0, buffer.length, from);
259
+ const text = buffer.toString("utf8");
260
+ const lastNewline = text.lastIndexOf("\n");
261
+ if (lastNewline === -1) return {
262
+ tokensIn: 0,
263
+ tokensOut: 0,
264
+ offset: from,
265
+ lastMessageId: since
266
+ };
267
+ let tokensIn = 0;
268
+ let tokensOut = 0;
269
+ let lastMessageId = since;
270
+ for (const line of text.slice(0, lastNewline).split("\n")) {
271
+ if (!line.trim()) continue;
272
+ let row;
273
+ try {
274
+ row = JSON.parse(line);
275
+ } catch {
276
+ continue;
277
+ }
278
+ const usage = extractUsage(row);
279
+ if (!usage) continue;
280
+ if (usage.messageId !== void 0 && usage.messageId === lastMessageId) continue;
281
+ lastMessageId = usage.messageId ?? lastMessageId;
282
+ tokensIn += usage.tokensIn;
283
+ tokensOut += usage.tokensOut;
284
+ }
285
+ return {
286
+ tokensIn,
287
+ tokensOut,
288
+ offset: from + Buffer.byteLength(text.slice(0, lastNewline + 1)),
289
+ lastMessageId
290
+ };
291
+ } catch {
292
+ return null;
293
+ } finally {
294
+ closeSync(fd);
295
+ }
296
+ }
297
+ function toCount$1(value) {
298
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
299
+ }
300
+ function extractUsage(row) {
301
+ if (typeof row !== "object" || row === null) return null;
302
+ const message = row.message;
303
+ if (typeof message !== "object" || message === null) return null;
304
+ const usage = message.usage;
305
+ if (typeof usage !== "object" || usage === null) return null;
306
+ const id = message.id;
307
+ const fields = usage;
308
+ return {
309
+ messageId: typeof id === "string" && id ? id : void 0,
310
+ tokensIn: toCount$1(fields.input_tokens) + toCount$1(fields.cache_creation_input_tokens),
311
+ tokensOut: toCount$1(fields.output_tokens)
312
+ };
313
+ }
314
+ /**
315
+ * Claude Code's transcript for the session this command was spawned from.
316
+ *
317
+ * Hooks are handed a `transcript_path` on stdin, but `start` and `stop` are run
318
+ * by the agent's own shell, which only gets the session id in the environment.
319
+ * That id is a uuid, so finding the file by name is exact — and doesn't depend
320
+ * on how Claude Code derives a project directory name from the working
321
+ * directory, which is not a documented format.
322
+ */
323
+ function claudeTranscriptPath() {
324
+ const sessionId = process.env.CLAUDE_CODE_SESSION_ID?.trim();
325
+ if (!sessionId) return void 0;
326
+ const projects = join(homedir(), ".claude", "projects");
327
+ let directories;
328
+ try {
329
+ directories = readdirSync(projects);
330
+ } catch {
331
+ return;
332
+ }
333
+ for (const directory of directories) {
334
+ const candidate = join(projects, directory, `${sessionId}.jsonl`);
335
+ if (existsSync(candidate)) return candidate;
336
+ }
337
+ }
338
+ //#endregion
212
339
  //#region src/ui.ts
213
340
  /**
214
341
  * Output helpers. Everything the agent shouldn't parse goes to stderr, so a
@@ -239,8 +366,157 @@ function fail(message, hint) {
239
366
  process.exit(1);
240
367
  }
241
368
  //#endregion
369
+ //#region src/commands/report.ts
370
+ /**
371
+ * Agent hooks hand their context to the command on stdin as JSON. Reading the
372
+ * transcript path from there means a hook is a bare `npx neuvikon report` with
373
+ * nothing to wire up, and nothing to keep in sync when a tool renames a flag.
374
+ */
375
+ async function transcriptFromHookPayload() {
376
+ const raw = await readStdin();
377
+ if (!raw.trim()) return void 0;
378
+ try {
379
+ const payload = JSON.parse(raw);
380
+ if (typeof payload !== "object" || payload === null) return void 0;
381
+ const path = payload.transcript_path;
382
+ return typeof path === "string" && path ? path : void 0;
383
+ } catch {
384
+ return;
385
+ }
386
+ }
387
+ function toCount(raw) {
388
+ if (raw === void 0) return void 0;
389
+ const value = Number(raw);
390
+ return Number.isFinite(value) && value >= 0 ? value : void 0;
391
+ }
392
+ /**
393
+ * What the transcript has accumulated since this session's baseline.
394
+ *
395
+ * A session with no baseline yet establishes one here rather than counting
396
+ * from the top: everything before the ticket started belongs to whatever the
397
+ * agent was doing beforehand, not to this ticket.
398
+ */
399
+ function pendingUsage(session, transcript) {
400
+ if (!transcript) return {
401
+ tokensIn: 0,
402
+ tokensOut: 0
403
+ };
404
+ let baseline = session.transcriptOffset;
405
+ if (baseline === void 0) {
406
+ baseline = transcriptSize(transcript) ?? void 0;
407
+ if (baseline !== void 0) writeSession({
408
+ ...session,
409
+ transcriptOffset: baseline
410
+ });
411
+ }
412
+ if (baseline === void 0) return {
413
+ tokensIn: 0,
414
+ tokensOut: 0
415
+ };
416
+ const usage = readUsageSince(transcript, baseline, session.lastMessageId);
417
+ if (!usage) return {
418
+ tokensIn: 0,
419
+ tokensOut: 0
420
+ };
421
+ return {
422
+ tokensIn: usage.tokensIn,
423
+ tokensOut: usage.tokensOut,
424
+ nextOffset: usage.offset,
425
+ nextMessageId: usage.lastMessageId
426
+ };
427
+ }
428
+ /**
429
+ * Hands a delta to the server, advancing the local offset only once it is
430
+ * accepted — a dropped request leaves the window to be retried by the next
431
+ * call instead of silently losing those tokens.
432
+ */
433
+ async function postUsage(credentials, session, delta) {
434
+ try {
435
+ await post(credentials.url, "/api/agent/events", {
436
+ token: credentials.token,
437
+ event: "report",
438
+ sessionKey: session.sessionKey,
439
+ tokensIn: delta.tokensIn,
440
+ tokensOut: delta.tokensOut
441
+ });
442
+ } catch {
443
+ return false;
444
+ }
445
+ if (delta.nextOffset !== void 0) writeSession({
446
+ ...session,
447
+ transcriptOffset: delta.nextOffset,
448
+ lastMessageId: delta.nextMessageId
449
+ });
450
+ return true;
451
+ }
452
+ /**
453
+ * Closes a session, carrying everything it accumulated since its last report.
454
+ *
455
+ * Switching tickets used to stop the outgoing session bare, which dropped every
456
+ * token it had earned since its last turn ended — and a ticket that was started
457
+ * and left inside a single turn recorded exactly nothing.
458
+ */
459
+ async function stopSession(credentials, session, transcript) {
460
+ const usage = pendingUsage(session, transcript);
461
+ await post(credentials.url, "/api/agent/events", {
462
+ token: credentials.token,
463
+ event: "stop",
464
+ sessionKey: session.sessionKey,
465
+ tokensIn: usage.tokensIn,
466
+ tokensOut: usage.tokensOut
467
+ }).catch(() => void 0);
468
+ }
469
+ /**
470
+ * Called from an agent hook on every turn, so it is deliberately silent and
471
+ * never exits non-zero: a token count is not worth interrupting someone's work
472
+ * over, and a hook that fails loudly is a hook people uninstall.
473
+ */
474
+ async function report(flags) {
475
+ const credentials = readCredentials();
476
+ const session = readSession();
477
+ if (!credentials || !session) {
478
+ if (flags.has("verbose")) info(dim(credentials ? "No ticket is being tracked here." : "Not logged in."));
479
+ return;
480
+ }
481
+ const fromTranscript = pendingUsage(session, flags.get("transcript") || await transcriptFromHookPayload() || claudeTranscriptPath());
482
+ const delta = {
483
+ tokensIn: (toCount(flags.get("tokens-in")) ?? 0) + fromTranscript.tokensIn,
484
+ tokensOut: (toCount(flags.get("tokens-out")) ?? 0) + fromTranscript.tokensOut,
485
+ nextOffset: fromTranscript.nextOffset,
486
+ nextMessageId: fromTranscript.nextMessageId
487
+ };
488
+ if (!delta.tokensIn && !delta.tokensOut && !flags.has("heartbeat")) {
489
+ if (flags.has("verbose")) info(dim("No new usage to report."));
490
+ return;
491
+ }
492
+ await postUsage(credentials, session, delta);
493
+ if (flags.has("verbose")) info(dim(`Reported ${delta.tokensIn} in / ${delta.tokensOut} out`));
494
+ }
495
+ //#endregion
242
496
  //#region src/commands/hook-start.ts
243
497
  /**
498
+ * `UserPromptSubmit` fires before the turn runs, which is what lets it capture
499
+ * a token baseline at the transcript's current size — anything read from that
500
+ * offset onward is this ticket's usage. Reading it here, instead of lazily on
501
+ * the first `Stop`, is what `report.ts` used to do: by the time `Stop` fires
502
+ * the first turn's usage is already appended, so that baseline came too late
503
+ * and silently discarded a whole turn's tokens (NS-33).
504
+ */
505
+ function parsePayload(raw) {
506
+ if (!raw.trim()) return {};
507
+ try {
508
+ const payload = JSON.parse(raw);
509
+ if (typeof payload !== "object" || payload === null) return {};
510
+ const record = payload;
511
+ return {
512
+ prompt: typeof record.prompt === "string" ? record.prompt : void 0,
513
+ transcriptPath: typeof record.transcript_path === "string" ? record.transcript_path : void 0
514
+ };
515
+ } catch {
516
+ return {};
517
+ }
518
+ }
519
+ /**
244
520
  * The `UserPromptSubmit` counterpart to `start`. Unlike `start`, this runs on
245
521
  * *every* prompt regardless of whether it mentions a ticket, so it must never
246
522
  * fail the hook and block the user's message — no credentials, no ticket ref,
@@ -250,25 +526,25 @@ async function hookStart() {
250
526
  try {
251
527
  const credentials = readCredentials();
252
528
  if (!credentials) return;
253
- const [ref] = extractTicketRefs(await readStdin());
529
+ const { prompt, transcriptPath } = parsePayload(await readStdin());
530
+ const [ref] = extractTicketRefs(prompt ?? "");
254
531
  if (!ref) return;
255
532
  const existing = readSession();
256
533
  if (existing?.ticket === ref) return;
534
+ const transcript = transcriptPath ?? claudeTranscriptPath();
257
535
  const result = await post(credentials.url, "/api/agent/events", {
258
536
  token: credentials.token,
259
537
  event: "start",
260
538
  ticketRef: ref,
261
539
  tool: "claude-code"
262
540
  });
263
- if (existing) await post(credentials.url, "/api/agent/events", {
264
- token: credentials.token,
265
- event: "stop",
266
- sessionKey: existing.sessionKey
267
- }).catch(() => void 0);
541
+ if (existing) await stopSession(credentials, existing, transcript);
542
+ const transcriptOffset = transcript ? transcriptSize(transcript) ?? void 0 : void 0;
268
543
  writeSession({
269
544
  sessionKey: result.sessionKey,
270
545
  ticket: ref,
271
- startedAt: Date.now()
546
+ startedAt: Date.now(),
547
+ transcriptOffset
272
548
  });
273
549
  success(`Tracking work on ${bold(ref)}`);
274
550
  } catch {}
@@ -625,175 +901,6 @@ async function logout() {
625
901
  info(dim(" Could not reach Neuvikon to revoke the token — revoke it in settings if it leaked."));
626
902
  }
627
903
  //#endregion
628
- //#region src/usage.ts
629
- /**
630
- * Sums token usage out of a JSONL agent transcript, starting from `offset`.
631
- *
632
- * Reading incrementally is what makes a per-turn hook safe to call repeatedly:
633
- * each call reports only what was appended since the last one. Anything that
634
- * doesn't parse is skipped rather than fatal — a transcript being appended to
635
- * while we read it will have a torn final line, and that is normal, not an
636
- * error worth failing a user's turn over.
637
- */
638
- /**
639
- * Where the transcript currently ends, so a session that starts mid-conversation
640
- * can mark its starting point instead of counting what came before it.
641
- */
642
- function transcriptSize(path) {
643
- let fd;
644
- try {
645
- fd = openSync(path, "r");
646
- } catch {
647
- return null;
648
- }
649
- try {
650
- return fstatSync(fd).size;
651
- } catch {
652
- return null;
653
- } finally {
654
- closeSync(fd);
655
- }
656
- }
657
- function readUsageSince(path, offset) {
658
- let fd;
659
- try {
660
- fd = openSync(path, "r");
661
- } catch {
662
- return null;
663
- }
664
- try {
665
- const size = fstatSync(fd).size;
666
- const from = offset > size ? 0 : offset;
667
- if (size === from) return {
668
- tokensIn: 0,
669
- tokensOut: 0,
670
- offset: size
671
- };
672
- const buffer = Buffer.alloc(size - from);
673
- readSync(fd, buffer, 0, buffer.length, from);
674
- const text = buffer.toString("utf8");
675
- const lastNewline = text.lastIndexOf("\n");
676
- if (lastNewline === -1) return {
677
- tokensIn: 0,
678
- tokensOut: 0,
679
- offset: from
680
- };
681
- let tokensIn = 0;
682
- let tokensOut = 0;
683
- for (const line of text.slice(0, lastNewline).split("\n")) {
684
- if (!line.trim()) continue;
685
- let row;
686
- try {
687
- row = JSON.parse(line);
688
- } catch {
689
- continue;
690
- }
691
- const usage = extractUsage(row);
692
- if (!usage) continue;
693
- tokensIn += usage.tokensIn;
694
- tokensOut += usage.tokensOut;
695
- }
696
- return {
697
- tokensIn,
698
- tokensOut,
699
- offset: from + Buffer.byteLength(text.slice(0, lastNewline + 1))
700
- };
701
- } catch {
702
- return null;
703
- } finally {
704
- closeSync(fd);
705
- }
706
- }
707
- function toCount$1(value) {
708
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
709
- }
710
- function extractUsage(row) {
711
- if (typeof row !== "object" || row === null) return null;
712
- const message = row.message;
713
- if (typeof message !== "object" || message === null) return null;
714
- const usage = message.usage;
715
- if (typeof usage !== "object" || usage === null) return null;
716
- const fields = usage;
717
- return {
718
- tokensIn: toCount$1(fields.input_tokens) + toCount$1(fields.cache_read_input_tokens) + toCount$1(fields.cache_creation_input_tokens),
719
- tokensOut: toCount$1(fields.output_tokens)
720
- };
721
- }
722
- //#endregion
723
- //#region src/commands/report.ts
724
- /**
725
- * Agent hooks hand their context to the command on stdin as JSON. Reading the
726
- * transcript path from there means a hook is a bare `npx neuvikon report` with
727
- * nothing to wire up, and nothing to keep in sync when a tool renames a flag.
728
- */
729
- async function transcriptFromHookPayload() {
730
- const raw = await readStdin();
731
- if (!raw.trim()) return void 0;
732
- try {
733
- const payload = JSON.parse(raw);
734
- if (typeof payload !== "object" || payload === null) return void 0;
735
- const path = payload.transcript_path;
736
- return typeof path === "string" && path ? path : void 0;
737
- } catch {
738
- return;
739
- }
740
- }
741
- function toCount(raw) {
742
- if (raw === void 0) return void 0;
743
- const value = Number(raw);
744
- return Number.isFinite(value) && value >= 0 ? value : void 0;
745
- }
746
- /**
747
- * Called from an agent hook on every turn, so it is deliberately silent and
748
- * never exits non-zero: a token count is not worth interrupting someone's work
749
- * over, and a hook that fails loudly is a hook people uninstall.
750
- */
751
- async function report(flags) {
752
- const credentials = readCredentials();
753
- const session = readSession();
754
- if (!credentials || !session) {
755
- if (flags.has("verbose")) info(dim(credentials ? "No ticket is being tracked here." : "Not logged in."));
756
- return;
757
- }
758
- let tokensIn = toCount(flags.get("tokens-in"));
759
- let tokensOut = toCount(flags.get("tokens-out"));
760
- let nextOffset;
761
- const transcript = flags.get("transcript") || await transcriptFromHookPayload();
762
- if (transcript) {
763
- const baseline = session.transcriptOffset ?? transcriptSize(transcript);
764
- if (session.transcriptOffset === void 0 && baseline !== null) writeSession({
765
- ...session,
766
- transcriptOffset: baseline
767
- });
768
- const usage = baseline === null ? null : readUsageSince(transcript, baseline);
769
- if (usage) {
770
- tokensIn = (tokensIn ?? 0) + usage.tokensIn;
771
- tokensOut = (tokensOut ?? 0) + usage.tokensOut;
772
- nextOffset = usage.offset;
773
- }
774
- }
775
- if (!tokensIn && !tokensOut) {
776
- if (flags.has("verbose")) info(dim("No new usage to report."));
777
- return;
778
- }
779
- try {
780
- await post(credentials.url, "/api/agent/events", {
781
- token: credentials.token,
782
- event: "report",
783
- sessionKey: session.sessionKey,
784
- tokensIn,
785
- tokensOut
786
- });
787
- } catch {
788
- return;
789
- }
790
- if (nextOffset !== void 0) writeSession({
791
- ...session,
792
- transcriptOffset: nextOffset
793
- });
794
- if (flags.has("verbose")) info(dim(`Reported ${tokensIn ?? 0} in / ${tokensOut ?? 0} out`));
795
- }
796
- //#endregion
797
904
  //#region src/commands/start.ts
798
905
  const TOOLS = /* @__PURE__ */ new Set([
799
906
  "claude-code",
@@ -812,17 +919,25 @@ async function resolveRef(flags, positional) {
812
919
  if (refs.length === 0) fail("No ticket reference found.", "Pass one with --ticket EWWQ-1, or pipe the ticket text in.");
813
920
  return refs[0];
814
921
  }
922
+ /**
923
+ * An agent shelling out to `start` rarely passes `--tool`, and "other" is a
924
+ * worse answer than the one the environment already gives us.
925
+ */
926
+ function detectTool() {
927
+ return process.env.CLAUDE_CODE_SESSION_ID ? "claude-code" : "other";
928
+ }
815
929
  async function start(flags, positional) {
816
930
  const credentials = readCredentials();
817
931
  if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
818
932
  const ref = await resolveRef(flags, positional);
819
- const tool = flags.get("tool") ?? "other";
933
+ const tool = flags.get("tool") ?? detectTool();
820
934
  if (!TOOLS.has(tool)) fail(`Unknown tool "${tool}".`, `Expected one of: ${[...TOOLS].join(", ")}.`);
821
935
  const existing = readSession();
822
936
  if (existing?.ticket === ref) {
823
937
  info(dim(`Already tracking ${ref}.`));
824
938
  return;
825
939
  }
940
+ const transcript = flags.get("transcript") || claudeTranscriptPath();
826
941
  let result;
827
942
  try {
828
943
  result = await post(credentials.url, "/api/agent/events", {
@@ -839,15 +954,12 @@ async function start(flags, positional) {
839
954
  }
840
955
  throw error;
841
956
  }
842
- if (existing) await post(credentials.url, "/api/agent/events", {
843
- token: credentials.token,
844
- event: "stop",
845
- sessionKey: existing.sessionKey
846
- }).catch(() => void 0);
957
+ if (existing) await stopSession(credentials, existing, transcript);
847
958
  writeSession({
848
959
  sessionKey: result.sessionKey,
849
960
  ticket: ref,
850
- startedAt: Date.now()
961
+ startedAt: Date.now(),
962
+ transcriptOffset: transcript ? transcriptSize(transcript) ?? void 0 : void 0
851
963
  });
852
964
  success(`Tracking work on ${bold(ref)}`);
853
965
  }
@@ -871,18 +983,14 @@ function status() {
871
983
  }
872
984
  //#endregion
873
985
  //#region src/commands/stop.ts
874
- async function stop() {
986
+ async function stop(flags) {
875
987
  const session = readSession();
876
988
  if (!session) {
877
989
  info(dim("No ticket is being tracked here."));
878
990
  return;
879
991
  }
880
992
  const credentials = readCredentials();
881
- if (credentials) await post(credentials.url, "/api/agent/events", {
882
- token: credentials.token,
883
- event: "stop",
884
- sessionKey: session.sessionKey
885
- }).catch(() => void 0);
993
+ if (credentials) await stopSession(credentials, session, flags.get("transcript") || claudeTranscriptPath());
886
994
  clearSession();
887
995
  success(`Stopped tracking ${session.ticket}`);
888
996
  }
@@ -943,7 +1051,7 @@ async function main() {
943
1051
  case "start": return start(flags, positional);
944
1052
  case "hook-start": return hookStart();
945
1053
  case "report": return report(flags);
946
- case "stop": return stop();
1054
+ case "stop": return stop(flags);
947
1055
  case "status": return status();
948
1056
  case void 0:
949
1057
  case "help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuvikon",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Tell Neuvikon which ticket your coding agent is working on.",
5
5
  "keywords": [
6
6
  "neuvikon",