neuvikon 0.2.15 → 0.2.17

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 +17 -1
  2. package/dist/index.mjs +144 -0
  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,
@@ -36,6 +36,7 @@ as being worked on, by whom, with which tool.
36
36
  | `neuvikon login` | Sign in through the browser and set up the current project |
37
37
  | `neuvikon init` | Set up another project (credentials are per-machine, rules are per-project) |
38
38
  | `neuvikon start --ticket EWWQ-1` | Begin tracking. `--tool claude-code\|codex\|cursor\|other` |
39
+ | `neuvikon start --tickets EWWQ-1,EWWQ-2 --spawn` | Open one terminal per ticket instead of tracking one here |
39
40
  | `neuvikon stop` | Stop tracking |
40
41
  | `neuvikon status` | Login, project setup, current ticket |
41
42
  | `neuvikon logout` | Revoke this machine's token and forget it |
@@ -48,6 +49,21 @@ as being worked on, by whom, with which tool.
48
49
  pbpaste | npx neuvikon start
49
50
  ```
50
51
 
52
+ ## Kicking off several tickets at once
53
+
54
+ ```sh
55
+ npx neuvikon start --tickets EWWQ-1,EWWQ-2,EWWQ-3 --spawn
56
+ ```
57
+
58
+ Opens one terminal per ticket (a tmux window if you're already in tmux, an
59
+ iTerm2 or Terminal.app tab on macOS, a Windows Terminal tab on Windows), each
60
+ `cd`'d into this directory with the ticket ref printed and waiting — nothing
61
+ is tracked automatically. That's deliberate: a session only scopes safely
62
+ once an agent is actually running in that terminal, so paste the ticket into
63
+ your agent (or run `npx neuvikon start --ticket EWWQ-1`) once it's up, same
64
+ as any other ticket. On a platform or terminal this can't drive, it just
65
+ prints the same command for you to paste by hand.
66
+
51
67
  ## Project pages
52
68
 
53
69
  An agent's most useful output often isn't the ticket, it's what it learned
package/dist/index.mjs CHANGED
@@ -1134,6 +1134,106 @@ async function pages(flags, positional) {
1134
1134
  }
1135
1135
  }
1136
1136
  //#endregion
1137
+ //#region src/terminal.ts
1138
+ function shQuote(value) {
1139
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1140
+ }
1141
+ /**
1142
+ * Deliberately does not run `neuvikon start` itself: that only scopes safely
1143
+ * once an agent is attached (see session.ts's instanceKey), and nothing is
1144
+ * attached yet in a terminal that just opened. Printing the ticket and
1145
+ * dropping to a shell lets the user launch their agent first, same as typing
1146
+ * the ticket by hand always has.
1147
+ */
1148
+ function buildInnerCommand({ ref, cwd }) {
1149
+ const echoes = [`Ticket: ${ref}`, `Paste it into your agent, or run: npx neuvikon start --ticket ${ref}`].map((line) => `echo ${shQuote(line)}`).join(" && ");
1150
+ return `cd ${shQuote(cwd)} && ${echoes} && exec $SHELL -l`;
1151
+ }
1152
+ function escapeAppleScript(value) {
1153
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1154
+ }
1155
+ /**
1156
+ * Same shape as openBrowser: fire-and-forget, never throws. A `false` return
1157
+ * means the spawn call itself failed synchronously (e.g. the binary isn't on
1158
+ * PATH) — the async case (binary exists but the app itself errors) is
1159
+ * swallowed by the "error" listener, same tradeoff openBrowser already makes.
1160
+ */
1161
+ function fireAndForget(command, args) {
1162
+ try {
1163
+ const child = spawn(command, args, {
1164
+ stdio: "ignore",
1165
+ detached: true
1166
+ });
1167
+ child.on("error", () => {});
1168
+ child.unref();
1169
+ return true;
1170
+ } catch {
1171
+ return false;
1172
+ }
1173
+ }
1174
+ function spawnViaTmux(target) {
1175
+ return fireAndForget("tmux", [
1176
+ "new-window",
1177
+ "-n",
1178
+ target.ref,
1179
+ "-c",
1180
+ target.cwd,
1181
+ buildInnerCommand(target)
1182
+ ]);
1183
+ }
1184
+ /**
1185
+ * iTerm2 needs its own dialect (Terminal.app's `do script` isn't understood
1186
+ * by it) — detected through TERM_PROGRAM rather than "is it running", because
1187
+ * a user who has iTerm2 installed but is currently in Terminal.app wants a
1188
+ * Terminal.app tab, not a background iTerm2 window they won't notice.
1189
+ */
1190
+ function spawnOnDarwin(target) {
1191
+ const inner = escapeAppleScript(buildInnerCommand(target));
1192
+ return fireAndForget("osascript", ["-e", process.env.TERM_PROGRAM === "iTerm.app" ? `tell application "iTerm2"
1193
+ activate
1194
+ try
1195
+ tell current window to create tab with default profile
1196
+ on error
1197
+ create window with default profile
1198
+ end try
1199
+ tell current window to tell current tab to tell current session to write text "${inner}"
1200
+ end tell` : `tell application "Terminal" to do script "${inner}"`]);
1201
+ }
1202
+ function spawnOnWindows(target) {
1203
+ const inner = buildInnerCommand(target).replace(/"/g, "\\\"");
1204
+ return fireAndForget("wt", [
1205
+ "-w",
1206
+ "0",
1207
+ "nt",
1208
+ "-d",
1209
+ target.cwd,
1210
+ "powershell",
1211
+ "-NoExit",
1212
+ "-Command",
1213
+ inner
1214
+ ]);
1215
+ }
1216
+ /**
1217
+ * `x-terminal-emulator` is the Debian/Ubuntu alternatives symlink to
1218
+ * whichever terminal the user has set as default — the closest thing Linux
1219
+ * has to a single common denominator. Anything else degrades to the plain
1220
+ * command list the caller already prints.
1221
+ */
1222
+ function spawnOnLinux(target) {
1223
+ return fireAndForget("x-terminal-emulator", [
1224
+ "-e",
1225
+ "bash",
1226
+ "-c",
1227
+ `${buildInnerCommand(target)}; exec bash`
1228
+ ]);
1229
+ }
1230
+ function spawnTerminal(target) {
1231
+ if (process.env.TMUX) return spawnViaTmux(target);
1232
+ if (process.platform === "darwin") return spawnOnDarwin(target);
1233
+ if (process.platform === "win32") return spawnOnWindows(target);
1234
+ return spawnOnLinux(target);
1235
+ }
1236
+ //#endregion
1137
1237
  //#region src/commands/start.ts
1138
1238
  const TOOLS = /* @__PURE__ */ new Set([
1139
1239
  "claude-code",
@@ -1141,6 +1241,46 @@ const TOOLS = /* @__PURE__ */ new Set([
1141
1241
  "cursor",
1142
1242
  "other"
1143
1243
  ]);
1244
+ /**
1245
+ * Explicit and comma-separated, not scraped from stdin like a single ref
1246
+ * would be: a ticket's own description can easily mention other refs
1247
+ * ("see NS-12"), and those must never trigger opening terminals for them.
1248
+ */
1249
+ function parseTicketsFlag(raw) {
1250
+ const seen = /* @__PURE__ */ new Set();
1251
+ for (const token of raw.split(",")) {
1252
+ const trimmed = token.trim();
1253
+ if (!trimmed) continue;
1254
+ const [ref] = extractTicketRefs(trimmed);
1255
+ if (!ref || ref !== trimmed.toUpperCase()) fail(`"${trimmed}" is not a ticket reference.`, "Expected something like EWWQ-1.");
1256
+ seen.add(ref);
1257
+ }
1258
+ if (seen.size === 0) fail("No ticket references found in --tickets.", "Expected something like NS-1,NS-2.");
1259
+ return [...seen];
1260
+ }
1261
+ async function resolveRefs(flags, positional) {
1262
+ const raw = flags.get("tickets");
1263
+ if (raw) return parseTicketsFlag(raw);
1264
+ return [await resolveRef(flags, positional)];
1265
+ }
1266
+ /**
1267
+ * Opens one terminal per ref and stops there — it deliberately never calls
1268
+ * `start` itself. A session file written outside of a running agent has no
1269
+ * CLAUDE_CODE_SESSION_ID to scope it, so two of these firing in the same
1270
+ * directory before either agent launches would each read the other's
1271
+ * not-yet-attached session as "existing" and stop it out from under it.
1272
+ * Letting the user (or their agent) run `start` once it's actually running
1273
+ * is what keeps concurrent tracking safe.
1274
+ */
1275
+ async function spawnMode(refs) {
1276
+ const cwd = process.cwd();
1277
+ info(`Opening ${refs.length} terminal${refs.length === 1 ? "" : "s"} in ${cwd}...`);
1278
+ for (const ref of refs) if (spawnTerminal({
1279
+ ref,
1280
+ cwd
1281
+ })) success(`${bold(ref)} — new terminal opened`);
1282
+ else info(`${bold(ref)} — couldn't open a terminal here, paste manually: ${dim(`npx neuvikon start --ticket ${ref}`)}`);
1283
+ }
1144
1284
  async function resolveRef(flags, positional) {
1145
1285
  const explicit = flags.get("ticket") ?? positional[0];
1146
1286
  if (explicit) {
@@ -1162,6 +1302,8 @@ function detectTool() {
1162
1302
  async function start(flags, positional) {
1163
1303
  const credentials = readCredentials();
1164
1304
  if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
1305
+ if (flags.has("tickets") && !flags.has("spawn")) fail("--tickets needs --spawn.", "Opening more than one terminal is the only thing --tickets does.");
1306
+ if (flags.has("spawn")) return spawnMode(await resolveRefs(flags, positional));
1165
1307
  const ref = await resolveRef(flags, positional);
1166
1308
  const tool = flags.get("tool") ?? detectTool();
1167
1309
  if (!TOOLS.has(tool)) fail(`Unknown tool "${tool}".`, `Expected one of: ${[...TOOLS].join(", ")}.`);
@@ -1235,6 +1377,8 @@ ${bold("Usage")}
1235
1377
  npx neuvikon login Sign in through the browser
1236
1378
  npx neuvikon init Teach this project's coding agents to report work
1237
1379
  npx neuvikon start --ticket EWWQ-1
1380
+ npx neuvikon start --tickets EWWQ-1,EWWQ-2 --spawn
1381
+ Open one terminal per ticket instead of tracking one here
1238
1382
  npx neuvikon stop
1239
1383
  npx neuvikon status Show login, project setup and current ticket
1240
1384
  npx neuvikon logout
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuvikon",
3
- "version": "0.2.15",
3
+ "version": "0.2.17",
4
4
  "description": "Tell Neuvikon which ticket your coding agent is working on.",
5
5
  "keywords": [
6
6
  "neuvikon",