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 +6 -1
- package/dist/index.mjs +96 -11
- package/package.json +1 -1
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
|
|
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
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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.")}`;
|