neuvikon 0.2.13 → 0.2.15
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 +28 -1
- package/dist/index.mjs +322 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ as being worked on, by whom, with which tool.
|
|
|
39
39
|
| `neuvikon stop` | Stop tracking |
|
|
40
40
|
| `neuvikon status` | Login, project setup, current ticket |
|
|
41
41
|
| `neuvikon logout` | Revoke this machine's token and forget it |
|
|
42
|
+
| `neuvikon pages [--ticket EWWQ-1]` | List the ticket's project pages, flattened to plain text |
|
|
43
|
+
| `neuvikon pages update "Title" [--ticket EWWQ-1]` | Create or update a page by title, body on stdin (or `--text`) |
|
|
42
44
|
|
|
43
45
|
`start` also accepts the ticket on stdin, and pulls the reference out of it:
|
|
44
46
|
|
|
@@ -46,6 +48,32 @@ as being worked on, by whom, with which tool.
|
|
|
46
48
|
pbpaste | npx neuvikon start
|
|
47
49
|
```
|
|
48
50
|
|
|
51
|
+
## Project pages
|
|
52
|
+
|
|
53
|
+
An agent's most useful output often isn't the ticket, it's what it learned
|
|
54
|
+
doing it. `pages` reads and writes the same project pages a person sees in the
|
|
55
|
+
app — the running knowledge base for that project, not a per-ticket scratchpad:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npx neuvikon pages --ticket EWWQ-1
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npx neuvikon pages update "Auth flow" --ticket EWWQ-1 <<'EOF'
|
|
63
|
+
Sessions are issued by convex/auth.ts and expire after 30 days.
|
|
64
|
+
See [[Session storage]] for where they're persisted.
|
|
65
|
+
EOF
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
An update matches an existing page by title (case-insensitive) and overwrites
|
|
69
|
+
it, so writing to the same title again refines that page instead of piling up
|
|
70
|
+
duplicates. `[[Another Title]]` inside the text becomes a real link to that
|
|
71
|
+
page if a page with that title already exists in the project, same as typing
|
|
72
|
+
it in the editor.
|
|
73
|
+
|
|
74
|
+
Both commands fall back to whichever ticket `npx neuvikon start` is already
|
|
75
|
+
tracking in this directory when `--ticket` is omitted.
|
|
76
|
+
|
|
49
77
|
## Token usage
|
|
50
78
|
|
|
51
79
|
`start`/`stop` are all an agent can report about itself — no agent knows its own
|
|
@@ -97,4 +125,3 @@ pnpm typecheck
|
|
|
97
125
|
```
|
|
98
126
|
|
|
99
127
|
No runtime dependencies, so `npx neuvikon` stays a fast cold start.
|
|
100
|
-
# neuvikon-cli
|
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
|
|
@@ -640,12 +712,61 @@ When the user hands you a Neuvikon ticket — text beginning with a reference li
|
|
|
640
712
|
|
|
641
713
|
npx neuvikon start --ticket EWWQ-1
|
|
642
714
|
|
|
643
|
-
|
|
715
|
+
Before you start, and any time you need more context, read the ticket's
|
|
716
|
+
project pages — this is the project's accumulated knowledge, not just this
|
|
717
|
+
ticket's:
|
|
718
|
+
|
|
719
|
+
npx neuvikon pages --ticket EWWQ-1
|
|
720
|
+
|
|
721
|
+
When you learn something worth keeping — how a system works, a decision and
|
|
722
|
+
why, anything future work on this project would benefit from — write it back
|
|
723
|
+
so the next agent or teammate has it:
|
|
724
|
+
|
|
725
|
+
npx neuvikon pages update "Some Title" --ticket EWWQ-1 <<'EOF'
|
|
726
|
+
What you learned.
|
|
727
|
+
EOF
|
|
728
|
+
|
|
729
|
+
An update with a title that already exists overwrites that page instead of
|
|
730
|
+
creating a duplicate, so reuse a title for anything you're extending.
|
|
731
|
+
Reference another page by writing its title in double brackets, e.g.
|
|
732
|
+
\`[[Auth Flow]]\` — it becomes a real link if that page already exists.
|
|
733
|
+
|
|
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:
|
|
644
763
|
|
|
645
764
|
npx neuvikon stop
|
|
646
765
|
|
|
647
|
-
|
|
648
|
-
|
|
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
|
|
649
770
|
work — never let it block you.`;
|
|
650
771
|
function instructionBlock() {
|
|
651
772
|
return `${START_MARKER}\n${INSTRUCTION_BODY}\n${END_MARKER}`;
|
|
@@ -907,6 +1028,112 @@ async function logout() {
|
|
|
907
1028
|
info(dim(" Could not reach Neuvikon to revoke the token — revoke it in settings if it leaked."));
|
|
908
1029
|
}
|
|
909
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
|
|
1074
|
+
//#region src/commands/pages.ts
|
|
1075
|
+
function requireCredentials() {
|
|
1076
|
+
const credentials = readCredentials();
|
|
1077
|
+
if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
|
|
1078
|
+
return credentials;
|
|
1079
|
+
}
|
|
1080
|
+
/** `--ticket` wins; otherwise fall back to whatever this directory is already tracking. */
|
|
1081
|
+
function resolveTicketRef(flags) {
|
|
1082
|
+
const ref = flags.get("ticket") ?? readSession()?.ticket;
|
|
1083
|
+
if (!ref) fail("No ticket to look up pages for.", "Pass --ticket EWWQ-1, or run this where `npx neuvikon start` is already tracking one.");
|
|
1084
|
+
return ref;
|
|
1085
|
+
}
|
|
1086
|
+
/** Both pages subcommands fail the same two ways `start` does; shared so they say so identically. */
|
|
1087
|
+
async function request(credentials, path, body) {
|
|
1088
|
+
try {
|
|
1089
|
+
return await post(credentials.url, path, body);
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
if (error instanceof NetworkError) fail("Could not reach Neuvikon.");
|
|
1092
|
+
if (error instanceof ApiError) {
|
|
1093
|
+
if (error.status === 401) fail(error.message, "Run `npx neuvikon login` again.");
|
|
1094
|
+
fail(error.message);
|
|
1095
|
+
}
|
|
1096
|
+
throw error;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
async function list(flags) {
|
|
1100
|
+
const credentials = requireCredentials();
|
|
1101
|
+
const ticketRef = resolveTicketRef(flags);
|
|
1102
|
+
const { pages } = await request(credentials, "/api/agent/pages", {
|
|
1103
|
+
token: credentials.token,
|
|
1104
|
+
ticketRef
|
|
1105
|
+
});
|
|
1106
|
+
if (pages.length === 0) {
|
|
1107
|
+
info(dim(`No pages in ${ticketRef}'s project yet.`));
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
const body = pages.map((page) => `# ${page.title}\n\n${page.text}`).join("\n\n---\n\n");
|
|
1111
|
+
process.stdout.write(`${body}\n`);
|
|
1112
|
+
}
|
|
1113
|
+
async function update(flags, positional) {
|
|
1114
|
+
const credentials = requireCredentials();
|
|
1115
|
+
const ticketRef = resolveTicketRef(flags);
|
|
1116
|
+
const title = flags.get("title") ?? positional[0];
|
|
1117
|
+
if (!title) fail("A page title is required.", `Pass --title "Auth flow notes", or give it as the first argument.`);
|
|
1118
|
+
const text = (flags.get("text") || await readStdin()).trim();
|
|
1119
|
+
if (!text) fail("No page text given.", "Pipe the page's body in on stdin, or pass --text.");
|
|
1120
|
+
success(`${(await request(credentials, "/api/agent/pages/upsert", {
|
|
1121
|
+
token: credentials.token,
|
|
1122
|
+
ticketRef,
|
|
1123
|
+
title,
|
|
1124
|
+
text
|
|
1125
|
+
})).created ? "Created" : "Updated"} page ${bold(title)} in ${ticketRef}'s project`);
|
|
1126
|
+
}
|
|
1127
|
+
async function pages(flags, positional) {
|
|
1128
|
+
const [subcommand, ...rest] = positional;
|
|
1129
|
+
switch (subcommand) {
|
|
1130
|
+
case void 0:
|
|
1131
|
+
case "list": return list(flags);
|
|
1132
|
+
case "update": return update(flags, rest);
|
|
1133
|
+
default: fail(`Unknown "pages ${subcommand}".`, "Try `pages` or `pages update`.");
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
//#endregion
|
|
910
1137
|
//#region src/commands/start.ts
|
|
911
1138
|
const TOOLS = /* @__PURE__ */ new Set([
|
|
912
1139
|
"claude-code",
|
|
@@ -1012,6 +1239,21 @@ ${bold("Usage")}
|
|
|
1012
1239
|
npx neuvikon status Show login, project setup and current ticket
|
|
1013
1240
|
npx neuvikon logout
|
|
1014
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
|
+
|
|
1250
|
+
${bold("Project pages")}
|
|
1251
|
+
npx neuvikon pages List the current ticket's project pages
|
|
1252
|
+
npx neuvikon pages update "Title" < notes.txt
|
|
1253
|
+
Create or update a page by title
|
|
1254
|
+
Reference another page with [[Its Title]], same as in the editor.
|
|
1255
|
+
Both accept --ticket EWWQ-1 instead of relying on a tracked session.
|
|
1256
|
+
|
|
1015
1257
|
${bold("Options")}
|
|
1016
1258
|
--tool <name> claude-code | codex | cursor | other
|
|
1017
1259
|
--transcript <path> Read token usage from an agent transcript
|
|
@@ -1059,6 +1301,10 @@ async function main() {
|
|
|
1059
1301
|
case "report": return report(flags);
|
|
1060
1302
|
case "stop": return stop(flags);
|
|
1061
1303
|
case "status": return status();
|
|
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);
|
|
1062
1308
|
case void 0:
|
|
1063
1309
|
case "help":
|
|
1064
1310
|
case "--help":
|