neuvikon 0.2.14 → 0.2.16

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 +1 -1
  2. package/dist/index.mjs +232 -76
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # neuvikon
2
2
 
3
- Tells [Neuvikon](https://neuvikon.com) which ticket your coding agent is working
3
+ Tells [Neuvikon](https://neuvikon.space) which ticket your coding agent is working
4
4
  on, and how many tokens it spent doing it.
5
5
 
6
6
  Works with any agent that reads a rules file and has a shell — Claude Code,
package/dist/index.mjs CHANGED
@@ -3,6 +3,49 @@ import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readF
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { spawn } from "node:child_process";
6
+ //#region src/api.ts
7
+ var ApiError = class extends Error {
8
+ status;
9
+ constructor(message, status) {
10
+ super(message);
11
+ this.status = status;
12
+ this.name = "ApiError";
13
+ }
14
+ };
15
+ /** A request that never reached the server: offline, DNS, TLS, timeout. */
16
+ var NetworkError = class extends Error {
17
+ constructor(cause) {
18
+ super("Could not reach Neuvikon", { cause });
19
+ this.name = "NetworkError";
20
+ }
21
+ };
22
+ const TIMEOUT_MS = 15e3;
23
+ /**
24
+ * The one place an HTTP failure becomes a typed error. Callers decide what a
25
+ * failure means — a hook wrapping the agent's turn wants to swallow it, `login`
26
+ * wants to shout about it.
27
+ */
28
+ async function post(url, path, body) {
29
+ let response;
30
+ try {
31
+ response = await fetch(`${url}${path}`, {
32
+ method: "POST",
33
+ headers: { "content-type": "application/json" },
34
+ body: JSON.stringify(body),
35
+ signal: AbortSignal.timeout(TIMEOUT_MS)
36
+ });
37
+ } catch (error) {
38
+ throw new NetworkError(error);
39
+ }
40
+ const payload = await response.json().catch(() => ({}));
41
+ const error = typeof payload === "object" && payload !== null ? payload.error : void 0;
42
+ if (!response.ok) throw new ApiError(typeof error === "string" ? error : `Request failed with status ${response.status}`, response.status);
43
+ return payload;
44
+ }
45
+ function normalizeUrl(url) {
46
+ return url.trim().replace(/\/+$/, "");
47
+ }
48
+ //#endregion
6
49
  //#region src/config.ts
7
50
  /**
8
51
  * The two deployments this CLI knows about. The browser origin is kept here
@@ -93,49 +136,6 @@ function clearCredentials() {
93
136
  }
94
137
  }
95
138
  //#endregion
96
- //#region src/api.ts
97
- var ApiError = class extends Error {
98
- status;
99
- constructor(message, status) {
100
- super(message);
101
- this.status = status;
102
- this.name = "ApiError";
103
- }
104
- };
105
- /** A request that never reached the server: offline, DNS, TLS, timeout. */
106
- var NetworkError = class extends Error {
107
- constructor(cause) {
108
- super("Could not reach Neuvikon", { cause });
109
- this.name = "NetworkError";
110
- }
111
- };
112
- const TIMEOUT_MS = 15e3;
113
- /**
114
- * The one place an HTTP failure becomes a typed error. Callers decide what a
115
- * failure means — a hook wrapping the agent's turn wants to swallow it, `login`
116
- * wants to shout about it.
117
- */
118
- async function post(url, path, body) {
119
- let response;
120
- try {
121
- response = await fetch(`${url}${path}`, {
122
- method: "POST",
123
- headers: { "content-type": "application/json" },
124
- body: JSON.stringify(body),
125
- signal: AbortSignal.timeout(TIMEOUT_MS)
126
- });
127
- } catch (error) {
128
- throw new NetworkError(error);
129
- }
130
- const payload = await response.json().catch(() => ({}));
131
- const error = typeof payload === "object" && payload !== null ? payload.error : void 0;
132
- if (!response.ok) throw new ApiError(typeof error === "string" ? error : `Request failed with status ${response.status}`, response.status);
133
- return payload;
134
- }
135
- function normalizeUrl(url) {
136
- return url.trim().replace(/\/+$/, "");
137
- }
138
- //#endregion
139
139
  //#region src/session.ts
140
140
  /**
141
141
  * Distinguishes concurrent agents working in the *same* checkout — two Claude
@@ -184,6 +184,74 @@ function clearSession() {
184
184
  } catch {}
185
185
  }
186
186
  //#endregion
187
+ //#region src/ui.ts
188
+ /**
189
+ * Output helpers. Everything the agent shouldn't parse goes to stderr, so a
190
+ * command's stdout stays a clean, pipeable value.
191
+ */
192
+ const useColor = process.env.NO_COLOR === void 0 && process.stderr.isTTY === true;
193
+ function paint(code, text) {
194
+ return useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
195
+ }
196
+ const dim = (text) => paint("2", text);
197
+ const bold = (text) => paint("1", text);
198
+ const green = (text) => paint("32", text);
199
+ const red = (text) => paint("31", text);
200
+ const cyan = (text) => paint("36", text);
201
+ function info(message) {
202
+ process.stderr.write(`${message}\n`);
203
+ }
204
+ function success(message) {
205
+ process.stderr.write(`${green("✓")} ${message}\n`);
206
+ }
207
+ function warn(message) {
208
+ process.stderr.write(`${red("!")} ${message}\n`);
209
+ }
210
+ /** Ends the process. Every user-facing failure funnels through here. */
211
+ function fail(message, hint) {
212
+ process.stderr.write(`${red("✗")} ${message}\n`);
213
+ if (hint) process.stderr.write(` ${dim(hint)}\n`);
214
+ process.exit(1);
215
+ }
216
+ //#endregion
217
+ //#region src/commands/board.ts
218
+ function requireCredentials$3() {
219
+ const credentials = readCredentials();
220
+ if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
221
+ return credentials;
222
+ }
223
+ function resolveTicketRef$3(flags) {
224
+ const ref = flags.get("ticket") ?? readSession()?.ticket;
225
+ if (!ref) fail("No ticket to look up a board for.", "Pass --ticket EWWQ-1, or run this where `npx neuvikon start` is already tracking one.");
226
+ return ref;
227
+ }
228
+ /**
229
+ * Shows an agent the real shape of this project's board before it guesses at
230
+ * a column name — boards vary per project, and some have no "done" at all.
231
+ */
232
+ async function board(flags) {
233
+ const credentials = requireCredentials$3();
234
+ const ticketRef = resolveTicketRef$3(flags);
235
+ let result;
236
+ try {
237
+ result = await post(credentials.url, "/api/agent/board", {
238
+ token: credentials.token,
239
+ ticketRef
240
+ });
241
+ } catch (error) {
242
+ if (error instanceof NetworkError) fail("Could not reach Neuvikon.");
243
+ if (error instanceof ApiError) {
244
+ if (error.status === 401) fail(error.message, "Run `npx neuvikon login` again.");
245
+ fail(error.message);
246
+ }
247
+ throw error;
248
+ }
249
+ info(`${ticketRef} is on ${bold(result.currentColumn || "(no column)")}`);
250
+ info(dim("Columns, in board order:"));
251
+ for (const column of result.columns) info(` - ${column.name}${column.isDone ? dim(" (terminal)") : ""}`);
252
+ if (!result.columns.some((column) => column.isDone)) info(dim("No column is marked terminal on this board — use your judgment on what counts as done."));
253
+ }
254
+ //#endregion
187
255
  //#region src/ticket.ts
188
256
  /**
189
257
  * Ticket refs look like `AWKN-14`. This pattern is deliberately looser than the
@@ -209,6 +277,40 @@ async function readStdin() {
209
277
  return Buffer.concat(chunks).toString("utf8");
210
278
  }
211
279
  //#endregion
280
+ //#region src/commands/comment.ts
281
+ function requireCredentials$2() {
282
+ const credentials = readCredentials();
283
+ if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
284
+ return credentials;
285
+ }
286
+ function resolveTicketRef$2(flags) {
287
+ const ref = flags.get("ticket") ?? readSession()?.ticket;
288
+ if (!ref) fail("No ticket to comment on.", "Pass --ticket EWWQ-1, or run this where `npx neuvikon start` is already tracking one.");
289
+ return ref;
290
+ }
291
+ /** A note on a ticket that doesn't move it — e.g. explaining a handoff mid-ticket. */
292
+ async function comment(flags) {
293
+ const credentials = requireCredentials$2();
294
+ const ticketRef = resolveTicketRef$2(flags);
295
+ const text = (flags.get("text") || await readStdin()).trim();
296
+ if (!text) fail("No comment text given.", "Pipe the comment in on stdin, or pass --text.");
297
+ try {
298
+ await post(credentials.url, "/api/agent/comment", {
299
+ token: credentials.token,
300
+ ticketRef,
301
+ comment: text
302
+ });
303
+ } catch (error) {
304
+ if (error instanceof NetworkError) fail("Could not reach Neuvikon.");
305
+ if (error instanceof ApiError) {
306
+ if (error.status === 401) fail(error.message, "Run `npx neuvikon login` again.");
307
+ fail(error.message);
308
+ }
309
+ throw error;
310
+ }
311
+ success(`Commented on ${ticketRef}`);
312
+ }
313
+ //#endregion
212
314
  //#region src/usage.ts
213
315
  /**
214
316
  * Sums token usage out of a JSONL agent transcript, starting from `offset`.
@@ -336,36 +438,6 @@ function claudeTranscriptPath() {
336
438
  }
337
439
  }
338
440
  //#endregion
339
- //#region src/ui.ts
340
- /**
341
- * Output helpers. Everything the agent shouldn't parse goes to stderr, so a
342
- * command's stdout stays a clean, pipeable value.
343
- */
344
- const useColor = process.env.NO_COLOR === void 0 && process.stderr.isTTY === true;
345
- function paint(code, text) {
346
- return useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
347
- }
348
- const dim = (text) => paint("2", text);
349
- const bold = (text) => paint("1", text);
350
- const green = (text) => paint("32", text);
351
- const red = (text) => paint("31", text);
352
- const cyan = (text) => paint("36", text);
353
- function info(message) {
354
- process.stderr.write(`${message}\n`);
355
- }
356
- function success(message) {
357
- process.stderr.write(`${green("✓")} ${message}\n`);
358
- }
359
- function warn(message) {
360
- process.stderr.write(`${red("!")} ${message}\n`);
361
- }
362
- /** Ends the process. Every user-facing failure funnels through here. */
363
- function fail(message, hint) {
364
- process.stderr.write(`${red("✗")} ${message}\n`);
365
- if (hint) process.stderr.write(` ${dim(hint)}\n`);
366
- process.exit(1);
367
- }
368
- //#endregion
369
441
  //#region src/commands/report.ts
370
442
  /**
371
443
  * Agent hooks hand their context to the command on stdin as JSON. Reading the
@@ -659,12 +731,42 @@ creating a duplicate, so reuse a title for anything you're extending.
659
731
  Reference another page by writing its title in double brackets, e.g.
660
732
  \`[[Auth Flow]]\` — it becomes a real link if that page already exists.
661
733
 
662
- When you finish that ticket, or the user moves you to a different one, run:
734
+ Before you change the ticket's status, look at what columns this project's
735
+ board actually has — they vary per project, and there may be no column
736
+ literally called "Done":
737
+
738
+ npx neuvikon board --ticket EWWQ-1
739
+
740
+ Then decide, based on what's actually true, not on finishing your turn:
741
+
742
+ - The work is really done — merged to main, deployed, whatever "done" means
743
+ for this project — move it to that board's terminal column (the one
744
+ \`board\` marks as terminal), and say why:
745
+
746
+ npx neuvikon move --ticket EWWQ-1 --column "Done" --comment "Merged to main in #142"
747
+
748
+ - The work needs a step before done — a PR is open, it's waiting on review,
749
+ it's blocked — move it to whichever real column matches that, with the
750
+ same reasoning:
751
+
752
+ npx neuvikon move --ticket EWWQ-1 --column "Code Review" --comment "PR up, waiting on review"
753
+
754
+ - If the board has no column that fits, or you're just leaving a note without
755
+ changing where the ticket sits (e.g. handing off mid-ticket), comment
756
+ instead of moving:
757
+
758
+ npx neuvikon comment --ticket EWWQ-1 --text "Handing off — blocked on design"
759
+
760
+ A ticket that isn't actually finished should never land on a terminal column
761
+ just because you're done for now. When you finish that ticket, or the user
762
+ moves you to a different one, also run:
663
763
 
664
764
  npx neuvikon stop
665
765
 
666
- This is bookkeeping that tells the user's team which ticket you are on. You do
667
- not need to narrate these commands, and if one fails, carry on with the actual
766
+ \`stop\` only tells the team you're no longer working on it — it does not move
767
+ the ticket, since stopping and finishing are not the same thing. This is
768
+ bookkeeping that tells the user's team which ticket you are on. You do not
769
+ need to narrate these commands, and if one fails, carry on with the actual
668
770
  work — never let it block you.`;
669
771
  function instructionBlock() {
670
772
  return `${START_MARKER}\n${INSTRUCTION_BODY}\n${END_MARKER}`;
@@ -926,6 +1028,49 @@ async function logout() {
926
1028
  info(dim(" Could not reach Neuvikon to revoke the token — revoke it in settings if it leaked."));
927
1029
  }
928
1030
  //#endregion
1031
+ //#region src/commands/move.ts
1032
+ function requireCredentials$1() {
1033
+ const credentials = readCredentials();
1034
+ if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
1035
+ return credentials;
1036
+ }
1037
+ function resolveTicketRef$1(flags) {
1038
+ const ref = flags.get("ticket") ?? readSession()?.ticket;
1039
+ if (!ref) fail("No ticket to move.", "Pass --ticket EWWQ-1, or run this where `npx neuvikon start` is already tracking one.");
1040
+ return ref;
1041
+ }
1042
+ /**
1043
+ * Explicit column change, e.g. `npx neuvikon move --column "Code Review" \
1044
+ * --comment "PR up, waiting on review"`. A comment is mandatory: this is the
1045
+ * command that lands a ticket on a done-like column, and it should never do
1046
+ * that silently.
1047
+ */
1048
+ async function move(flags) {
1049
+ const credentials = requireCredentials$1();
1050
+ const ticketRef = resolveTicketRef$1(flags);
1051
+ const column = flags.get("column");
1052
+ if (!column) fail("A target column is required.", `Pass --column "Code Review" — run \`npx neuvikon board\` first if you're not sure what this project calls it.`);
1053
+ const comment = (flags.get("comment") || await readStdin()).trim();
1054
+ if (!comment) fail("A comment is required.", "Pass --comment \"why this ticket is moving\", e.g. \"Merged to main.\"");
1055
+ let result;
1056
+ try {
1057
+ result = await post(credentials.url, "/api/agent/move", {
1058
+ token: credentials.token,
1059
+ ticketRef,
1060
+ column,
1061
+ comment
1062
+ });
1063
+ } catch (error) {
1064
+ if (error instanceof NetworkError) fail("Could not reach Neuvikon.");
1065
+ if (error instanceof ApiError) {
1066
+ if (error.status === 401) fail(error.message, "Run `npx neuvikon login` again.");
1067
+ fail(error.message);
1068
+ }
1069
+ throw error;
1070
+ }
1071
+ success(`Moved ${ticketRef} to ${bold(result.column)}`);
1072
+ }
1073
+ //#endregion
929
1074
  //#region src/commands/pages.ts
930
1075
  function requireCredentials() {
931
1076
  const credentials = readCredentials();
@@ -1094,6 +1239,14 @@ ${bold("Usage")}
1094
1239
  npx neuvikon status Show login, project setup and current ticket
1095
1240
  npx neuvikon logout
1096
1241
 
1242
+ ${bold("Board")}
1243
+ npx neuvikon board Show this project's columns and where the ticket sits
1244
+ npx neuvikon move --column "Code Review" --comment "PR up, waiting on review"
1245
+ Move the ticket to a specific column, with why
1246
+ npx neuvikon comment --text "Handing off, blocked on design"
1247
+ Leave a note without changing the column
1248
+ All three accept --ticket EWWQ-1 instead of relying on a tracked session.
1249
+
1097
1250
  ${bold("Project pages")}
1098
1251
  npx neuvikon pages List the current ticket's project pages
1099
1252
  npx neuvikon pages update "Title" < notes.txt
@@ -1149,6 +1302,9 @@ async function main() {
1149
1302
  case "stop": return stop(flags);
1150
1303
  case "status": return status();
1151
1304
  case "pages": return pages(flags, positional);
1305
+ case "board": return board(flags);
1306
+ case "move": return move(flags);
1307
+ case "comment": return comment(flags);
1152
1308
  case void 0:
1153
1309
  case "help":
1154
1310
  case "--help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuvikon",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "Tell Neuvikon which ticket your coding agent is working on.",
5
5
  "keywords": [
6
6
  "neuvikon",