@nolto/cli 0.2.0 → 0.3.0

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.js +437 -56
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import { createRequire as createRequire2 } from "module";
5
5
  import { fileURLToPath as fileURLToPath2 } from "url";
6
- import path5 from "path";
6
+ import path6 from "path";
7
7
  import { CommanderError } from "commander";
8
8
 
9
9
  // src/config.ts
@@ -275,6 +275,22 @@ import { createRequire } from "module";
275
275
  import { fileURLToPath } from "url";
276
276
  import path2 from "path";
277
277
  import fs from "fs";
278
+
279
+ // src/unwrap.ts
280
+ function unwrapList(result, key) {
281
+ if (Array.isArray(result)) {
282
+ return result;
283
+ }
284
+ if (result != null && typeof result === "object") {
285
+ const inner = result[key];
286
+ if (Array.isArray(inner)) {
287
+ return inner;
288
+ }
289
+ }
290
+ return [];
291
+ }
292
+
293
+ // src/commands/init.ts
278
294
  var __dirname = path2.dirname(fileURLToPath(import.meta.url));
279
295
  var _require = createRequire(import.meta.url);
280
296
  function getCliVersion() {
@@ -295,9 +311,9 @@ function getCliVersion() {
295
311
  }
296
312
  return "0.0.0";
297
313
  }
298
- function register(program, _deps) {
314
+ function register(program, deps) {
299
315
  program.command("init").description("Interactive setup: configure token, base URL, and default project.").option("--force", "Overwrite existing config without prompting").action(async (opts) => {
300
- const configPath = getConfigPath(process.env);
316
+ const configPath = deps.configPath;
301
317
  if (!opts.force) {
302
318
  let existing = null;
303
319
  try {
@@ -330,9 +346,7 @@ function register(program, _deps) {
330
346
  let projects = [];
331
347
  try {
332
348
  const result = await caller.call("list_projects", {});
333
- if (Array.isArray(result)) {
334
- projects = result;
335
- }
349
+ projects = unwrapList(result, "projects");
336
350
  } catch (err) {
337
351
  if (err instanceof CliError && err.exitCode === 3) {
338
352
  throw new CliError(`Token rejected by ${baseUrl}`, 3, "Check that your token is valid and has not been revoked.");
@@ -397,6 +411,375 @@ async function promptHidden(rl, prompt) {
397
411
  return value;
398
412
  }
399
413
 
414
+ // src/commands/login.ts
415
+ import readline2 from "readline/promises";
416
+
417
+ // src/http.ts
418
+ function createHttpClient(opts) {
419
+ const { baseUrl, version, token } = opts;
420
+ const base = baseUrl.replace(/\/+$/, "");
421
+ return {
422
+ async post(path7, body) {
423
+ if (!path7.startsWith("/api/")) {
424
+ throw new CliError(`HTTP client path must start with /api/, got: ${path7}`, 2);
425
+ }
426
+ const url = `${base}${path7}`;
427
+ const headers = {
428
+ "Content-Type": "application/json",
429
+ "User-Agent": `${CLI_USER_AGENT_NAME}/${version}`
430
+ };
431
+ if (token) {
432
+ headers["Authorization"] = `Bearer ${token}`;
433
+ }
434
+ let response;
435
+ try {
436
+ response = await fetch(url, {
437
+ method: "POST",
438
+ headers,
439
+ body: JSON.stringify(body)
440
+ });
441
+ } catch (err) {
442
+ if (isNetworkError(err)) {
443
+ throw new CliError(
444
+ `Network error contacting ${base}: ${err instanceof Error ? err.message : String(err)}`,
445
+ 5
446
+ );
447
+ }
448
+ throw err;
449
+ }
450
+ if (!response.ok) {
451
+ const retryAfter = response.headers.get("retry-after") ?? void 0;
452
+ const wwwAuthenticate = response.headers.get("www-authenticate") ?? void 0;
453
+ throw mapHttpStatusToCliError(response.status, retryAfter, wwwAuthenticate);
454
+ }
455
+ return response.json();
456
+ }
457
+ };
458
+ }
459
+
460
+ // src/login-poll.ts
461
+ async function pollUntilToken(opts) {
462
+ const { http, deviceCode, expiresIn, baseUrl } = opts;
463
+ let intervalSeconds = opts.intervalSeconds;
464
+ const deadline = Date.now() + expiresIn * 1e3;
465
+ await sleep(intervalSeconds * 1e3);
466
+ while (Date.now() < deadline) {
467
+ let resp;
468
+ try {
469
+ resp = await http.post("/api/cli/auth/poll", {
470
+ device_code: deviceCode
471
+ });
472
+ } catch (err) {
473
+ if (err instanceof CliError && err.exitCode === 4) {
474
+ await sleep(intervalSeconds * 1e3);
475
+ continue;
476
+ }
477
+ throw err;
478
+ }
479
+ if (!resp.error) {
480
+ if (!resp.token) {
481
+ throw new CliError("Server returned success but no token.", 5);
482
+ }
483
+ return resp.token;
484
+ }
485
+ switch (resp.error) {
486
+ case "authorization_pending":
487
+ await sleep(intervalSeconds * 1e3);
488
+ break;
489
+ case "slow_down":
490
+ intervalSeconds += 5;
491
+ await sleep(intervalSeconds * 1e3);
492
+ break;
493
+ case "expired_token":
494
+ throw new CliError("Device code expired. Run `nolto login` again.", 2);
495
+ case "access_denied":
496
+ throw new CliError("\u30ED\u30B0\u30A4\u30F3\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F\u3002", 3);
497
+ case "token_cap_exceeded":
498
+ throw new CliError(
499
+ `\u30A2\u30AF\u30C6\u30A3\u30D6\u306A API token \u304C\u4E0A\u9650(20)\u3067\u3059\u3002${baseUrl}/settings/tokens \u3067\u4E0D\u8981\u306A\u30C8\u30FC\u30AF\u30F3\u3092\u5931\u52B9\u3057\u3066\u304B\u3089\u518D\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002`,
500
+ 2
501
+ );
502
+ default:
503
+ throw new CliError(`Unexpected poll response: ${resp.error}`, 5);
504
+ }
505
+ }
506
+ throw new CliError("Login timed out. Run `nolto login` again.", 2);
507
+ }
508
+ function sleep(ms) {
509
+ return new Promise((resolve) => setTimeout(resolve, ms));
510
+ }
511
+
512
+ // src/client-inject.ts
513
+ import { readFile as readFile2, writeFile as writeFile2, rename, mkdir as mkdir2, unlink } from "fs/promises";
514
+ import { execFile } from "child_process";
515
+ import { promisify } from "util";
516
+ import os2 from "os";
517
+ import path3 from "path";
518
+ function getExecFileAsync() {
519
+ return promisify(execFile);
520
+ }
521
+ async function atomicWrite(filePath, content, mode2) {
522
+ const dir = path3.dirname(filePath);
523
+ await mkdir2(dir, { recursive: true, mode: 448 });
524
+ const tmp = `${filePath}.tmp.${Date.now()}`;
525
+ try {
526
+ await writeFile2(tmp, content, { mode: mode2 });
527
+ await rename(tmp, filePath);
528
+ } catch (err) {
529
+ await unlink(tmp).catch(() => void 0);
530
+ throw err;
531
+ }
532
+ }
533
+ async function readJsonConfig(filePath) {
534
+ try {
535
+ const raw = await readFile2(filePath, "utf8");
536
+ const parsed = JSON.parse(raw);
537
+ return { parsed, raw };
538
+ } catch (err) {
539
+ const code = err.code;
540
+ if (code === "ENOENT") return null;
541
+ return null;
542
+ }
543
+ }
544
+ async function injectCursor(opts) {
545
+ const configDir = opts.cursorConfigDir ?? process.env["CURSOR_CONFIG_DIR"] ?? path3.join(os2.homedir(), ".cursor");
546
+ const configPath = path3.join(configDir, "mcp.json");
547
+ const mcpUrl = `${opts.baseUrl.replace(/\/+$/, "")}/mcp`;
548
+ let existing = {};
549
+ const raw = await readFile2(configPath, "utf8").catch((err) => {
550
+ if (err.code === "ENOENT") return null;
551
+ return null;
552
+ });
553
+ if (raw !== null) {
554
+ try {
555
+ existing = JSON.parse(raw);
556
+ } catch {
557
+ const backupPath = `${configPath}.bak.${Date.now()}`;
558
+ process.stdout.write(
559
+ `Warning: ${configPath} contains invalid JSON. Backing up to ${backupPath}
560
+ `
561
+ );
562
+ await writeFile2(backupPath, raw, { mode: 384 }).catch(() => void 0);
563
+ existing = {};
564
+ }
565
+ }
566
+ const updatedServers = {
567
+ ...existing.mcpServers,
568
+ nolto: {
569
+ url: mcpUrl,
570
+ headers: {
571
+ Authorization: `Bearer ${opts.token}`
572
+ }
573
+ }
574
+ };
575
+ const newConfig = {
576
+ ...existing,
577
+ mcpServers: updatedServers
578
+ };
579
+ await atomicWrite(configPath, JSON.stringify(newConfig, null, 2) + "\n", 384);
580
+ process.stdout.write(`Updated ${configPath} (cursor)
581
+ `);
582
+ }
583
+ async function injectClaude(opts) {
584
+ const cwd = opts.claudeCwd ?? process.cwd();
585
+ const configPath = path3.join(cwd, ".mcp.json");
586
+ const mcpUrl = `${opts.baseUrl.replace(/\/+$/, "")}/mcp`;
587
+ const result = await readJsonConfig(configPath);
588
+ const existing = result?.parsed ?? {};
589
+ const updatedServers = {
590
+ ...existing.mcpServers,
591
+ nolto: {
592
+ url: mcpUrl,
593
+ headers: {
594
+ Authorization: `Bearer ${opts.token}`
595
+ },
596
+ env: {
597
+ NOLTO_MCP_TOKEN: opts.token
598
+ }
599
+ }
600
+ };
601
+ const newConfig = {
602
+ ...existing,
603
+ mcpServers: updatedServers
604
+ };
605
+ await atomicWrite(configPath, JSON.stringify(newConfig, null, 2) + "\n", 384);
606
+ process.stdout.write(`Updated ${configPath} (claude project .mcp.json)
607
+ `);
608
+ }
609
+ async function injectCodex(opts) {
610
+ const mcpUrl = `${opts.baseUrl.replace(/\/+$/, "")}/mcp`;
611
+ const codexPath = await findOnPath("codex");
612
+ if (!codexPath) {
613
+ process.stdout.write(
614
+ `
615
+ To add Nolto to Codex, run:
616
+
617
+ codex mcp add nolto --url ${mcpUrl} --bearer-token-env-var NOLTO_TOKEN
618
+
619
+ Then set NOLTO_TOKEN=${opts.token} in your environment.
620
+ `
621
+ );
622
+ return;
623
+ }
624
+ try {
625
+ const execFileAsync2 = getExecFileAsync();
626
+ const { stderr } = await execFileAsync2(codexPath, [
627
+ "mcp",
628
+ "add",
629
+ "nolto",
630
+ "--url",
631
+ mcpUrl,
632
+ "--bearer-token-env-var",
633
+ "NOLTO_TOKEN"
634
+ ]);
635
+ if (stderr) {
636
+ process.stderr.write(`[codex mcp add] ${stderr}
637
+ `);
638
+ }
639
+ process.stdout.write(`Registered nolto MCP server in Codex.
640
+ `);
641
+ process.stdout.write(
642
+ `Set NOLTO_TOKEN=${opts.token.slice(0, 8)}... in your environment (or shell profile).
643
+ `
644
+ );
645
+ } catch (err) {
646
+ const msg = err instanceof Error ? err.message : String(err);
647
+ throw new CliError(`codex mcp add failed: ${msg}`, 5);
648
+ }
649
+ }
650
+ async function findOnPath(bin) {
651
+ try {
652
+ const execFileAsync2 = getExecFileAsync();
653
+ const { stdout } = await execFileAsync2(
654
+ process.platform === "win32" ? "where" : "which",
655
+ [bin]
656
+ );
657
+ const found = stdout.trim().split("\n")[0]?.trim();
658
+ return found && found.length > 0 ? found : null;
659
+ } catch {
660
+ return null;
661
+ }
662
+ }
663
+ async function injectClient(opts) {
664
+ switch (opts.client) {
665
+ case "cursor":
666
+ return injectCursor(opts);
667
+ case "claude":
668
+ return injectClaude(opts);
669
+ case "codex":
670
+ return injectCodex(opts);
671
+ default: {
672
+ const _never = opts.client;
673
+ throw new CliError(`Unknown client: ${String(_never)}`, 2);
674
+ }
675
+ }
676
+ }
677
+
678
+ // src/commands/login.ts
679
+ function register2(program, deps) {
680
+ program.command("login").description("Authenticate via browser and save an API token to the config file.").option("--client <name>", "Inject token into MCP client config (cursor|claude|codex)").option("--force", "Overwrite existing token without prompting").action(
681
+ async (opts) => {
682
+ const { configPath, settings, output } = deps;
683
+ const validClients = ["cursor", "claude", "codex"];
684
+ if (opts.client !== void 0 && !validClients.includes(opts.client)) {
685
+ throw new CliError(
686
+ `--client must be one of: ${validClients.join(", ")}. Got: ${opts.client}`,
687
+ 2
688
+ );
689
+ }
690
+ const clientTarget = opts.client;
691
+ if (!opts.force) {
692
+ let existing = null;
693
+ try {
694
+ existing = await loadConfigFile(configPath);
695
+ } catch {
696
+ }
697
+ if (existing?.token) {
698
+ const rl = readline2.createInterface({
699
+ input: process.stdin,
700
+ output: process.stdout
701
+ });
702
+ try {
703
+ const answer = await rl.question(
704
+ `A token is already configured (${maskToken(existing.token)}). Overwrite? [y/N] `
705
+ );
706
+ if (answer.trim().toLowerCase() !== "y") {
707
+ process.stdout.write("Cancelled.\n");
708
+ return;
709
+ }
710
+ } finally {
711
+ rl.close();
712
+ }
713
+ }
714
+ }
715
+ const http = createHttpClient({
716
+ baseUrl: settings.baseUrl,
717
+ version: deps.version
718
+ });
719
+ let startResp;
720
+ try {
721
+ startResp = await http.post("/api/cli/auth/start", {});
722
+ } catch (err) {
723
+ if (err instanceof CliError && err.exitCode === 4) {
724
+ throw new CliError(
725
+ "Rate limit reached. Please wait a few minutes and try again.",
726
+ 4
727
+ );
728
+ }
729
+ throw err;
730
+ }
731
+ process.stdout.write("\n");
732
+ process.stdout.write("Open the following URL in your browser to log in:\n\n");
733
+ process.stdout.write(` ${startResp.verification_url}
734
+
735
+ `);
736
+ process.stdout.write(`Your confirmation code: ${startResp.user_code}
737
+
738
+ `);
739
+ process.stdout.write("Waiting for browser confirmation...\n");
740
+ const token = await pollUntilToken({
741
+ http,
742
+ deviceCode: startResp.device_code,
743
+ intervalSeconds: startResp.interval,
744
+ expiresIn: startResp.expires_in,
745
+ baseUrl: settings.baseUrl
746
+ });
747
+ const existingConfig = await loadConfigFile(configPath).catch(() => null);
748
+ const newConfig = {
749
+ ...existingConfig,
750
+ token
751
+ };
752
+ await saveConfigFile(configPath, newConfig);
753
+ process.stdout.write("\n");
754
+ if (output.mode === "json") {
755
+ process.stdout.write(
756
+ JSON.stringify({ ok: true, token: `...${maskToken(token)}` }, null, 2) + "\n"
757
+ );
758
+ } else {
759
+ process.stdout.write(`Saved ${configPath}
760
+ `);
761
+ process.stdout.write(`token: ${maskToken(token)}
762
+ `);
763
+ }
764
+ if (clientTarget) {
765
+ try {
766
+ await injectClient({
767
+ client: clientTarget,
768
+ token,
769
+ baseUrl: settings.baseUrl
770
+ });
771
+ } catch (err) {
772
+ const msg = err instanceof Error ? err.message : String(err);
773
+ process.stderr.write(
774
+ `Warning: failed to inject into ${clientTarget} config: ${msg}
775
+ `
776
+ );
777
+ }
778
+ }
779
+ }
780
+ );
781
+ }
782
+
400
783
  // src/output.ts
401
784
  function printResult(value, mode2, opts = {}) {
402
785
  const out = opts.stream ?? process.stdout;
@@ -466,7 +849,7 @@ function formatValue(value) {
466
849
  }
467
850
 
468
851
  // src/commands/whoami.ts
469
- function register2(program, deps) {
852
+ function register3(program, deps) {
470
853
  program.command("whoami").description("Show the current authentication and configuration state.").action(async () => {
471
854
  const { settings, output, configPath } = deps;
472
855
  const mode2 = output.mode;
@@ -474,9 +857,7 @@ function register2(program, deps) {
474
857
  if (settings.token != null) {
475
858
  try {
476
859
  const result = await deps.caller.call("list_projects", {});
477
- if (Array.isArray(result)) {
478
- projectCount = result.length;
479
- }
860
+ projectCount = unwrapList(result, "projects").length;
480
861
  } catch {
481
862
  }
482
863
  }
@@ -501,7 +882,7 @@ function register2(program, deps) {
501
882
  ["configPath", configPath],
502
883
  ["token", tokenDisplay],
503
884
  ["defaultProject", `${settings.defaultProjectId ?? "none"} (source: ${settings.source.project})`],
504
- ["projects", projectCount != null ? String(projectCount) : "(no token)"]
885
+ ["projects", projectCount != null ? String(projectCount) : settings.token != null ? "(unavailable)" : "(no token)"]
505
886
  ];
506
887
  const maxKey = lines.reduce((m, [k]) => Math.max(m, k.length), 0);
507
888
  for (const [key, val] of lines) {
@@ -521,7 +902,7 @@ function assertToken(token) {
521
902
  );
522
903
  }
523
904
  }
524
- function register3(program, deps) {
905
+ function register4(program, deps) {
525
906
  const project = program.command("project").description("Manage projects.");
526
907
  project.command("list").description("List all projects.").action(async () => {
527
908
  assertToken(deps.settings.token);
@@ -530,7 +911,7 @@ function register3(program, deps) {
530
911
  printResult(result, "json");
531
912
  return;
532
913
  }
533
- const rows = Array.isArray(result) ? result : [];
914
+ const rows = unwrapList(result, "projects");
534
915
  const defaultId = deps.settings.defaultProjectId;
535
916
  const tableRows = rows.map((p) => ({
536
917
  id: p.id ?? "",
@@ -558,9 +939,8 @@ function register3(program, deps) {
558
939
  });
559
940
  project.command("set-default <projectId>").description("Set the default project.").option("--local", "Write to local config file only (no MCP call)").action(async (projectId, opts) => {
560
941
  if (opts.local) {
561
- const configPath = getConfigPath(process.env);
562
- const existing = await loadConfigFile(configPath).catch(() => null);
563
- await saveConfigFile(configPath, { ...existing ?? {}, defaultProjectId: projectId });
942
+ const existing = await loadConfigFile(deps.configPath).catch(() => null);
943
+ await saveConfigFile(deps.configPath, { ...existing ?? {}, defaultProjectId: projectId });
564
944
  } else {
565
945
  assertToken(deps.settings.token);
566
946
  await deps.caller.call("set_default_project", { projectId });
@@ -571,18 +951,18 @@ function register3(program, deps) {
571
951
  }
572
952
 
573
953
  // src/commands/plan-register.ts
574
- import { execFile } from "child_process";
575
- import { promisify } from "util";
954
+ import { execFile as execFile2 } from "child_process";
955
+ import { promisify as promisify2 } from "util";
576
956
  import { z as z2 } from "zod";
577
957
 
578
958
  // src/fsx.ts
579
- import { readFile as readFile2, stat as stat2 } from "fs/promises";
580
- import path3 from "path";
959
+ import { readFile as readFile3, stat as stat2 } from "fs/promises";
960
+ import path4 from "path";
581
961
  async function readPlanFile(filePath) {
582
- const absPath = path3.resolve(filePath);
962
+ const absPath = path4.resolve(filePath);
583
963
  let content;
584
964
  try {
585
- content = await readFile2(absPath, "utf8");
965
+ content = await readFile3(absPath, "utf8");
586
966
  } catch (err) {
587
967
  const code = err != null && typeof err === "object" && "code" in err ? err.code : "";
588
968
  if (code === "ENOENT") {
@@ -600,11 +980,11 @@ async function readPlanFile(filePath) {
600
980
  2
601
981
  );
602
982
  }
603
- const stem = path3.basename(absPath, path3.extname(absPath));
983
+ const stem = path4.basename(absPath, path4.extname(absPath));
604
984
  return { content: trimmed, sourcePath: absPath, titleFallback: stem };
605
985
  }
606
986
  async function readDocFile(filePath) {
607
- const absPath = path3.resolve(filePath);
987
+ const absPath = path4.resolve(filePath);
608
988
  let fileSize;
609
989
  try {
610
990
  const info = await stat2(absPath);
@@ -621,12 +1001,12 @@ async function readDocFile(filePath) {
621
1001
  }
622
1002
  let buf;
623
1003
  try {
624
- buf = await readFile2(absPath);
1004
+ buf = await readFile3(absPath);
625
1005
  } catch (err) {
626
1006
  throw new CliError(`Cannot read file: ${absPath}: ${String(err)}`, 2);
627
1007
  }
628
1008
  const isBinary = buf.includes(0) || !isUtf8RoundTrip(buf);
629
- const rawFilename = path3.basename(absPath);
1009
+ const rawFilename = path4.basename(absPath);
630
1010
  const filename = rawFilename.slice(0, DOCUMENT_FILENAME_MAX);
631
1011
  if (isBinary) {
632
1012
  return { content: buf.toString("base64"), encoding: "base64", filename };
@@ -683,7 +1063,7 @@ function extractTitle(md) {
683
1063
  }
684
1064
 
685
1065
  // src/commands/plan-register.ts
686
- var execFileAsync = promisify(execFile);
1066
+ var execFileAsync = promisify2(execFile2);
687
1067
  function assertToken2(token) {
688
1068
  if (token == null) {
689
1069
  throw new CliError(
@@ -830,7 +1210,7 @@ function assertToken3(token) {
830
1210
  );
831
1211
  }
832
1212
  }
833
- function register4(program, deps) {
1213
+ function register5(program, deps) {
834
1214
  const plan = program.command("plan").description("Manage plans.");
835
1215
  plan.command("list").description("List plans.").option("--status <status>", "Filter by status").action(async (opts) => {
836
1216
  assertToken3(deps.settings.token);
@@ -848,12 +1228,12 @@ function register4(program, deps) {
848
1228
  printResult(result, "json");
849
1229
  return;
850
1230
  }
851
- const rows = Array.isArray(result) ? result : [];
1231
+ const rows = unwrapList(result, "plans");
852
1232
  const tableRows = rows.map((p) => ({
853
1233
  id: p.id ?? "",
854
- title: p.title ?? "",
1234
+ title: p.display_title ?? p.raw_title ?? p.title ?? "",
855
1235
  status: p.status ?? "",
856
- createdAt: p.createdAt ?? ""
1236
+ createdAt: p.created_at ?? p.createdAt ?? ""
857
1237
  }));
858
1238
  process.stdout.write(formatTable(tableRows, ["id", "title", "status", "createdAt"]) + "\n");
859
1239
  });
@@ -918,7 +1298,7 @@ function assertToken4(token) {
918
1298
  );
919
1299
  }
920
1300
  }
921
- function register5(program, deps) {
1301
+ function register6(program, deps) {
922
1302
  const phase = program.command("phase").description("Manage plan phases.");
923
1303
  phase.command("status <planId> <phaseId> <status>").description("Update a phase status.").option("--message <text>", "Optional message").action(async (planId, phaseId, status, opts) => {
924
1304
  assertToken4(deps.settings.token);
@@ -992,7 +1372,7 @@ function assertToken5(token) {
992
1372
  );
993
1373
  }
994
1374
  }
995
- function register6(program, deps) {
1375
+ function register7(program, deps) {
996
1376
  const doc = program.command("doc").description("Manage plan documents.");
997
1377
  doc.command("upload <planId>").description("Upload a document to a plan.").requiredOption("--file <path>", "Path to document file").requiredOption("--kind <kind>", `Document kind (${PLAN_DOCUMENT_KINDS.join(", ")})`).option("--phase <phaseId>", "Associate with a specific phase").option("--filename <name>", "Override filename").action(async (planId, opts) => {
998
1378
  assertToken5(deps.settings.token);
@@ -1026,17 +1406,17 @@ function register6(program, deps) {
1026
1406
 
1027
1407
  // src/queue-file.ts
1028
1408
  import { openSync, closeSync, writeFileSync, unlinkSync, renameSync, statSync } from "fs";
1029
- import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, unlink, appendFile } from "fs/promises";
1030
- import path4 from "path";
1409
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3, unlink as unlink2, appendFile } from "fs/promises";
1410
+ import path5 from "path";
1031
1411
  import crypto from "crypto";
1032
1412
  function queueFilePath(projectDir) {
1033
- return path4.join(projectDir, ".nolto", "pending.jsonl");
1413
+ return path5.join(projectDir, ".nolto", "pending.jsonl");
1034
1414
  }
1035
1415
  function lockFilePath(projectDir) {
1036
- return path4.join(projectDir, ".nolto", "flush.lock");
1416
+ return path5.join(projectDir, ".nolto", "flush.lock");
1037
1417
  }
1038
1418
  function logFilePath(projectDir) {
1039
- return path4.join(projectDir, ".nolto", "flush.log");
1419
+ return path5.join(projectDir, ".nolto", "flush.log");
1040
1420
  }
1041
1421
  function resolveQueueDir(inputs) {
1042
1422
  if (inputs.flagDir != null) return inputs.flagDir;
@@ -1050,7 +1430,7 @@ function findAncestorWithMarker(startDir) {
1050
1430
  if (hasMarkerSync(current, ".git") || hasMarkerSync(current, ".nolto")) {
1051
1431
  return current;
1052
1432
  }
1053
- const parent = path4.dirname(current);
1433
+ const parent = path5.dirname(current);
1054
1434
  if (parent === current) break;
1055
1435
  current = parent;
1056
1436
  }
@@ -1058,7 +1438,7 @@ function findAncestorWithMarker(startDir) {
1058
1438
  }
1059
1439
  function hasMarkerSync(dir, marker) {
1060
1440
  try {
1061
- statSync(path4.join(dir, marker));
1441
+ statSync(path5.join(dir, marker));
1062
1442
  return true;
1063
1443
  } catch {
1064
1444
  return false;
@@ -1068,7 +1448,7 @@ async function readQueue(projectDir) {
1068
1448
  const filePath = queueFilePath(projectDir);
1069
1449
  let raw;
1070
1450
  try {
1071
- raw = await readFile3(filePath, "utf8");
1451
+ raw = await readFile4(filePath, "utf8");
1072
1452
  } catch {
1073
1453
  return [];
1074
1454
  }
@@ -1086,8 +1466,8 @@ async function readQueue(projectDir) {
1086
1466
  return entries;
1087
1467
  }
1088
1468
  async function appendEntry(projectDir, entry) {
1089
- const noltoDir = path4.join(projectDir, ".nolto");
1090
- await mkdir2(noltoDir, { recursive: true, mode: 448 });
1469
+ const noltoDir = path5.join(projectDir, ".nolto");
1470
+ await mkdir3(noltoDir, { recursive: true, mode: 448 });
1091
1471
  const existing = await readQueue(projectDir);
1092
1472
  if (existing.length >= QUEUE_MAX_ENTRIES) {
1093
1473
  throw new CliError(
@@ -1117,23 +1497,23 @@ async function atomicRewriteQueue(projectDir, entries) {
1117
1497
  const filePath = queueFilePath(projectDir);
1118
1498
  if (entries.length === 0) {
1119
1499
  try {
1120
- await unlink(filePath);
1500
+ await unlink2(filePath);
1121
1501
  } catch (err) {
1122
1502
  const code = err.code;
1123
1503
  if (code !== "ENOENT") throw err;
1124
1504
  }
1125
1505
  return;
1126
1506
  }
1127
- const noltoDir = path4.join(projectDir, ".nolto");
1128
- await mkdir2(noltoDir, { recursive: true });
1507
+ const noltoDir = path5.join(projectDir, ".nolto");
1508
+ await mkdir3(noltoDir, { recursive: true });
1129
1509
  const tmpPath = filePath + ".tmp";
1130
1510
  const content = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
1131
- await writeFile2(tmpPath, content, "utf8");
1511
+ await writeFile3(tmpPath, content, "utf8");
1132
1512
  renameSync(tmpPath, filePath);
1133
1513
  }
1134
1514
  async function acquireLock(projectDir) {
1135
- const noltoDir = path4.join(projectDir, ".nolto");
1136
- await mkdir2(noltoDir, { recursive: true });
1515
+ const noltoDir = path5.join(projectDir, ".nolto");
1516
+ await mkdir3(noltoDir, { recursive: true });
1137
1517
  const lockPath = lockFilePath(projectDir);
1138
1518
  return tryAcquire(lockPath);
1139
1519
  }
@@ -1148,7 +1528,7 @@ async function tryAcquire(lockPath) {
1148
1528
  }
1149
1529
  let pidStr;
1150
1530
  try {
1151
- pidStr = await readFile3(lockPath, "utf8");
1531
+ pidStr = await readFile4(lockPath, "utf8");
1152
1532
  } catch {
1153
1533
  try {
1154
1534
  unlinkSync(lockPath);
@@ -1193,8 +1573,8 @@ function makeLockHandle(lockPath) {
1193
1573
  }
1194
1574
  async function appendLog(projectDir, level, message) {
1195
1575
  try {
1196
- const noltoDir = path4.join(projectDir, ".nolto");
1197
- await mkdir2(noltoDir, { recursive: true });
1576
+ const noltoDir = path5.join(projectDir, ".nolto");
1577
+ await mkdir3(noltoDir, { recursive: true });
1198
1578
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}
1199
1579
  `;
1200
1580
  await appendFile(logFilePath(projectDir), line, "utf8");
@@ -1401,7 +1781,7 @@ async function runFlushWorker(deps, projectDir) {
1401
1781
  await lock.release();
1402
1782
  }
1403
1783
  }
1404
- function register7(program, deps) {
1784
+ function register8(program, deps) {
1405
1785
  program.command("flush").description("Flush the pending queue, sending each entry to the Nolto MCP server.").option("--detach", "Spawn a detached background worker and exit immediately.").option("--queue-dir <path>", "Override project directory for queue files").action(async (opts) => {
1406
1786
  const projectDir = resolveQueueDir({
1407
1787
  flagDir: opts.queueDir,
@@ -1447,17 +1827,18 @@ function buildProgram(deps) {
1447
1827
  register4(program, deps);
1448
1828
  register5(program, deps);
1449
1829
  register6(program, deps);
1450
- registerQueue(program, deps);
1451
1830
  register7(program, deps);
1831
+ registerQueue(program, deps);
1832
+ register8(program, deps);
1452
1833
  return program;
1453
1834
  }
1454
1835
 
1455
1836
  // src/index.ts
1456
- var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
1837
+ var __dirname2 = path6.dirname(fileURLToPath2(import.meta.url));
1457
1838
  var require2 = createRequire2(import.meta.url);
1458
1839
  function getVersion() {
1459
1840
  try {
1460
- const pkgPath = path5.resolve(__dirname2, "../package.json");
1841
+ const pkgPath = path6.resolve(__dirname2, "../package.json");
1461
1842
  const pkg = require2(pkgPath);
1462
1843
  return pkg.version ?? "0.0.0";
1463
1844
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nolto/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for Nolto — register plans and update progress from your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",