neuvikon 0.2.3 → 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,12 +66,16 @@ 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
  | | |
71
75
  |---|---|
72
76
  | `~/.neuvikon/config.json` | Token and deployment URL, `0600` |
73
77
  | `.neuvikon-session` | Which ticket this directory is tracking. Git-ignored by `init` |
78
+ | `--dev` | Target the development deployment instead of production |
74
79
  | `NEUVIKON_URL` | Point at a self-hosted deployment. `--url` does the same per command |
75
80
  | `NEUVIKON_TOKEN` | Use this token instead of logging in — for CI and anywhere a browser cannot open |
76
81
 
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;
@@ -142,14 +199,35 @@ function isInstalled(root) {
142
199
  //#endregion
143
200
  //#region src/config.ts
144
201
  /**
145
- * The deployment the CLI talks to. Overridable so self-hosted installs and
146
- * local Convex dev deployments work without a separate build.
147
- *
148
- * MUST point at the production Convex deployment before publishing: every
149
- * installed copy of this CLI hardcodes whatever ships here, and a dev
150
- * deployment can be reset out from under them.
202
+ * The two deployments this CLI knows about. The browser origin is kept here
203
+ * rather than read back from the server, because the API answers on a Convex
204
+ * domain while approval happens on the web app's — one is not derivable from
205
+ * the other, and guessing wrong sends people to a page that does not exist.
206
+ */
207
+ const DEPLOYMENTS = {
208
+ production: {
209
+ api: "https://flexible-bird-801.eu-west-1.convex.site",
210
+ web: "https://www.neuvikon.space"
211
+ },
212
+ development: {
213
+ api: "https://calm-sturgeon-831.eu-west-1.convex.site",
214
+ web: "http://localhost:3000"
215
+ }
216
+ };
217
+ /**
218
+ * `--dev` targets the development deployment; NEUVIKON_URL overrides the API
219
+ * for a self-hosted install, in which case the web origin is unknowable and
220
+ * falls back to the matching default.
151
221
  */
152
- const DEFAULT_SITE_URL = process.env.NEUVIKON_URL ?? "https://flexible-bird-801.eu-west-1.convex.sites";
222
+ function resolveDeployment(flags) {
223
+ const base = flags.has("dev") ? DEPLOYMENTS.development : DEPLOYMENTS.production;
224
+ const override = flags.get("url") || process.env.NEUVIKON_URL;
225
+ return override ? {
226
+ api: override.trim().replace(/\/+$/, ""),
227
+ web: base.web
228
+ } : base;
229
+ }
230
+ const DEFAULT_SITE_URL = process.env.NEUVIKON_URL ?? DEPLOYMENTS.production.api;
153
231
  const CONFIG_DIR = join(homedir(), ".neuvikon");
154
232
  const CONFIG_PATH = join(CONFIG_DIR, "config.json");
155
233
  /**
@@ -247,6 +325,7 @@ function fail(message, hint) {
247
325
  function init() {
248
326
  const results = installAgentInstructions(process.cwd());
249
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})`);
250
329
  else success(`${result.path} — ${result.action}`);
251
330
  if (!readCredentials()) warn("Not logged in yet — run `npx neuvikon login`.");
252
331
  }
@@ -317,7 +396,8 @@ function sleep(ms) {
317
396
  return new Promise((resolve) => setTimeout(resolve, ms));
318
397
  }
319
398
  async function login(flags) {
320
- const url = normalizeUrl(flags.get("url") ?? DEFAULT_SITE_URL);
399
+ const deployment = resolveDeployment(flags);
400
+ const url = normalizeUrl(deployment.api);
321
401
  let start;
322
402
  try {
323
403
  start = await post(url, "/api/cli/auth/start", {});
@@ -326,12 +406,13 @@ async function login(flags) {
326
406
  if (error instanceof ApiError) fail(error.message);
327
407
  throw error;
328
408
  }
409
+ const verifyUrl = `${deployment.web}/cli?code=${encodeURIComponent(start.userCode)}`;
329
410
  info("");
330
- info(` Open ${cyan(start.verifyUrl)}`);
411
+ info(` Open ${cyan(verifyUrl)}`);
331
412
  info(` and confirm this code: ${bold(start.userCode)}`);
332
413
  info("");
333
414
  info(dim(" Waiting for approval..."));
334
- openBrowser(start.verifyUrl);
415
+ openBrowser(verifyUrl);
335
416
  const interval = Math.max(1e3, start.intervalMs);
336
417
  for (;;) {
337
418
  if (Date.now() > start.expiresAt) fail("That login request expired.", "Run `npx neuvikon login` to get a fresh code.");
@@ -357,9 +438,12 @@ async function login(flags) {
357
438
  info("");
358
439
  success(`Logged in as ${bold(result.userName)} · ${result.organizationName}`);
359
440
  info(dim(` Token stored in ${CONFIG_PATH}`));
360
- 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");
361
444
  if (touched.length > 0) success(`Told your coding agent how to report work (${touched.map((e) => e.path).join(", ")})`);
362
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})`);
363
447
  info("");
364
448
  info(dim(" Paste a ticket into any coding agent and it will do the rest."));
365
449
  return;
@@ -714,6 +798,7 @@ ${bold("Options")}
714
798
  --tool <name> claude-code | codex | cursor | other
715
799
  --transcript <path> Read token usage from an agent transcript
716
800
  --tokens-in <n> --tokens-out <n>
801
+ --dev Use the development deployment
717
802
  --url <site-url> Point at a self-hosted deployment
718
803
 
719
804
  ${dim("Paste a ticket into any coding agent after logging in — it takes it from there.")}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuvikon",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Tell Neuvikon which ticket your coding agent is working on.",
5
5
  "keywords": [
6
6
  "neuvikon",