neuvikon 0.2.9 → 0.2.11
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/dist/index.mjs +145 -134
- package/package.json +4 -1
package/dist/index.mjs
CHANGED
|
@@ -238,7 +238,7 @@ function transcriptSize(path) {
|
|
|
238
238
|
closeSync(fd);
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
-
function readUsageSince(path, offset) {
|
|
241
|
+
function readUsageSince(path, offset, since) {
|
|
242
242
|
let fd;
|
|
243
243
|
try {
|
|
244
244
|
fd = openSync(path, "r");
|
|
@@ -251,7 +251,8 @@ function readUsageSince(path, offset) {
|
|
|
251
251
|
if (size === from) return {
|
|
252
252
|
tokensIn: 0,
|
|
253
253
|
tokensOut: 0,
|
|
254
|
-
offset: size
|
|
254
|
+
offset: size,
|
|
255
|
+
lastMessageId: since
|
|
255
256
|
};
|
|
256
257
|
const buffer = Buffer.alloc(size - from);
|
|
257
258
|
readSync(fd, buffer, 0, buffer.length, from);
|
|
@@ -260,10 +261,12 @@ function readUsageSince(path, offset) {
|
|
|
260
261
|
if (lastNewline === -1) return {
|
|
261
262
|
tokensIn: 0,
|
|
262
263
|
tokensOut: 0,
|
|
263
|
-
offset: from
|
|
264
|
+
offset: from,
|
|
265
|
+
lastMessageId: since
|
|
264
266
|
};
|
|
265
267
|
let tokensIn = 0;
|
|
266
268
|
let tokensOut = 0;
|
|
269
|
+
let lastMessageId = since;
|
|
267
270
|
for (const line of text.slice(0, lastNewline).split("\n")) {
|
|
268
271
|
if (!line.trim()) continue;
|
|
269
272
|
let row;
|
|
@@ -274,13 +277,16 @@ function readUsageSince(path, offset) {
|
|
|
274
277
|
}
|
|
275
278
|
const usage = extractUsage(row);
|
|
276
279
|
if (!usage) continue;
|
|
280
|
+
if (usage.messageId !== void 0 && usage.messageId === lastMessageId) continue;
|
|
281
|
+
lastMessageId = usage.messageId ?? lastMessageId;
|
|
277
282
|
tokensIn += usage.tokensIn;
|
|
278
283
|
tokensOut += usage.tokensOut;
|
|
279
284
|
}
|
|
280
285
|
return {
|
|
281
286
|
tokensIn,
|
|
282
287
|
tokensOut,
|
|
283
|
-
offset: from + Buffer.byteLength(text.slice(0, lastNewline + 1))
|
|
288
|
+
offset: from + Buffer.byteLength(text.slice(0, lastNewline + 1)),
|
|
289
|
+
lastMessageId
|
|
284
290
|
};
|
|
285
291
|
} catch {
|
|
286
292
|
return null;
|
|
@@ -297,9 +303,11 @@ function extractUsage(row) {
|
|
|
297
303
|
if (typeof message !== "object" || message === null) return null;
|
|
298
304
|
const usage = message.usage;
|
|
299
305
|
if (typeof usage !== "object" || usage === null) return null;
|
|
306
|
+
const id = message.id;
|
|
300
307
|
const fields = usage;
|
|
301
308
|
return {
|
|
302
|
-
|
|
309
|
+
messageId: typeof id === "string" && id ? id : void 0,
|
|
310
|
+
tokensIn: toCount$1(fields.input_tokens) + toCount$1(fields.cache_creation_input_tokens),
|
|
303
311
|
tokensOut: toCount$1(fields.output_tokens)
|
|
304
312
|
};
|
|
305
313
|
}
|
|
@@ -358,6 +366,133 @@ function fail(message, hint) {
|
|
|
358
366
|
process.exit(1);
|
|
359
367
|
}
|
|
360
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
|
|
361
496
|
//#region src/commands/hook-start.ts
|
|
362
497
|
/**
|
|
363
498
|
* `UserPromptSubmit` fires before the turn runs, which is what lets it capture
|
|
@@ -396,18 +531,14 @@ async function hookStart() {
|
|
|
396
531
|
if (!ref) return;
|
|
397
532
|
const existing = readSession();
|
|
398
533
|
if (existing?.ticket === ref) return;
|
|
534
|
+
const transcript = transcriptPath ?? claudeTranscriptPath();
|
|
399
535
|
const result = await post(credentials.url, "/api/agent/events", {
|
|
400
536
|
token: credentials.token,
|
|
401
537
|
event: "start",
|
|
402
538
|
ticketRef: ref,
|
|
403
539
|
tool: "claude-code"
|
|
404
540
|
});
|
|
405
|
-
if (existing) await
|
|
406
|
-
token: credentials.token,
|
|
407
|
-
event: "stop",
|
|
408
|
-
sessionKey: existing.sessionKey
|
|
409
|
-
}).catch(() => void 0);
|
|
410
|
-
const transcript = transcriptPath ?? claudeTranscriptPath();
|
|
541
|
+
if (existing) await stopSession(credentials, existing, transcript);
|
|
411
542
|
const transcriptOffset = transcript ? transcriptSize(transcript) ?? void 0 : void 0;
|
|
412
543
|
writeSession({
|
|
413
544
|
sessionKey: result.sessionKey,
|
|
@@ -770,113 +901,6 @@ async function logout() {
|
|
|
770
901
|
info(dim(" Could not reach Neuvikon to revoke the token — revoke it in settings if it leaked."));
|
|
771
902
|
}
|
|
772
903
|
//#endregion
|
|
773
|
-
//#region src/commands/report.ts
|
|
774
|
-
/**
|
|
775
|
-
* Agent hooks hand their context to the command on stdin as JSON. Reading the
|
|
776
|
-
* transcript path from there means a hook is a bare `npx neuvikon report` with
|
|
777
|
-
* nothing to wire up, and nothing to keep in sync when a tool renames a flag.
|
|
778
|
-
*/
|
|
779
|
-
async function transcriptFromHookPayload() {
|
|
780
|
-
const raw = await readStdin();
|
|
781
|
-
if (!raw.trim()) return void 0;
|
|
782
|
-
try {
|
|
783
|
-
const payload = JSON.parse(raw);
|
|
784
|
-
if (typeof payload !== "object" || payload === null) return void 0;
|
|
785
|
-
const path = payload.transcript_path;
|
|
786
|
-
return typeof path === "string" && path ? path : void 0;
|
|
787
|
-
} catch {
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
function toCount(raw) {
|
|
792
|
-
if (raw === void 0) return void 0;
|
|
793
|
-
const value = Number(raw);
|
|
794
|
-
return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
795
|
-
}
|
|
796
|
-
/**
|
|
797
|
-
* What the transcript has accumulated since this session's baseline.
|
|
798
|
-
*
|
|
799
|
-
* A session with no baseline yet establishes one here rather than counting
|
|
800
|
-
* from the top: everything before the ticket started belongs to whatever the
|
|
801
|
-
* agent was doing beforehand, not to this ticket.
|
|
802
|
-
*/
|
|
803
|
-
function pendingUsage(session, transcript) {
|
|
804
|
-
if (!transcript) return {
|
|
805
|
-
tokensIn: 0,
|
|
806
|
-
tokensOut: 0
|
|
807
|
-
};
|
|
808
|
-
let baseline = session.transcriptOffset;
|
|
809
|
-
if (baseline === void 0) {
|
|
810
|
-
baseline = transcriptSize(transcript) ?? void 0;
|
|
811
|
-
if (baseline !== void 0) writeSession({
|
|
812
|
-
...session,
|
|
813
|
-
transcriptOffset: baseline
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
if (baseline === void 0) return {
|
|
817
|
-
tokensIn: 0,
|
|
818
|
-
tokensOut: 0
|
|
819
|
-
};
|
|
820
|
-
const usage = readUsageSince(transcript, baseline);
|
|
821
|
-
if (!usage) return {
|
|
822
|
-
tokensIn: 0,
|
|
823
|
-
tokensOut: 0
|
|
824
|
-
};
|
|
825
|
-
return {
|
|
826
|
-
tokensIn: usage.tokensIn,
|
|
827
|
-
tokensOut: usage.tokensOut,
|
|
828
|
-
nextOffset: usage.offset
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
/**
|
|
832
|
-
* Hands a delta to the server, advancing the local offset only once it is
|
|
833
|
-
* accepted — a dropped request leaves the window to be retried by the next
|
|
834
|
-
* call instead of silently losing those tokens.
|
|
835
|
-
*/
|
|
836
|
-
async function postUsage(credentials, session, delta) {
|
|
837
|
-
try {
|
|
838
|
-
await post(credentials.url, "/api/agent/events", {
|
|
839
|
-
token: credentials.token,
|
|
840
|
-
event: "report",
|
|
841
|
-
sessionKey: session.sessionKey,
|
|
842
|
-
tokensIn: delta.tokensIn,
|
|
843
|
-
tokensOut: delta.tokensOut
|
|
844
|
-
});
|
|
845
|
-
} catch {
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
if (delta.nextOffset !== void 0) writeSession({
|
|
849
|
-
...session,
|
|
850
|
-
transcriptOffset: delta.nextOffset
|
|
851
|
-
});
|
|
852
|
-
return true;
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Called from an agent hook on every turn, so it is deliberately silent and
|
|
856
|
-
* never exits non-zero: a token count is not worth interrupting someone's work
|
|
857
|
-
* over, and a hook that fails loudly is a hook people uninstall.
|
|
858
|
-
*/
|
|
859
|
-
async function report(flags) {
|
|
860
|
-
const credentials = readCredentials();
|
|
861
|
-
const session = readSession();
|
|
862
|
-
if (!credentials || !session) {
|
|
863
|
-
if (flags.has("verbose")) info(dim(credentials ? "No ticket is being tracked here." : "Not logged in."));
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
const fromTranscript = pendingUsage(session, flags.get("transcript") || await transcriptFromHookPayload() || claudeTranscriptPath());
|
|
867
|
-
const delta = {
|
|
868
|
-
tokensIn: (toCount(flags.get("tokens-in")) ?? 0) + fromTranscript.tokensIn,
|
|
869
|
-
tokensOut: (toCount(flags.get("tokens-out")) ?? 0) + fromTranscript.tokensOut,
|
|
870
|
-
nextOffset: fromTranscript.nextOffset
|
|
871
|
-
};
|
|
872
|
-
if (!delta.tokensIn && !delta.tokensOut && !flags.has("heartbeat")) {
|
|
873
|
-
if (flags.has("verbose")) info(dim("No new usage to report."));
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
await postUsage(credentials, session, delta);
|
|
877
|
-
if (flags.has("verbose")) info(dim(`Reported ${delta.tokensIn} in / ${delta.tokensOut} out`));
|
|
878
|
-
}
|
|
879
|
-
//#endregion
|
|
880
904
|
//#region src/commands/start.ts
|
|
881
905
|
const TOOLS = /* @__PURE__ */ new Set([
|
|
882
906
|
"claude-code",
|
|
@@ -913,6 +937,7 @@ async function start(flags, positional) {
|
|
|
913
937
|
info(dim(`Already tracking ${ref}.`));
|
|
914
938
|
return;
|
|
915
939
|
}
|
|
940
|
+
const transcript = flags.get("transcript") || claudeTranscriptPath();
|
|
916
941
|
let result;
|
|
917
942
|
try {
|
|
918
943
|
result = await post(credentials.url, "/api/agent/events", {
|
|
@@ -929,12 +954,7 @@ async function start(flags, positional) {
|
|
|
929
954
|
}
|
|
930
955
|
throw error;
|
|
931
956
|
}
|
|
932
|
-
if (existing) await
|
|
933
|
-
token: credentials.token,
|
|
934
|
-
event: "stop",
|
|
935
|
-
sessionKey: existing.sessionKey
|
|
936
|
-
}).catch(() => void 0);
|
|
937
|
-
const transcript = flags.get("transcript") || claudeTranscriptPath();
|
|
957
|
+
if (existing) await stopSession(credentials, existing, transcript);
|
|
938
958
|
writeSession({
|
|
939
959
|
sessionKey: result.sessionKey,
|
|
940
960
|
ticket: ref,
|
|
@@ -970,16 +990,7 @@ async function stop(flags) {
|
|
|
970
990
|
return;
|
|
971
991
|
}
|
|
972
992
|
const credentials = readCredentials();
|
|
973
|
-
if (credentials)
|
|
974
|
-
const usage = pendingUsage(session, flags.get("transcript") || claudeTranscriptPath());
|
|
975
|
-
await post(credentials.url, "/api/agent/events", {
|
|
976
|
-
token: credentials.token,
|
|
977
|
-
event: "stop",
|
|
978
|
-
sessionKey: session.sessionKey,
|
|
979
|
-
tokensIn: usage.tokensIn,
|
|
980
|
-
tokensOut: usage.tokensOut
|
|
981
|
-
}).catch(() => void 0);
|
|
982
|
-
}
|
|
993
|
+
if (credentials) await stopSession(credentials, session, flags.get("transcript") || claudeTranscriptPath());
|
|
983
994
|
clearSession();
|
|
984
995
|
success(`Stopped tracking ${session.ticket}`);
|
|
985
996
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuvikon",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "Tell Neuvikon which ticket your coding agent is working on.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neuvikon",
|
|
@@ -40,5 +40,8 @@
|
|
|
40
40
|
"@types/node": "^26.2.0",
|
|
41
41
|
"tsdown": "^0.22.14",
|
|
42
42
|
"typescript": "^7.0.2"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"neuvikon": "link:"
|
|
43
46
|
}
|
|
44
47
|
}
|