neuvikon 0.2.4 → 0.2.5

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
@@ -49,7 +49,8 @@ pbpaste | npx neuvikon start
49
49
  ## Token usage
50
50
 
51
51
  `start`/`stop` are all an agent can report about itself — no agent knows its own
52
- token count. On Claude Code the transcript does, so a hook can report it:
52
+ token count. On Claude Code the transcript does, so `login`/`init` wire a hook
53
+ into `.claude/settings.json` for you, whenever that project already has one:
53
54
 
54
55
  ```jsonc
55
56
  // .claude/settings.json
@@ -65,6 +66,9 @@ token count. On Claude Code the transcript does, so a hook can report it:
65
66
  turn adds that turn rather than re-counting the conversation. It is silent and
66
67
  always exits 0 — a token count is never worth interrupting someone's work.
67
68
 
69
+ If `.claude/settings.json` doesn't exist yet, or another agent is the one
70
+ being used, add the block above by hand.
71
+
68
72
  ## Configuration
69
73
 
70
74
  | | |
package/dist/index.mjs CHANGED
@@ -3,6 +3,62 @@ import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readF
3
3
  import { dirname, join } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { spawn } from "node:child_process";
6
+ //#region src/agents/hooks.ts
7
+ const REPORT_COMMAND = "npx neuvikon report";
8
+ const STOP_COMMAND = "npx neuvikon stop";
9
+ function hasCommand(groups, command) {
10
+ return (groups ?? []).some((group) => group.hooks.some((hook) => hook.command === command));
11
+ }
12
+ function withCommand(groups, command) {
13
+ if (hasCommand(groups, command)) return groups;
14
+ return [...groups ?? [], { hooks: [{
15
+ type: "command",
16
+ command
17
+ }] }];
18
+ }
19
+ /**
20
+ * Wires token reporting into Claude Code's own hook system — the one agent
21
+ * whose transcript exposes usage, per `usage.ts`. `Stop` fires after every
22
+ * turn, exactly when `report` should read what the transcript gained since
23
+ * last time; `SessionEnd` is the backstop for `stop` when nobody remembered
24
+ * to run it by hand. Without this, `start`/`stop` are the only events an
25
+ * agent ever sends and token usage never leaves zero.
26
+ */
27
+ function installClaudeHooks(root) {
28
+ const relativePath = join(".claude", "settings.json");
29
+ const path = join(root, relativePath);
30
+ const raw = existsSync(path) ? readFileSync(path, "utf8") : "";
31
+ let settings;
32
+ try {
33
+ settings = raw.trim() ? JSON.parse(raw) : {};
34
+ } catch {
35
+ return {
36
+ path: relativePath,
37
+ action: "skipped",
38
+ reason: "settings.json is not valid JSON"
39
+ };
40
+ }
41
+ const hooks = settings.hooks ?? {};
42
+ if (hasCommand(hooks.Stop, REPORT_COMMAND) && hasCommand(hooks.SessionEnd, STOP_COMMAND)) return {
43
+ path: relativePath,
44
+ action: "unchanged"
45
+ };
46
+ const nextSettings = {
47
+ ...settings,
48
+ hooks: {
49
+ ...hooks,
50
+ Stop: withCommand(hooks.Stop, REPORT_COMMAND),
51
+ SessionEnd: withCommand(hooks.SessionEnd, STOP_COMMAND)
52
+ }
53
+ };
54
+ mkdirSync(dirname(path), { recursive: true });
55
+ writeFileSync(path, `${JSON.stringify(nextSettings, null, 2)}\n`);
56
+ return {
57
+ path: relativePath,
58
+ action: raw ? "updated" : "created"
59
+ };
60
+ }
61
+ //#endregion
6
62
  //#region src/agents/instructions.ts
7
63
  /**
8
64
  * The bridge between "user pasted a ticket" and "Neuvikon knows about it".
@@ -123,6 +179,7 @@ function installAgentInstructions(root) {
123
179
  if (existsSync(path)) results.push(writeMarkdownTarget(root, target));
124
180
  }
125
181
  if (existsSync(join(root, ".cursor"))) results.push(writeCursorRule(root));
182
+ if (existsSync(join(root, ".claude"))) results.push(installClaudeHooks(root));
126
183
  const gitignore = ignoreSessionFile(root);
127
184
  if (gitignore) results.push(gitignore);
128
185
  return results;
@@ -268,6 +325,7 @@ function fail(message, hint) {
268
325
  function init() {
269
326
  const results = installAgentInstructions(process.cwd());
270
327
  for (const result of results) if (result.action === "unchanged") info(dim(` ${result.path} — already up to date`));
328
+ else if (result.action === "skipped") warn(`${result.path} — skipped (${result.reason})`);
271
329
  else success(`${result.path} — ${result.action}`);
272
330
  if (!readCredentials()) warn("Not logged in yet — run `npx neuvikon login`.");
273
331
  }
@@ -380,9 +438,12 @@ async function login(flags) {
380
438
  info("");
381
439
  success(`Logged in as ${bold(result.userName)} · ${result.organizationName}`);
382
440
  info(dim(` Token stored in ${CONFIG_PATH}`));
383
- const touched = installAgentInstructions(process.cwd()).filter((entry) => entry.action !== "unchanged");
441
+ const installed = installAgentInstructions(process.cwd());
442
+ const touched = installed.filter((entry) => entry.action === "created" || entry.action === "updated");
443
+ const skipped = installed.filter((entry) => entry.action === "skipped");
384
444
  if (touched.length > 0) success(`Told your coding agent how to report work (${touched.map((e) => e.path).join(", ")})`);
385
445
  else success("Your coding agent already knows how to report work");
446
+ for (const entry of skipped) if (entry.action === "skipped") warn(`${entry.path} — skipped (${entry.reason})`);
386
447
  info("");
387
448
  info(dim(" Paste a ticket into any coding agent and it will do the rest."));
388
449
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuvikon",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Tell Neuvikon which ticket your coding agent is working on.",
5
5
  "keywords": [
6
6
  "neuvikon",