neuvikon 0.2.17 → 0.2.19
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 +16 -8
- package/dist/index.mjs +314 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,14 +55,22 @@ pbpaste | npx neuvikon start
|
|
|
55
55
|
npx neuvikon start --tickets EWWQ-1,EWWQ-2,EWWQ-3 --spawn
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
If [Herdr](https://herdr.dev) is installed, every ticket in the batch lands as
|
|
59
|
+
its own tab in one Herdr workspace instead of separate OS windows — Herdr's
|
|
60
|
+
own UI then shows each agent's live working/blocked/done state. Otherwise it
|
|
61
|
+
opens one terminal per ticket the usual way (a tmux window if you're already
|
|
62
|
+
in tmux, an iTerm2 or Terminal.app tab on macOS, a Windows Terminal tab on
|
|
63
|
+
Windows), each `cd`'d into this directory. Either way, if a coding agent CLI
|
|
64
|
+
is installed (Claude Code, Cursor, Codex) it's launched with that ticket's
|
|
65
|
+
full text as its first prompt — same substance a manual paste would give it.
|
|
66
|
+
With more than one installed, you're asked once which to use and that choice
|
|
67
|
+
is remembered (`--agent <name>` overrides it any time). Nothing is tracked
|
|
68
|
+
automatically, even then: a session only scopes safely once an agent is
|
|
69
|
+
actually running, so the launched agent runs `npx neuvikon start --ticket
|
|
70
|
+
EWWQ-1` itself once it's up, via its rules file, same as a manual paste
|
|
71
|
+
always has. Without a detected agent, or on a platform/terminal this can't
|
|
72
|
+
drive, it just prints the ticket ref and that command for you to paste by
|
|
73
|
+
hand.
|
|
66
74
|
|
|
67
75
|
## Project pages
|
|
68
76
|
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
4
|
+
import { delimiter, dirname, join } from "node:path";
|
|
5
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
6
7
|
//#region src/api.ts
|
|
7
8
|
var ApiError = class extends Error {
|
|
8
9
|
status;
|
|
@@ -79,6 +80,23 @@ function resolveDeployment(flags) {
|
|
|
79
80
|
const DEFAULT_SITE_URL = process.env.NEUVIKON_URL ?? DEPLOYMENTS.production.api;
|
|
80
81
|
const CONFIG_DIR = join(homedir(), ".neuvikon");
|
|
81
82
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
83
|
+
function readConfigFile() {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
86
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
87
|
+
} catch {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function writeConfigFile(config) {
|
|
92
|
+
mkdirSync(CONFIG_DIR, {
|
|
93
|
+
recursive: true,
|
|
94
|
+
mode: 448
|
|
95
|
+
});
|
|
96
|
+
chmodSync(CONFIG_DIR, 448);
|
|
97
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
|
|
98
|
+
chmodSync(CONFIG_PATH, 384);
|
|
99
|
+
}
|
|
82
100
|
/**
|
|
83
101
|
* Somewhere a browser cannot open — CI, a container, a remote box — there is no
|
|
84
102
|
* way to complete a device login, so a token handed in through the environment
|
|
@@ -116,16 +134,15 @@ function readCredentials() {
|
|
|
116
134
|
/**
|
|
117
135
|
* A token on disk is only as safe as its permissions, so directory and file are
|
|
118
136
|
* locked to the owner on every write, not just on first create — an earlier
|
|
119
|
-
* version may have left them readable.
|
|
137
|
+
* version may have left them readable. Merges over the existing file rather
|
|
138
|
+
* than replacing it outright, so a re-login doesn't silently drop unrelated
|
|
139
|
+
* settings this file has grown since (e.g. `preferredAgent`).
|
|
120
140
|
*/
|
|
121
141
|
function writeCredentials(credentials) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
writeConfigFile({
|
|
143
|
+
...readConfigFile(),
|
|
144
|
+
...credentials
|
|
125
145
|
});
|
|
126
|
-
chmodSync(CONFIG_DIR, 448);
|
|
127
|
-
writeFileSync(CONFIG_PATH, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 384 });
|
|
128
|
-
chmodSync(CONFIG_PATH, 384);
|
|
129
146
|
}
|
|
130
147
|
function clearCredentials() {
|
|
131
148
|
try {
|
|
@@ -135,6 +152,22 @@ function clearCredentials() {
|
|
|
135
152
|
return false;
|
|
136
153
|
}
|
|
137
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Which agent CLI `start --spawn` should launch without asking, remembered
|
|
157
|
+
* across invocations once the user has answered the question once. Kept in
|
|
158
|
+
* the same config file as credentials rather than a separate one — it's the
|
|
159
|
+
* same "settings for this machine" concern.
|
|
160
|
+
*/
|
|
161
|
+
function readPreferredAgent() {
|
|
162
|
+
const value = readConfigFile().preferredAgent;
|
|
163
|
+
return typeof value === "string" && value ? value : null;
|
|
164
|
+
}
|
|
165
|
+
function writePreferredAgent(id) {
|
|
166
|
+
writeConfigFile({
|
|
167
|
+
...readConfigFile(),
|
|
168
|
+
preferredAgent: id
|
|
169
|
+
});
|
|
170
|
+
}
|
|
138
171
|
//#endregion
|
|
139
172
|
//#region src/session.ts
|
|
140
173
|
/**
|
|
@@ -1134,20 +1167,234 @@ async function pages(flags, positional) {
|
|
|
1134
1167
|
}
|
|
1135
1168
|
}
|
|
1136
1169
|
//#endregion
|
|
1170
|
+
//#region src/agentCli.ts
|
|
1171
|
+
const KNOWN_AGENT_CLIS = [
|
|
1172
|
+
{
|
|
1173
|
+
id: "claude-code",
|
|
1174
|
+
label: "Claude Code",
|
|
1175
|
+
binary: "claude",
|
|
1176
|
+
buildArgs: (prompt) => [prompt]
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
id: "cursor",
|
|
1180
|
+
label: "Cursor",
|
|
1181
|
+
binary: "cursor-agent",
|
|
1182
|
+
buildArgs: (prompt) => [prompt]
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
id: "codex",
|
|
1186
|
+
label: "Codex",
|
|
1187
|
+
binary: "codex",
|
|
1188
|
+
buildArgs: (prompt) => [prompt]
|
|
1189
|
+
}
|
|
1190
|
+
];
|
|
1191
|
+
/**
|
|
1192
|
+
* Manual PATH scan instead of shelling out to `which`/`where`: keeps this
|
|
1193
|
+
* dependency-free and synchronous, and doesn't assume either helper exists
|
|
1194
|
+
* (minimal containers, and Windows has neither `which` nor always `where`
|
|
1195
|
+
* usable from a spawned child the same way).
|
|
1196
|
+
*/
|
|
1197
|
+
function isOnPath(binary) {
|
|
1198
|
+
const pathEnv = process.env.PATH ?? "";
|
|
1199
|
+
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";") : [""];
|
|
1200
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
1201
|
+
if (!dir) continue;
|
|
1202
|
+
for (const ext of exts) if (existsSync(join(dir, binary + ext))) return true;
|
|
1203
|
+
}
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
function detectAgentClis() {
|
|
1207
|
+
return KNOWN_AGENT_CLIS.filter((agent) => isOnPath(agent.binary));
|
|
1208
|
+
}
|
|
1209
|
+
function findById(id) {
|
|
1210
|
+
return KNOWN_AGENT_CLIS.find((agent) => agent.id === id);
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Only prompts when stdin is a real terminal: `--tickets`/`--spawn` never
|
|
1214
|
+
* reads stdin for the ticket itself (unlike plain `start`), so it's free for
|
|
1215
|
+
* this, but a non-interactive invocation (CI, a script) must never block on
|
|
1216
|
+
* input that will never arrive.
|
|
1217
|
+
*/
|
|
1218
|
+
async function promptForAgent(candidates) {
|
|
1219
|
+
if (!process.stdin.isTTY) return candidates[0] ?? null;
|
|
1220
|
+
info("Multiple coding agents found on this machine:");
|
|
1221
|
+
candidates.forEach((agent, index) => info(` ${index + 1}. ${agent.label}`));
|
|
1222
|
+
info(dim("This choice is remembered — override any time with --agent <name>."));
|
|
1223
|
+
const rl = createInterface({
|
|
1224
|
+
input: process.stdin,
|
|
1225
|
+
output: process.stderr
|
|
1226
|
+
});
|
|
1227
|
+
try {
|
|
1228
|
+
for (;;) {
|
|
1229
|
+
const answer = (await rl.question(`Which one should Neuvikon launch? [1-${candidates.length}]: `)).trim();
|
|
1230
|
+
const picked = candidates[Number(answer) - 1];
|
|
1231
|
+
if (picked) return picked;
|
|
1232
|
+
info(dim(`Enter a number from 1 to ${candidates.length}.`));
|
|
1233
|
+
}
|
|
1234
|
+
} finally {
|
|
1235
|
+
rl.close();
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Resolves which agent CLI (if any) `--spawn` should launch. `--agent` is a
|
|
1240
|
+
* one-off override and is never persisted, same as `--tool` never rewrites
|
|
1241
|
+
* anything. Otherwise: a stored preference wins as long as it's still on
|
|
1242
|
+
* this machine's PATH (an uninstalled tool shouldn't stay sticky forever);
|
|
1243
|
+
* failing that, one candidate is used silently, and more than one is asked
|
|
1244
|
+
* about exactly once and then remembered.
|
|
1245
|
+
*/
|
|
1246
|
+
async function resolveAgentCli(flags) {
|
|
1247
|
+
const explicit = flags.get("agent");
|
|
1248
|
+
if (explicit) {
|
|
1249
|
+
const agent = findById(explicit);
|
|
1250
|
+
if (!agent) fail(`Unknown agent "${explicit}".`, `Expected one of: ${KNOWN_AGENT_CLIS.map((a) => a.id).join(", ")}.`);
|
|
1251
|
+
return agent;
|
|
1252
|
+
}
|
|
1253
|
+
const candidates = detectAgentClis();
|
|
1254
|
+
if (candidates.length === 0) return null;
|
|
1255
|
+
const preferredId = readPreferredAgent();
|
|
1256
|
+
if (preferredId) {
|
|
1257
|
+
const preferred = candidates.find((agent) => agent.id === preferredId);
|
|
1258
|
+
if (preferred) return preferred;
|
|
1259
|
+
}
|
|
1260
|
+
const picked = candidates.length === 1 ? candidates[0] : await promptForAgent(candidates);
|
|
1261
|
+
if (picked) writePreferredAgent(picked.id);
|
|
1262
|
+
return picked;
|
|
1263
|
+
}
|
|
1264
|
+
//#endregion
|
|
1265
|
+
//#region src/herdr.ts
|
|
1266
|
+
/** Herdr's `--kind` values for the agents we already know how to pick between. */
|
|
1267
|
+
const HERDR_KIND = {
|
|
1268
|
+
"claude-code": "claude",
|
|
1269
|
+
codex: "codex",
|
|
1270
|
+
cursor: "cursor"
|
|
1271
|
+
};
|
|
1272
|
+
function herdrAvailable() {
|
|
1273
|
+
return isOnPath("herdr");
|
|
1274
|
+
}
|
|
1275
|
+
/** Every mutating Herdr socket-API command prints one JSON object; a failure prints to stderr instead. */
|
|
1276
|
+
function runHerdr(args) {
|
|
1277
|
+
try {
|
|
1278
|
+
const output = execFileSync("herdr", args, {
|
|
1279
|
+
encoding: "utf8",
|
|
1280
|
+
stdio: [
|
|
1281
|
+
"ignore",
|
|
1282
|
+
"pipe",
|
|
1283
|
+
"ignore"
|
|
1284
|
+
]
|
|
1285
|
+
});
|
|
1286
|
+
return JSON.parse(output);
|
|
1287
|
+
} catch {
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Herdr's `agent start` requires a name matching `[a-z][a-z0-9_-]{0,31}`,
|
|
1293
|
+
* unique among live agents. A ticket ref (`NS-60`) lowercases straight into
|
|
1294
|
+
* that shape, and reusing the ref as the name is what makes `herdr agent
|
|
1295
|
+
* get NS-60`-style lookups obvious later.
|
|
1296
|
+
*/
|
|
1297
|
+
function agentName(ref) {
|
|
1298
|
+
return ref.toLowerCase();
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* One Herdr workspace per `--spawn` call, one tab per ticket, so a whole
|
|
1302
|
+
* batch shows up together in Herdr's own UI — with its live
|
|
1303
|
+
* working/blocked/done state per agent — instead of scattered across OS
|
|
1304
|
+
* windows. The first successful ref seeds the workspace; every later ref in
|
|
1305
|
+
* the same batch adds a tab to it.
|
|
1306
|
+
*/
|
|
1307
|
+
var HerdrBatch = class {
|
|
1308
|
+
workspaceId = null;
|
|
1309
|
+
openPane(ref, cwd) {
|
|
1310
|
+
if (!this.workspaceId) {
|
|
1311
|
+
const created = runHerdr([
|
|
1312
|
+
"workspace",
|
|
1313
|
+
"create",
|
|
1314
|
+
"--cwd",
|
|
1315
|
+
cwd,
|
|
1316
|
+
"--label",
|
|
1317
|
+
ref,
|
|
1318
|
+
"--focus"
|
|
1319
|
+
]);
|
|
1320
|
+
if (!created) return null;
|
|
1321
|
+
this.workspaceId = created.result.workspace.workspace_id;
|
|
1322
|
+
return created.result.root_pane.pane_id;
|
|
1323
|
+
}
|
|
1324
|
+
return runHerdr([
|
|
1325
|
+
"tab",
|
|
1326
|
+
"create",
|
|
1327
|
+
"--workspace",
|
|
1328
|
+
this.workspaceId,
|
|
1329
|
+
"--cwd",
|
|
1330
|
+
cwd,
|
|
1331
|
+
"--label",
|
|
1332
|
+
ref,
|
|
1333
|
+
"--focus"
|
|
1334
|
+
])?.result.root_pane.pane_id ?? null;
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* True once this ref has a live Herdr pane — even if starting an agent in
|
|
1338
|
+
* it failed, in which case the pane still exists with the same
|
|
1339
|
+
* print-and-wait fallback a plain spawned terminal gets. False only means
|
|
1340
|
+
* Herdr itself couldn't be reached, and the caller should fall back to
|
|
1341
|
+
* opening a native OS terminal for this ref instead.
|
|
1342
|
+
*/
|
|
1343
|
+
spawn(ref, cwd, agent, ticketText) {
|
|
1344
|
+
const paneId = this.openPane(ref, cwd);
|
|
1345
|
+
if (!paneId) return false;
|
|
1346
|
+
if (agent && ticketText) {
|
|
1347
|
+
if (runHerdr([
|
|
1348
|
+
"agent",
|
|
1349
|
+
"start",
|
|
1350
|
+
agentName(ref),
|
|
1351
|
+
"--kind",
|
|
1352
|
+
HERDR_KIND[agent.id],
|
|
1353
|
+
"--pane",
|
|
1354
|
+
paneId
|
|
1355
|
+
])) {
|
|
1356
|
+
runHerdr([
|
|
1357
|
+
"agent",
|
|
1358
|
+
"prompt",
|
|
1359
|
+
agentName(ref),
|
|
1360
|
+
ticketText
|
|
1361
|
+
]);
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
runHerdr([
|
|
1366
|
+
"pane",
|
|
1367
|
+
"run",
|
|
1368
|
+
paneId,
|
|
1369
|
+
`echo 'Ticket: ${ref}' && echo 'Paste it into your agent, or run: npx neuvikon start --ticket ${ref}'`
|
|
1370
|
+
]);
|
|
1371
|
+
return true;
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
//#endregion
|
|
1137
1375
|
//#region src/terminal.ts
|
|
1138
1376
|
function shQuote(value) {
|
|
1139
1377
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1140
1378
|
}
|
|
1141
1379
|
/**
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1380
|
+
* Never `exec`s the agent in place: if it's the wrong binary, isn't actually
|
|
1381
|
+
* installed, or the user just exits it, the terminal must not go on to close
|
|
1382
|
+
* or hang — it drops to the same interactive shell either way, ticket ref
|
|
1383
|
+
* already printed above.
|
|
1384
|
+
*
|
|
1385
|
+
* Still deliberately does not run `neuvikon start` itself: that only scopes
|
|
1386
|
+
* safely once an agent is attached (see session.ts's instanceKey), and
|
|
1387
|
+
* nothing is attached yet in a terminal that just opened. Launching the
|
|
1388
|
+
* agent (rather than `start`) is safe precisely because the agent is the one
|
|
1389
|
+
* that calls `start`, once it's actually running, via the rules file — same
|
|
1390
|
+
* as if the ticket had been pasted in by hand.
|
|
1147
1391
|
*/
|
|
1148
|
-
function buildInnerCommand({ ref, cwd }) {
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1392
|
+
function buildInnerCommand({ ref, cwd, launch }) {
|
|
1393
|
+
const lines = [`Ticket: ${ref}`];
|
|
1394
|
+
if (!launch) lines.push(`Paste it into your agent, or run: npx neuvikon start --ticket ${ref}`);
|
|
1395
|
+
const echoes = lines.map((line) => `echo ${shQuote(line)}`).join(" && ");
|
|
1396
|
+
const launchCmd = launch ? `${launch.binary} ${launch.args.map(shQuote).join(" ")}; ` : "";
|
|
1397
|
+
return `cd ${shQuote(cwd)} && ${echoes} && ${launchCmd}exec $SHELL -l`;
|
|
1151
1398
|
}
|
|
1152
1399
|
function escapeAppleScript(value) {
|
|
1153
1400
|
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
@@ -1264,22 +1511,60 @@ async function resolveRefs(flags, positional) {
|
|
|
1264
1511
|
return [await resolveRef(flags, positional)];
|
|
1265
1512
|
}
|
|
1266
1513
|
/**
|
|
1514
|
+
* Best effort, same shape as `spawnTerminal` itself: a ticket that can't be
|
|
1515
|
+
* fetched (offline, a typo'd ref, a revoked token) degrades that one terminal
|
|
1516
|
+
* back to the plain print-and-shell fallback rather than failing the whole
|
|
1517
|
+
* batch, or worse, launching an agent with nothing to work from.
|
|
1518
|
+
*/
|
|
1519
|
+
async function fetchTicketText(credentials, ref) {
|
|
1520
|
+
try {
|
|
1521
|
+
return (await post(credentials.url, "/api/agent/ticket", {
|
|
1522
|
+
token: credentials.token,
|
|
1523
|
+
ticketRef: ref
|
|
1524
|
+
})).text;
|
|
1525
|
+
} catch {
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1267
1530
|
* Opens one terminal per ref and stops there — it deliberately never calls
|
|
1268
1531
|
* `start` itself. A session file written outside of a running agent has no
|
|
1269
1532
|
* CLAUDE_CODE_SESSION_ID to scope it, so two of these firing in the same
|
|
1270
1533
|
* 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
|
-
*
|
|
1273
|
-
*
|
|
1534
|
+
* not-yet-attached session as "existing" and stop it out from under it. That
|
|
1535
|
+
* still holds here: a detected agent CLI is launched with the ticket's text
|
|
1536
|
+
* as its first prompt, but this never calls `start` itself — the agent does,
|
|
1537
|
+
* once it's actually running, via the rules file, exactly as if the ticket
|
|
1538
|
+
* had been pasted in by hand.
|
|
1539
|
+
*
|
|
1540
|
+
* Herdr, when installed, wins over a native OS terminal: it's a multiplexer
|
|
1541
|
+
* built for exactly this (multiple coding agents, one UI, live
|
|
1542
|
+
* working/blocked/done state), so a whole `--tickets` batch lands as one
|
|
1543
|
+
* Herdr workspace with one tab per ticket instead of scattered app windows.
|
|
1274
1544
|
*/
|
|
1275
|
-
async function spawnMode(refs) {
|
|
1545
|
+
async function spawnMode(flags, credentials, refs) {
|
|
1276
1546
|
const cwd = process.cwd();
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1547
|
+
const agent = await resolveAgentCli(flags);
|
|
1548
|
+
const herdr = herdrAvailable() ? new HerdrBatch() : null;
|
|
1549
|
+
info(`Opening ${refs.length} terminal${refs.length === 1 ? "" : "s"}${herdr ? " in Herdr" : ` in ${cwd}`}...`);
|
|
1550
|
+
for (const ref of refs) {
|
|
1551
|
+
const ticketText = agent ? await fetchTicketText(credentials, ref) : null;
|
|
1552
|
+
if (herdr?.spawn(ref, cwd, agent, ticketText)) {
|
|
1553
|
+
success(`${bold(ref)} — ${agent && ticketText ? `${agent.label} starting in Herdr` : "opened in Herdr"}`);
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
const launch = agent && ticketText ? {
|
|
1557
|
+
binary: agent.binary,
|
|
1558
|
+
args: agent.buildArgs(ticketText)
|
|
1559
|
+
} : null;
|
|
1560
|
+
const startingLabel = launch && agent ? agent.label : null;
|
|
1561
|
+
if (spawnTerminal({
|
|
1562
|
+
ref,
|
|
1563
|
+
cwd,
|
|
1564
|
+
launch
|
|
1565
|
+
})) success(startingLabel ? `${bold(ref)} — ${startingLabel} starting` : `${bold(ref)} — new terminal opened`);
|
|
1566
|
+
else info(`${bold(ref)} — couldn't open a terminal here, paste manually: ${dim(`npx neuvikon start --ticket ${ref}`)}`);
|
|
1567
|
+
}
|
|
1283
1568
|
}
|
|
1284
1569
|
async function resolveRef(flags, positional) {
|
|
1285
1570
|
const explicit = flags.get("ticket") ?? positional[0];
|
|
@@ -1303,7 +1588,7 @@ async function start(flags, positional) {
|
|
|
1303
1588
|
const credentials = readCredentials();
|
|
1304
1589
|
if (!credentials) fail("Not logged in.", "Run `npx neuvikon login` first.");
|
|
1305
1590
|
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));
|
|
1591
|
+
if (flags.has("spawn")) return spawnMode(flags, credentials, await resolveRefs(flags, positional));
|
|
1307
1592
|
const ref = await resolveRef(flags, positional);
|
|
1308
1593
|
const tool = flags.get("tool") ?? detectTool();
|
|
1309
1594
|
if (!TOOLS.has(tool)) fail(`Unknown tool "${tool}".`, `Expected one of: ${[...TOOLS].join(", ")}.`);
|
|
@@ -1400,6 +1685,7 @@ ${bold("Project pages")}
|
|
|
1400
1685
|
|
|
1401
1686
|
${bold("Options")}
|
|
1402
1687
|
--tool <name> claude-code | codex | cursor | other
|
|
1688
|
+
--agent <name> claude-code | codex | cursor — which agent CLI --spawn launches
|
|
1403
1689
|
--transcript <path> Read token usage from an agent transcript
|
|
1404
1690
|
--tokens-in <n> --tokens-out <n>
|
|
1405
1691
|
--dev Use the development deployment
|