@nolto/cli 0.2.1 → 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 +414 -45
  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
@@ -311,9 +311,9 @@ function getCliVersion() {
311
311
  }
312
312
  return "0.0.0";
313
313
  }
314
- function register(program, _deps) {
314
+ function register(program, deps) {
315
315
  program.command("init").description("Interactive setup: configure token, base URL, and default project.").option("--force", "Overwrite existing config without prompting").action(async (opts) => {
316
- const configPath = getConfigPath(process.env);
316
+ const configPath = deps.configPath;
317
317
  if (!opts.force) {
318
318
  let existing = null;
319
319
  try {
@@ -411,6 +411,375 @@ async function promptHidden(rl, prompt) {
411
411
  return value;
412
412
  }
413
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
+
414
783
  // src/output.ts
415
784
  function printResult(value, mode2, opts = {}) {
416
785
  const out = opts.stream ?? process.stdout;
@@ -480,7 +849,7 @@ function formatValue(value) {
480
849
  }
481
850
 
482
851
  // src/commands/whoami.ts
483
- function register2(program, deps) {
852
+ function register3(program, deps) {
484
853
  program.command("whoami").description("Show the current authentication and configuration state.").action(async () => {
485
854
  const { settings, output, configPath } = deps;
486
855
  const mode2 = output.mode;
@@ -533,7 +902,7 @@ function assertToken(token) {
533
902
  );
534
903
  }
535
904
  }
536
- function register3(program, deps) {
905
+ function register4(program, deps) {
537
906
  const project = program.command("project").description("Manage projects.");
538
907
  project.command("list").description("List all projects.").action(async () => {
539
908
  assertToken(deps.settings.token);
@@ -570,9 +939,8 @@ function register3(program, deps) {
570
939
  });
571
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) => {
572
941
  if (opts.local) {
573
- const configPath = getConfigPath(process.env);
574
- const existing = await loadConfigFile(configPath).catch(() => null);
575
- await saveConfigFile(configPath, { ...existing ?? {}, defaultProjectId: projectId });
942
+ const existing = await loadConfigFile(deps.configPath).catch(() => null);
943
+ await saveConfigFile(deps.configPath, { ...existing ?? {}, defaultProjectId: projectId });
576
944
  } else {
577
945
  assertToken(deps.settings.token);
578
946
  await deps.caller.call("set_default_project", { projectId });
@@ -583,18 +951,18 @@ function register3(program, deps) {
583
951
  }
584
952
 
585
953
  // src/commands/plan-register.ts
586
- import { execFile } from "child_process";
587
- import { promisify } from "util";
954
+ import { execFile as execFile2 } from "child_process";
955
+ import { promisify as promisify2 } from "util";
588
956
  import { z as z2 } from "zod";
589
957
 
590
958
  // src/fsx.ts
591
- import { readFile as readFile2, stat as stat2 } from "fs/promises";
592
- import path3 from "path";
959
+ import { readFile as readFile3, stat as stat2 } from "fs/promises";
960
+ import path4 from "path";
593
961
  async function readPlanFile(filePath) {
594
- const absPath = path3.resolve(filePath);
962
+ const absPath = path4.resolve(filePath);
595
963
  let content;
596
964
  try {
597
- content = await readFile2(absPath, "utf8");
965
+ content = await readFile3(absPath, "utf8");
598
966
  } catch (err) {
599
967
  const code = err != null && typeof err === "object" && "code" in err ? err.code : "";
600
968
  if (code === "ENOENT") {
@@ -612,11 +980,11 @@ async function readPlanFile(filePath) {
612
980
  2
613
981
  );
614
982
  }
615
- const stem = path3.basename(absPath, path3.extname(absPath));
983
+ const stem = path4.basename(absPath, path4.extname(absPath));
616
984
  return { content: trimmed, sourcePath: absPath, titleFallback: stem };
617
985
  }
618
986
  async function readDocFile(filePath) {
619
- const absPath = path3.resolve(filePath);
987
+ const absPath = path4.resolve(filePath);
620
988
  let fileSize;
621
989
  try {
622
990
  const info = await stat2(absPath);
@@ -633,12 +1001,12 @@ async function readDocFile(filePath) {
633
1001
  }
634
1002
  let buf;
635
1003
  try {
636
- buf = await readFile2(absPath);
1004
+ buf = await readFile3(absPath);
637
1005
  } catch (err) {
638
1006
  throw new CliError(`Cannot read file: ${absPath}: ${String(err)}`, 2);
639
1007
  }
640
1008
  const isBinary = buf.includes(0) || !isUtf8RoundTrip(buf);
641
- const rawFilename = path3.basename(absPath);
1009
+ const rawFilename = path4.basename(absPath);
642
1010
  const filename = rawFilename.slice(0, DOCUMENT_FILENAME_MAX);
643
1011
  if (isBinary) {
644
1012
  return { content: buf.toString("base64"), encoding: "base64", filename };
@@ -695,7 +1063,7 @@ function extractTitle(md) {
695
1063
  }
696
1064
 
697
1065
  // src/commands/plan-register.ts
698
- var execFileAsync = promisify(execFile);
1066
+ var execFileAsync = promisify2(execFile2);
699
1067
  function assertToken2(token) {
700
1068
  if (token == null) {
701
1069
  throw new CliError(
@@ -842,7 +1210,7 @@ function assertToken3(token) {
842
1210
  );
843
1211
  }
844
1212
  }
845
- function register4(program, deps) {
1213
+ function register5(program, deps) {
846
1214
  const plan = program.command("plan").description("Manage plans.");
847
1215
  plan.command("list").description("List plans.").option("--status <status>", "Filter by status").action(async (opts) => {
848
1216
  assertToken3(deps.settings.token);
@@ -930,7 +1298,7 @@ function assertToken4(token) {
930
1298
  );
931
1299
  }
932
1300
  }
933
- function register5(program, deps) {
1301
+ function register6(program, deps) {
934
1302
  const phase = program.command("phase").description("Manage plan phases.");
935
1303
  phase.command("status <planId> <phaseId> <status>").description("Update a phase status.").option("--message <text>", "Optional message").action(async (planId, phaseId, status, opts) => {
936
1304
  assertToken4(deps.settings.token);
@@ -1004,7 +1372,7 @@ function assertToken5(token) {
1004
1372
  );
1005
1373
  }
1006
1374
  }
1007
- function register6(program, deps) {
1375
+ function register7(program, deps) {
1008
1376
  const doc = program.command("doc").description("Manage plan documents.");
1009
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) => {
1010
1378
  assertToken5(deps.settings.token);
@@ -1038,17 +1406,17 @@ function register6(program, deps) {
1038
1406
 
1039
1407
  // src/queue-file.ts
1040
1408
  import { openSync, closeSync, writeFileSync, unlinkSync, renameSync, statSync } from "fs";
1041
- import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, unlink, appendFile } from "fs/promises";
1042
- 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";
1043
1411
  import crypto from "crypto";
1044
1412
  function queueFilePath(projectDir) {
1045
- return path4.join(projectDir, ".nolto", "pending.jsonl");
1413
+ return path5.join(projectDir, ".nolto", "pending.jsonl");
1046
1414
  }
1047
1415
  function lockFilePath(projectDir) {
1048
- return path4.join(projectDir, ".nolto", "flush.lock");
1416
+ return path5.join(projectDir, ".nolto", "flush.lock");
1049
1417
  }
1050
1418
  function logFilePath(projectDir) {
1051
- return path4.join(projectDir, ".nolto", "flush.log");
1419
+ return path5.join(projectDir, ".nolto", "flush.log");
1052
1420
  }
1053
1421
  function resolveQueueDir(inputs) {
1054
1422
  if (inputs.flagDir != null) return inputs.flagDir;
@@ -1062,7 +1430,7 @@ function findAncestorWithMarker(startDir) {
1062
1430
  if (hasMarkerSync(current, ".git") || hasMarkerSync(current, ".nolto")) {
1063
1431
  return current;
1064
1432
  }
1065
- const parent = path4.dirname(current);
1433
+ const parent = path5.dirname(current);
1066
1434
  if (parent === current) break;
1067
1435
  current = parent;
1068
1436
  }
@@ -1070,7 +1438,7 @@ function findAncestorWithMarker(startDir) {
1070
1438
  }
1071
1439
  function hasMarkerSync(dir, marker) {
1072
1440
  try {
1073
- statSync(path4.join(dir, marker));
1441
+ statSync(path5.join(dir, marker));
1074
1442
  return true;
1075
1443
  } catch {
1076
1444
  return false;
@@ -1080,7 +1448,7 @@ async function readQueue(projectDir) {
1080
1448
  const filePath = queueFilePath(projectDir);
1081
1449
  let raw;
1082
1450
  try {
1083
- raw = await readFile3(filePath, "utf8");
1451
+ raw = await readFile4(filePath, "utf8");
1084
1452
  } catch {
1085
1453
  return [];
1086
1454
  }
@@ -1098,8 +1466,8 @@ async function readQueue(projectDir) {
1098
1466
  return entries;
1099
1467
  }
1100
1468
  async function appendEntry(projectDir, entry) {
1101
- const noltoDir = path4.join(projectDir, ".nolto");
1102
- await mkdir2(noltoDir, { recursive: true, mode: 448 });
1469
+ const noltoDir = path5.join(projectDir, ".nolto");
1470
+ await mkdir3(noltoDir, { recursive: true, mode: 448 });
1103
1471
  const existing = await readQueue(projectDir);
1104
1472
  if (existing.length >= QUEUE_MAX_ENTRIES) {
1105
1473
  throw new CliError(
@@ -1129,23 +1497,23 @@ async function atomicRewriteQueue(projectDir, entries) {
1129
1497
  const filePath = queueFilePath(projectDir);
1130
1498
  if (entries.length === 0) {
1131
1499
  try {
1132
- await unlink(filePath);
1500
+ await unlink2(filePath);
1133
1501
  } catch (err) {
1134
1502
  const code = err.code;
1135
1503
  if (code !== "ENOENT") throw err;
1136
1504
  }
1137
1505
  return;
1138
1506
  }
1139
- const noltoDir = path4.join(projectDir, ".nolto");
1140
- await mkdir2(noltoDir, { recursive: true });
1507
+ const noltoDir = path5.join(projectDir, ".nolto");
1508
+ await mkdir3(noltoDir, { recursive: true });
1141
1509
  const tmpPath = filePath + ".tmp";
1142
1510
  const content = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
1143
- await writeFile2(tmpPath, content, "utf8");
1511
+ await writeFile3(tmpPath, content, "utf8");
1144
1512
  renameSync(tmpPath, filePath);
1145
1513
  }
1146
1514
  async function acquireLock(projectDir) {
1147
- const noltoDir = path4.join(projectDir, ".nolto");
1148
- await mkdir2(noltoDir, { recursive: true });
1515
+ const noltoDir = path5.join(projectDir, ".nolto");
1516
+ await mkdir3(noltoDir, { recursive: true });
1149
1517
  const lockPath = lockFilePath(projectDir);
1150
1518
  return tryAcquire(lockPath);
1151
1519
  }
@@ -1160,7 +1528,7 @@ async function tryAcquire(lockPath) {
1160
1528
  }
1161
1529
  let pidStr;
1162
1530
  try {
1163
- pidStr = await readFile3(lockPath, "utf8");
1531
+ pidStr = await readFile4(lockPath, "utf8");
1164
1532
  } catch {
1165
1533
  try {
1166
1534
  unlinkSync(lockPath);
@@ -1205,8 +1573,8 @@ function makeLockHandle(lockPath) {
1205
1573
  }
1206
1574
  async function appendLog(projectDir, level, message) {
1207
1575
  try {
1208
- const noltoDir = path4.join(projectDir, ".nolto");
1209
- await mkdir2(noltoDir, { recursive: true });
1576
+ const noltoDir = path5.join(projectDir, ".nolto");
1577
+ await mkdir3(noltoDir, { recursive: true });
1210
1578
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}
1211
1579
  `;
1212
1580
  await appendFile(logFilePath(projectDir), line, "utf8");
@@ -1413,7 +1781,7 @@ async function runFlushWorker(deps, projectDir) {
1413
1781
  await lock.release();
1414
1782
  }
1415
1783
  }
1416
- function register7(program, deps) {
1784
+ function register8(program, deps) {
1417
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) => {
1418
1786
  const projectDir = resolveQueueDir({
1419
1787
  flagDir: opts.queueDir,
@@ -1459,17 +1827,18 @@ function buildProgram(deps) {
1459
1827
  register4(program, deps);
1460
1828
  register5(program, deps);
1461
1829
  register6(program, deps);
1462
- registerQueue(program, deps);
1463
1830
  register7(program, deps);
1831
+ registerQueue(program, deps);
1832
+ register8(program, deps);
1464
1833
  return program;
1465
1834
  }
1466
1835
 
1467
1836
  // src/index.ts
1468
- var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
1837
+ var __dirname2 = path6.dirname(fileURLToPath2(import.meta.url));
1469
1838
  var require2 = createRequire2(import.meta.url);
1470
1839
  function getVersion() {
1471
1840
  try {
1472
- const pkgPath = path5.resolve(__dirname2, "../package.json");
1841
+ const pkgPath = path6.resolve(__dirname2, "../package.json");
1473
1842
  const pkg = require2(pkgPath);
1474
1843
  return pkg.version ?? "0.0.0";
1475
1844
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nolto/cli",
3
- "version": "0.2.1",
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",