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.
- package/README.md +17 -1
- package/dist/index.mjs +144 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# neuvikon
|
|
2
2
|
|
|
3
|
-
Tells [Neuvikon](https://neuvikon.
|
|
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
|