neuvikon 0.2.29 → 0.2.31

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 (2) hide show
  1. package/dist/index.mjs +121 -28
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -276,12 +276,12 @@ function fail(message, hint) {
276
276
  }
277
277
  //#endregion
278
278
  //#region src/commands/board.ts
279
- function requireCredentials$4() {
279
+ function requireCredentials$5() {
280
280
  const credentials = readCredentials();
281
281
  if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
282
282
  return credentials;
283
283
  }
284
- function resolveTicketRef$4(flags) {
284
+ function resolveTicketRef$5(flags) {
285
285
  const ref = flags.get("ticket") ?? readSession()?.ticket;
286
286
  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.");
287
287
  return ref;
@@ -291,8 +291,8 @@ function resolveTicketRef$4(flags) {
291
291
  * a column name — boards vary per project, and some have no "done" at all.
292
292
  */
293
293
  async function board(flags) {
294
- const credentials = requireCredentials$4();
295
- const ticketRef = resolveTicketRef$4(flags);
294
+ const credentials = requireCredentials$5();
295
+ const ticketRef = resolveTicketRef$5(flags);
296
296
  let result;
297
297
  try {
298
298
  result = await post(credentials.url, "/api/agent/board", {
@@ -339,20 +339,20 @@ async function readStdin() {
339
339
  }
340
340
  //#endregion
341
341
  //#region src/commands/comment.ts
342
- function requireCredentials$3() {
342
+ function requireCredentials$4() {
343
343
  const credentials = readCredentials();
344
344
  if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
345
345
  return credentials;
346
346
  }
347
- function resolveTicketRef$3(flags) {
347
+ function resolveTicketRef$4(flags) {
348
348
  const ref = flags.get("ticket") ?? readSession()?.ticket;
349
349
  if (!ref) fail("No ticket to comment on.", "Pass --ticket EWWQ-1, or run this where `npx neuvikon start` is already tracking one.");
350
350
  return ref;
351
351
  }
352
352
  /** A note on a ticket that doesn't move it — e.g. explaining a handoff mid-ticket. */
353
353
  async function comment(flags) {
354
- const credentials = requireCredentials$3();
355
- const ticketRef = resolveTicketRef$3(flags);
354
+ const credentials = requireCredentials$4();
355
+ const ticketRef = resolveTicketRef$4(flags);
356
356
  const text = (flags.get("text") || await readStdin()).trim();
357
357
  if (!text) fail("No comment text given.", "Pipe the comment in on stdin, or pass --text.");
358
358
  try {
@@ -374,18 +374,18 @@ async function comment(flags) {
374
374
  //#endregion
375
375
  //#region src/commands/create.ts
376
376
  const TYPES = /* @__PURE__ */ new Set(["feature", "bug"]);
377
- const PRIORITIES = /* @__PURE__ */ new Set([
377
+ const PRIORITIES$1 = /* @__PURE__ */ new Set([
378
378
  "low",
379
379
  "medium",
380
380
  "high",
381
381
  "urgent"
382
382
  ]);
383
- function requireCredentials$2() {
383
+ function requireCredentials$3() {
384
384
  const credentials = readCredentials();
385
385
  if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
386
386
  return credentials;
387
387
  }
388
- function resolveTicketRef$2(flags) {
388
+ function resolveTicketRef$3(flags) {
389
389
  const ref = flags.get("ticket") ?? readSession()?.ticket;
390
390
  if (!ref) fail("No ticket to file this against.", "Pass --ticket EWWQ-1, or run this where `npx neuvikon start` is already tracking one.");
391
391
  return ref;
@@ -413,16 +413,18 @@ function resolveParentRef(flags, ticketRef) {
413
413
  * own "+ sub-ticket" control.
414
414
  */
415
415
  async function create(flags, positional) {
416
- const credentials = requireCredentials$2();
417
- const ticketRef = resolveTicketRef$2(flags);
416
+ const credentials = requireCredentials$3();
417
+ const ticketRef = resolveTicketRef$3(flags);
418
418
  const parentRef = resolveParentRef(flags, ticketRef);
419
419
  const title = flags.get("title") ?? positional[0];
420
420
  if (!title) fail("A ticket title is required.", `Pass --title "Invite emails aren't localized", or give it as the first argument.`);
421
421
  const type = flags.get("type");
422
422
  if (type && !TYPES.has(type)) fail(`Unknown type "${type}".`, "Expected feature or bug.");
423
423
  const priority = flags.get("priority");
424
- if (priority && !PRIORITIES.has(priority)) fail(`Unknown priority "${priority}".`, "Expected low, medium, high, or urgent.");
424
+ if (priority && !PRIORITIES$1.has(priority)) fail(`Unknown priority "${priority}".`, "Expected low, medium, high, or urgent.");
425
425
  const description = (flags.get("description") || await readStdin()).trim() || void 0;
426
+ const assignee = flags.get("assignee");
427
+ const sprint = flags.get("sprint");
426
428
  let result;
427
429
  try {
428
430
  result = await post(credentials.url, "/api/agent/tickets/create", {
@@ -432,7 +434,9 @@ async function create(flags, positional) {
432
434
  description,
433
435
  type,
434
436
  priority,
435
- parentRef
437
+ parentRef,
438
+ assignee,
439
+ sprint
436
440
  });
437
441
  } catch (error) {
438
442
  if (error instanceof NetworkError) fail("Could not reach Neuvikon.");
@@ -733,6 +737,27 @@ function parsePayload(raw) {
733
737
  }
734
738
  }
735
739
  /**
740
+ * What actually gets the model/effort switch to happen, versus the
741
+ * AGENTS.md wording alone: tested against real tickets, an agent reading
742
+ * "judge difficulty and switch model" in its rules file consistently read
743
+ * past it and started working at whatever model/effort it already had —
744
+ * the instruction competed with everything else in that file and lost.
745
+ *
746
+ * `UserPromptSubmit` can inject `additionalContext`, which Claude Code
747
+ * surfaces as a system-level reminder on the *same turn* the ticket lands,
748
+ * not as background file content the agent may or may not re-read. It's
749
+ * still just text — this can't force a `/model`/`/effort` call, only put it
750
+ * in front of the model at the one moment it's actually relevant instead of
751
+ * buried in a rules file next to a dozen other instructions.
752
+ */
753
+ const MODEL_TRIAGE_CONTEXT = "A Neuvikon ticket just landed in this prompt. Before reading any file or running `start`: judge how difficult it actually is. If it's a small, mechanical change (button placement, a copy/label edit, a trivial styling tweak, a one-line config change), send `/model haiku` and `/effort low` now, as their own messages, before touching the ticket — there's no reason to spend a frontier model thinking hard about something this small. If it needs real design judgment, touches multiple systems, or you're unsure, stay on the current model and effort level instead.";
754
+ function hookResponse(additionalContext) {
755
+ return JSON.stringify({ hookSpecificOutput: {
756
+ hookEventName: "UserPromptSubmit",
757
+ additionalContext
758
+ } });
759
+ }
760
+ /**
736
761
  * The `UserPromptSubmit` counterpart to `start`. Unlike `start`, this runs on
737
762
  * *every* prompt regardless of whether it mentions a ticket, so it must never
738
763
  * fail the hook and block the user's message — no credentials, no ticket ref,
@@ -745,6 +770,7 @@ async function hookStart() {
745
770
  const { prompt, transcriptPath } = parsePayload(await readStdin());
746
771
  const [ref] = extractTicketRefs(prompt ?? "");
747
772
  if (!ref) return;
773
+ process.stdout.write(hookResponse(MODEL_TRIAGE_CONTEXT));
748
774
  const existing = readSession();
749
775
  if (existing?.ticket === ref) return;
750
776
  const transcript = transcriptPath ?? claudeTranscriptPath();
@@ -1272,12 +1298,12 @@ async function logout() {
1272
1298
  }
1273
1299
  //#endregion
1274
1300
  //#region src/commands/move.ts
1275
- function requireCredentials$1() {
1301
+ function requireCredentials$2() {
1276
1302
  const credentials = readCredentials();
1277
1303
  if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
1278
1304
  return credentials;
1279
1305
  }
1280
- function resolveTicketRef$1(flags) {
1306
+ function resolveTicketRef$2(flags) {
1281
1307
  const ref = flags.get("ticket") ?? readSession()?.ticket;
1282
1308
  if (!ref) fail("No ticket to move.", "Pass --ticket EWWQ-1, or run this where `npx neuvikon start` is already tracking one.");
1283
1309
  return ref;
@@ -1289,8 +1315,8 @@ function resolveTicketRef$1(flags) {
1289
1315
  * that silently.
1290
1316
  */
1291
1317
  async function move(flags) {
1292
- const credentials = requireCredentials$1();
1293
- const ticketRef = resolveTicketRef$1(flags);
1318
+ const credentials = requireCredentials$2();
1319
+ const ticketRef = resolveTicketRef$2(flags);
1294
1320
  const column = flags.get("column");
1295
1321
  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.`);
1296
1322
  const comment = (flags.get("comment") || await readStdin()).trim();
@@ -1315,13 +1341,13 @@ async function move(flags) {
1315
1341
  }
1316
1342
  //#endregion
1317
1343
  //#region src/commands/pages.ts
1318
- function requireCredentials() {
1344
+ function requireCredentials$1() {
1319
1345
  const credentials = readCredentials();
1320
1346
  if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
1321
1347
  return credentials;
1322
1348
  }
1323
1349
  /** `--ticket` wins; otherwise fall back to whatever this directory is already tracking. */
1324
- function resolveTicketRef(flags) {
1350
+ function resolveTicketRef$1(flags) {
1325
1351
  const ref = flags.get("ticket") ?? readSession()?.ticket;
1326
1352
  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.");
1327
1353
  return ref;
@@ -1340,8 +1366,8 @@ async function request(credentials, path, body) {
1340
1366
  }
1341
1367
  }
1342
1368
  async function list(flags) {
1343
- const credentials = requireCredentials();
1344
- const ticketRef = resolveTicketRef(flags);
1369
+ const credentials = requireCredentials$1();
1370
+ const ticketRef = resolveTicketRef$1(flags);
1345
1371
  const { pages } = await request(credentials, "/api/agent/pages", {
1346
1372
  token: credentials.token,
1347
1373
  ticketRef
@@ -1353,9 +1379,9 @@ async function list(flags) {
1353
1379
  const body = pages.map((page) => `# ${page.title}\n\n${page.text}`).join("\n\n---\n\n");
1354
1380
  process.stdout.write(`${body}\n`);
1355
1381
  }
1356
- async function update(flags, positional) {
1357
- const credentials = requireCredentials();
1358
- const ticketRef = resolveTicketRef(flags);
1382
+ async function update$1(flags, positional) {
1383
+ const credentials = requireCredentials$1();
1384
+ const ticketRef = resolveTicketRef$1(flags);
1359
1385
  const title = flags.get("title") ?? positional[0];
1360
1386
  if (!title) fail("A page title is required.", `Pass --title "Auth flow notes", or give it as the first argument.`);
1361
1387
  const text = (flags.get("text") || await readStdin()).trim();
@@ -1372,7 +1398,7 @@ async function pages(flags, positional) {
1372
1398
  switch (subcommand) {
1373
1399
  case void 0:
1374
1400
  case "list": return list(flags);
1375
- case "update": return update(flags, rest);
1401
+ case "update": return update$1(flags, rest);
1376
1402
  default: fail(`Unknown "pages ${subcommand}".`, "Try `pages` or `pages update`.");
1377
1403
  }
1378
1404
  }
@@ -1888,6 +1914,67 @@ async function stop(flags) {
1888
1914
  success(`Stopped tracking ${session.ticket}`);
1889
1915
  }
1890
1916
  //#endregion
1917
+ //#region src/commands/update.ts
1918
+ const PRIORITIES = /* @__PURE__ */ new Set([
1919
+ "low",
1920
+ "medium",
1921
+ "high",
1922
+ "urgent"
1923
+ ]);
1924
+ /** What `--assignee none` / `--sprint none` means: clear the field. */
1925
+ const NONE = "none";
1926
+ function requireCredentials() {
1927
+ const credentials = readCredentials();
1928
+ if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
1929
+ return credentials;
1930
+ }
1931
+ function resolveTicketRef(flags) {
1932
+ const ref = flags.get("ticket") ?? readSession()?.ticket;
1933
+ if (!ref) fail("No ticket to update.", "Pass --ticket EWWQ-1, or run this where `npx neuvikon start` is already tracking one.");
1934
+ return ref;
1935
+ }
1936
+ /** `undefined` means "leave it alone", `null` means "clear it", a string means "set it to this". */
1937
+ function resolveClearable(flags, name) {
1938
+ const value = flags.get(name);
1939
+ if (value === void 0) return void 0;
1940
+ return value.toLowerCase() === NONE ? null : value;
1941
+ }
1942
+ /**
1943
+ * Changes fields on a ticket that already exists, e.g.
1944
+ * `npx neuvikon update --ticket NS-91 --assignee Ogulcan --sprint "Sprint 1"`.
1945
+ * `--assignee none` / `--sprint none` clears that field instead of setting it.
1946
+ */
1947
+ async function update(flags) {
1948
+ const credentials = requireCredentials();
1949
+ const ticketRef = resolveTicketRef(flags);
1950
+ const assignee = resolveClearable(flags, "assignee");
1951
+ const sprint = resolveClearable(flags, "sprint");
1952
+ const priority = flags.get("priority");
1953
+ if (priority && !PRIORITIES.has(priority)) fail(`Unknown priority "${priority}".`, "Expected low, medium, high, or urgent.");
1954
+ const labelsFlag = flags.get("labels");
1955
+ const labels = labelsFlag !== void 0 ? labelsFlag.split(",").map((label) => label.trim()).filter(Boolean) : void 0;
1956
+ if (assignee === void 0 && sprint === void 0 && priority === void 0 && labels === void 0) fail("Nothing to update.", "Pass at least one of --assignee, --sprint, --priority, --labels.");
1957
+ let result;
1958
+ try {
1959
+ result = await post(credentials.url, "/api/agent/tickets/update", {
1960
+ token: credentials.token,
1961
+ ticketRef,
1962
+ assignee,
1963
+ sprint,
1964
+ priority,
1965
+ labels
1966
+ });
1967
+ } catch (error) {
1968
+ if (error instanceof NetworkError) fail("Could not reach Neuvikon.");
1969
+ if (error instanceof ApiError) {
1970
+ if (error.status === 401) fail(error.message, "Run `npx neuvikon login` again.");
1971
+ fail(error.message);
1972
+ }
1973
+ throw error;
1974
+ }
1975
+ success(`Updated ${bold(result.ref)}: ${result.changed.join(", ")}`);
1976
+ }
1977
+ //#endregion
1891
1978
  //#region src/index.ts
1892
1979
  const HELP = `${bold("neuvikon")} — tell Neuvikon which ticket your coding agent is on
1893
1980
 
@@ -1911,7 +1998,12 @@ ${bold("Board")}
1911
1998
  File something you noticed as its own ticket, same project
1912
1999
  npx neuvikon create "Add French copy" --sub
1913
2000
  ...or as a sub-ticket (--sub for this one, --parent NS-12 for another)
1914
- All four accept --ticket EWWQ-1 instead of relying on a tracked session.
2001
+ npx neuvikon create "..." --assignee Ogulcan --sprint "Sprint 1"
2002
+ File it already assigned and/or planned in a sprint
2003
+ npx neuvikon update --assignee Ogulcan --sprint "Sprint 1" --priority high
2004
+ Change assignee, sprint, priority, or labels on an existing ticket
2005
+ ("none" clears --assignee/--sprint)
2006
+ All five accept --ticket EWWQ-1 instead of relying on a tracked session.
1915
2007
 
1916
2008
  ${bold("Project pages")}
1917
2009
  npx neuvikon pages List the current ticket's project pages
@@ -1973,6 +2065,7 @@ async function main() {
1973
2065
  case "move": return move(flags);
1974
2066
  case "comment": return comment(flags);
1975
2067
  case "create": return create(flags, positional);
2068
+ case "update": return update(flags);
1976
2069
  case void 0:
1977
2070
  case "help":
1978
2071
  case "--help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuvikon",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
4
4
  "description": "Tell Neuvikon which ticket your coding agent is working on.",
5
5
  "keywords": [
6
6
  "neuvikon",