@nolto/cli 0.2.1 → 0.3.1

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 +448 -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,409 @@ 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 mcpUrl = `${opts.baseUrl.replace(/\/+$/, "")}/mcp`;
585
+ const claudePath = await findOnPath("claude");
586
+ if (claudePath) {
587
+ const execFileAsync2 = getExecFileAsync();
588
+ await execFileAsync2(claudePath, ["mcp", "remove", "nolto", "--scope", "user"]).catch(
589
+ () => void 0
590
+ );
591
+ try {
592
+ const { stderr } = await execFileAsync2(claudePath, [
593
+ "mcp",
594
+ "add",
595
+ "nolto",
596
+ mcpUrl,
597
+ "--transport",
598
+ "http",
599
+ "--scope",
600
+ "user",
601
+ "--header",
602
+ `Authorization: Bearer ${opts.token}`
603
+ ]);
604
+ if (stderr) {
605
+ process.stderr.write(`[claude mcp add] ${stderr}
606
+ `);
607
+ }
608
+ process.stdout.write("Registered nolto MCP server in Claude Code (user scope).\n");
609
+ process.stdout.write("Reconnect or restart Claude Code to use it.\n");
610
+ return;
611
+ } catch (err) {
612
+ const msg = err instanceof Error ? err.message : String(err);
613
+ process.stderr.write(
614
+ `Warning: claude mcp add failed (${msg}); writing project .mcp.json instead.
615
+ `
616
+ );
617
+ }
618
+ }
619
+ const cwd = opts.claudeCwd ?? process.cwd();
620
+ const configPath = path3.join(cwd, ".mcp.json");
621
+ const result = await readJsonConfig(configPath);
622
+ const existing = result?.parsed ?? {};
623
+ const updatedServers = {
624
+ ...existing.mcpServers,
625
+ nolto: {
626
+ url: mcpUrl,
627
+ headers: {
628
+ Authorization: `Bearer ${opts.token}`
629
+ },
630
+ env: {
631
+ NOLTO_MCP_TOKEN: opts.token
632
+ }
633
+ }
634
+ };
635
+ const newConfig = {
636
+ ...existing,
637
+ mcpServers: updatedServers
638
+ };
639
+ await atomicWrite(configPath, JSON.stringify(newConfig, null, 2) + "\n", 384);
640
+ process.stdout.write(`Updated ${configPath} (claude project .mcp.json)
641
+ `);
642
+ }
643
+ async function injectCodex(opts) {
644
+ const mcpUrl = `${opts.baseUrl.replace(/\/+$/, "")}/mcp`;
645
+ const codexPath = await findOnPath("codex");
646
+ if (!codexPath) {
647
+ process.stdout.write(
648
+ `
649
+ To add Nolto to Codex, run:
650
+
651
+ codex mcp add nolto --url ${mcpUrl} --bearer-token-env-var NOLTO_TOKEN
652
+
653
+ Then set NOLTO_TOKEN=${opts.token} in your environment.
654
+ `
655
+ );
656
+ return;
657
+ }
658
+ try {
659
+ const execFileAsync2 = getExecFileAsync();
660
+ const { stderr } = await execFileAsync2(codexPath, [
661
+ "mcp",
662
+ "add",
663
+ "nolto",
664
+ "--url",
665
+ mcpUrl,
666
+ "--bearer-token-env-var",
667
+ "NOLTO_TOKEN"
668
+ ]);
669
+ if (stderr) {
670
+ process.stderr.write(`[codex mcp add] ${stderr}
671
+ `);
672
+ }
673
+ process.stdout.write(`Registered nolto MCP server in Codex.
674
+ `);
675
+ process.stdout.write(
676
+ `Set NOLTO_TOKEN=${opts.token.slice(0, 8)}... in your environment (or shell profile).
677
+ `
678
+ );
679
+ } catch (err) {
680
+ const msg = err instanceof Error ? err.message : String(err);
681
+ throw new CliError(`codex mcp add failed: ${msg}`, 5);
682
+ }
683
+ }
684
+ async function findOnPath(bin) {
685
+ try {
686
+ const execFileAsync2 = getExecFileAsync();
687
+ const { stdout } = await execFileAsync2(
688
+ process.platform === "win32" ? "where" : "which",
689
+ [bin]
690
+ );
691
+ const found = stdout.trim().split("\n")[0]?.trim();
692
+ return found && found.length > 0 ? found : null;
693
+ } catch {
694
+ return null;
695
+ }
696
+ }
697
+ async function injectClient(opts) {
698
+ switch (opts.client) {
699
+ case "cursor":
700
+ return injectCursor(opts);
701
+ case "claude":
702
+ return injectClaude(opts);
703
+ case "codex":
704
+ return injectCodex(opts);
705
+ default: {
706
+ const _never = opts.client;
707
+ throw new CliError(`Unknown client: ${String(_never)}`, 2);
708
+ }
709
+ }
710
+ }
711
+
712
+ // src/commands/login.ts
713
+ function register2(program, deps) {
714
+ 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(
715
+ async (opts) => {
716
+ const { configPath, settings, output } = deps;
717
+ const validClients = ["cursor", "claude", "codex"];
718
+ if (opts.client !== void 0 && !validClients.includes(opts.client)) {
719
+ throw new CliError(
720
+ `--client must be one of: ${validClients.join(", ")}. Got: ${opts.client}`,
721
+ 2
722
+ );
723
+ }
724
+ const clientTarget = opts.client;
725
+ if (!opts.force) {
726
+ let existing = null;
727
+ try {
728
+ existing = await loadConfigFile(configPath);
729
+ } catch {
730
+ }
731
+ if (existing?.token) {
732
+ const rl = readline2.createInterface({
733
+ input: process.stdin,
734
+ output: process.stdout
735
+ });
736
+ try {
737
+ const answer = await rl.question(
738
+ `A token is already configured (${maskToken(existing.token)}). Overwrite? [y/N] `
739
+ );
740
+ if (answer.trim().toLowerCase() !== "y") {
741
+ process.stdout.write("Cancelled.\n");
742
+ return;
743
+ }
744
+ } finally {
745
+ rl.close();
746
+ }
747
+ }
748
+ }
749
+ const http = createHttpClient({
750
+ baseUrl: settings.baseUrl,
751
+ version: deps.version
752
+ });
753
+ let startResp;
754
+ try {
755
+ startResp = await http.post("/api/cli/auth/start", {});
756
+ } catch (err) {
757
+ if (err instanceof CliError && err.exitCode === 4) {
758
+ throw new CliError(
759
+ "Rate limit reached. Please wait a few minutes and try again.",
760
+ 4
761
+ );
762
+ }
763
+ throw err;
764
+ }
765
+ process.stdout.write("\n");
766
+ process.stdout.write("Open the following URL in your browser to log in:\n\n");
767
+ process.stdout.write(` ${startResp.verification_url}
768
+
769
+ `);
770
+ process.stdout.write(`Your confirmation code: ${startResp.user_code}
771
+
772
+ `);
773
+ process.stdout.write("Waiting for browser confirmation...\n");
774
+ const token = await pollUntilToken({
775
+ http,
776
+ deviceCode: startResp.device_code,
777
+ intervalSeconds: startResp.interval,
778
+ expiresIn: startResp.expires_in,
779
+ baseUrl: settings.baseUrl
780
+ });
781
+ const existingConfig = await loadConfigFile(configPath).catch(() => null);
782
+ const newConfig = {
783
+ ...existingConfig,
784
+ token
785
+ };
786
+ await saveConfigFile(configPath, newConfig);
787
+ process.stdout.write("\n");
788
+ if (output.mode === "json") {
789
+ process.stdout.write(
790
+ JSON.stringify({ ok: true, token: `...${maskToken(token)}` }, null, 2) + "\n"
791
+ );
792
+ } else {
793
+ process.stdout.write(`Saved ${configPath}
794
+ `);
795
+ process.stdout.write(`token: ${maskToken(token)}
796
+ `);
797
+ }
798
+ if (clientTarget) {
799
+ try {
800
+ await injectClient({
801
+ client: clientTarget,
802
+ token,
803
+ baseUrl: settings.baseUrl
804
+ });
805
+ } catch (err) {
806
+ const msg = err instanceof Error ? err.message : String(err);
807
+ process.stderr.write(
808
+ `Warning: failed to inject into ${clientTarget} config: ${msg}
809
+ `
810
+ );
811
+ }
812
+ }
813
+ }
814
+ );
815
+ }
816
+
414
817
  // src/output.ts
415
818
  function printResult(value, mode2, opts = {}) {
416
819
  const out = opts.stream ?? process.stdout;
@@ -480,7 +883,7 @@ function formatValue(value) {
480
883
  }
481
884
 
482
885
  // src/commands/whoami.ts
483
- function register2(program, deps) {
886
+ function register3(program, deps) {
484
887
  program.command("whoami").description("Show the current authentication and configuration state.").action(async () => {
485
888
  const { settings, output, configPath } = deps;
486
889
  const mode2 = output.mode;
@@ -533,7 +936,7 @@ function assertToken(token) {
533
936
  );
534
937
  }
535
938
  }
536
- function register3(program, deps) {
939
+ function register4(program, deps) {
537
940
  const project = program.command("project").description("Manage projects.");
538
941
  project.command("list").description("List all projects.").action(async () => {
539
942
  assertToken(deps.settings.token);
@@ -570,9 +973,8 @@ function register3(program, deps) {
570
973
  });
571
974
  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
975
  if (opts.local) {
573
- const configPath = getConfigPath(process.env);
574
- const existing = await loadConfigFile(configPath).catch(() => null);
575
- await saveConfigFile(configPath, { ...existing ?? {}, defaultProjectId: projectId });
976
+ const existing = await loadConfigFile(deps.configPath).catch(() => null);
977
+ await saveConfigFile(deps.configPath, { ...existing ?? {}, defaultProjectId: projectId });
576
978
  } else {
577
979
  assertToken(deps.settings.token);
578
980
  await deps.caller.call("set_default_project", { projectId });
@@ -583,18 +985,18 @@ function register3(program, deps) {
583
985
  }
584
986
 
585
987
  // src/commands/plan-register.ts
586
- import { execFile } from "child_process";
587
- import { promisify } from "util";
988
+ import { execFile as execFile2 } from "child_process";
989
+ import { promisify as promisify2 } from "util";
588
990
  import { z as z2 } from "zod";
589
991
 
590
992
  // src/fsx.ts
591
- import { readFile as readFile2, stat as stat2 } from "fs/promises";
592
- import path3 from "path";
993
+ import { readFile as readFile3, stat as stat2 } from "fs/promises";
994
+ import path4 from "path";
593
995
  async function readPlanFile(filePath) {
594
- const absPath = path3.resolve(filePath);
996
+ const absPath = path4.resolve(filePath);
595
997
  let content;
596
998
  try {
597
- content = await readFile2(absPath, "utf8");
999
+ content = await readFile3(absPath, "utf8");
598
1000
  } catch (err) {
599
1001
  const code = err != null && typeof err === "object" && "code" in err ? err.code : "";
600
1002
  if (code === "ENOENT") {
@@ -612,11 +1014,11 @@ async function readPlanFile(filePath) {
612
1014
  2
613
1015
  );
614
1016
  }
615
- const stem = path3.basename(absPath, path3.extname(absPath));
1017
+ const stem = path4.basename(absPath, path4.extname(absPath));
616
1018
  return { content: trimmed, sourcePath: absPath, titleFallback: stem };
617
1019
  }
618
1020
  async function readDocFile(filePath) {
619
- const absPath = path3.resolve(filePath);
1021
+ const absPath = path4.resolve(filePath);
620
1022
  let fileSize;
621
1023
  try {
622
1024
  const info = await stat2(absPath);
@@ -633,12 +1035,12 @@ async function readDocFile(filePath) {
633
1035
  }
634
1036
  let buf;
635
1037
  try {
636
- buf = await readFile2(absPath);
1038
+ buf = await readFile3(absPath);
637
1039
  } catch (err) {
638
1040
  throw new CliError(`Cannot read file: ${absPath}: ${String(err)}`, 2);
639
1041
  }
640
1042
  const isBinary = buf.includes(0) || !isUtf8RoundTrip(buf);
641
- const rawFilename = path3.basename(absPath);
1043
+ const rawFilename = path4.basename(absPath);
642
1044
  const filename = rawFilename.slice(0, DOCUMENT_FILENAME_MAX);
643
1045
  if (isBinary) {
644
1046
  return { content: buf.toString("base64"), encoding: "base64", filename };
@@ -695,7 +1097,7 @@ function extractTitle(md) {
695
1097
  }
696
1098
 
697
1099
  // src/commands/plan-register.ts
698
- var execFileAsync = promisify(execFile);
1100
+ var execFileAsync = promisify2(execFile2);
699
1101
  function assertToken2(token) {
700
1102
  if (token == null) {
701
1103
  throw new CliError(
@@ -842,7 +1244,7 @@ function assertToken3(token) {
842
1244
  );
843
1245
  }
844
1246
  }
845
- function register4(program, deps) {
1247
+ function register5(program, deps) {
846
1248
  const plan = program.command("plan").description("Manage plans.");
847
1249
  plan.command("list").description("List plans.").option("--status <status>", "Filter by status").action(async (opts) => {
848
1250
  assertToken3(deps.settings.token);
@@ -930,7 +1332,7 @@ function assertToken4(token) {
930
1332
  );
931
1333
  }
932
1334
  }
933
- function register5(program, deps) {
1335
+ function register6(program, deps) {
934
1336
  const phase = program.command("phase").description("Manage plan phases.");
935
1337
  phase.command("status <planId> <phaseId> <status>").description("Update a phase status.").option("--message <text>", "Optional message").action(async (planId, phaseId, status, opts) => {
936
1338
  assertToken4(deps.settings.token);
@@ -1004,7 +1406,7 @@ function assertToken5(token) {
1004
1406
  );
1005
1407
  }
1006
1408
  }
1007
- function register6(program, deps) {
1409
+ function register7(program, deps) {
1008
1410
  const doc = program.command("doc").description("Manage plan documents.");
1009
1411
  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
1412
  assertToken5(deps.settings.token);
@@ -1038,17 +1440,17 @@ function register6(program, deps) {
1038
1440
 
1039
1441
  // src/queue-file.ts
1040
1442
  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";
1443
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3, unlink as unlink2, appendFile } from "fs/promises";
1444
+ import path5 from "path";
1043
1445
  import crypto from "crypto";
1044
1446
  function queueFilePath(projectDir) {
1045
- return path4.join(projectDir, ".nolto", "pending.jsonl");
1447
+ return path5.join(projectDir, ".nolto", "pending.jsonl");
1046
1448
  }
1047
1449
  function lockFilePath(projectDir) {
1048
- return path4.join(projectDir, ".nolto", "flush.lock");
1450
+ return path5.join(projectDir, ".nolto", "flush.lock");
1049
1451
  }
1050
1452
  function logFilePath(projectDir) {
1051
- return path4.join(projectDir, ".nolto", "flush.log");
1453
+ return path5.join(projectDir, ".nolto", "flush.log");
1052
1454
  }
1053
1455
  function resolveQueueDir(inputs) {
1054
1456
  if (inputs.flagDir != null) return inputs.flagDir;
@@ -1062,7 +1464,7 @@ function findAncestorWithMarker(startDir) {
1062
1464
  if (hasMarkerSync(current, ".git") || hasMarkerSync(current, ".nolto")) {
1063
1465
  return current;
1064
1466
  }
1065
- const parent = path4.dirname(current);
1467
+ const parent = path5.dirname(current);
1066
1468
  if (parent === current) break;
1067
1469
  current = parent;
1068
1470
  }
@@ -1070,7 +1472,7 @@ function findAncestorWithMarker(startDir) {
1070
1472
  }
1071
1473
  function hasMarkerSync(dir, marker) {
1072
1474
  try {
1073
- statSync(path4.join(dir, marker));
1475
+ statSync(path5.join(dir, marker));
1074
1476
  return true;
1075
1477
  } catch {
1076
1478
  return false;
@@ -1080,7 +1482,7 @@ async function readQueue(projectDir) {
1080
1482
  const filePath = queueFilePath(projectDir);
1081
1483
  let raw;
1082
1484
  try {
1083
- raw = await readFile3(filePath, "utf8");
1485
+ raw = await readFile4(filePath, "utf8");
1084
1486
  } catch {
1085
1487
  return [];
1086
1488
  }
@@ -1098,8 +1500,8 @@ async function readQueue(projectDir) {
1098
1500
  return entries;
1099
1501
  }
1100
1502
  async function appendEntry(projectDir, entry) {
1101
- const noltoDir = path4.join(projectDir, ".nolto");
1102
- await mkdir2(noltoDir, { recursive: true, mode: 448 });
1503
+ const noltoDir = path5.join(projectDir, ".nolto");
1504
+ await mkdir3(noltoDir, { recursive: true, mode: 448 });
1103
1505
  const existing = await readQueue(projectDir);
1104
1506
  if (existing.length >= QUEUE_MAX_ENTRIES) {
1105
1507
  throw new CliError(
@@ -1129,23 +1531,23 @@ async function atomicRewriteQueue(projectDir, entries) {
1129
1531
  const filePath = queueFilePath(projectDir);
1130
1532
  if (entries.length === 0) {
1131
1533
  try {
1132
- await unlink(filePath);
1534
+ await unlink2(filePath);
1133
1535
  } catch (err) {
1134
1536
  const code = err.code;
1135
1537
  if (code !== "ENOENT") throw err;
1136
1538
  }
1137
1539
  return;
1138
1540
  }
1139
- const noltoDir = path4.join(projectDir, ".nolto");
1140
- await mkdir2(noltoDir, { recursive: true });
1541
+ const noltoDir = path5.join(projectDir, ".nolto");
1542
+ await mkdir3(noltoDir, { recursive: true });
1141
1543
  const tmpPath = filePath + ".tmp";
1142
1544
  const content = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
1143
- await writeFile2(tmpPath, content, "utf8");
1545
+ await writeFile3(tmpPath, content, "utf8");
1144
1546
  renameSync(tmpPath, filePath);
1145
1547
  }
1146
1548
  async function acquireLock(projectDir) {
1147
- const noltoDir = path4.join(projectDir, ".nolto");
1148
- await mkdir2(noltoDir, { recursive: true });
1549
+ const noltoDir = path5.join(projectDir, ".nolto");
1550
+ await mkdir3(noltoDir, { recursive: true });
1149
1551
  const lockPath = lockFilePath(projectDir);
1150
1552
  return tryAcquire(lockPath);
1151
1553
  }
@@ -1160,7 +1562,7 @@ async function tryAcquire(lockPath) {
1160
1562
  }
1161
1563
  let pidStr;
1162
1564
  try {
1163
- pidStr = await readFile3(lockPath, "utf8");
1565
+ pidStr = await readFile4(lockPath, "utf8");
1164
1566
  } catch {
1165
1567
  try {
1166
1568
  unlinkSync(lockPath);
@@ -1205,8 +1607,8 @@ function makeLockHandle(lockPath) {
1205
1607
  }
1206
1608
  async function appendLog(projectDir, level, message) {
1207
1609
  try {
1208
- const noltoDir = path4.join(projectDir, ".nolto");
1209
- await mkdir2(noltoDir, { recursive: true });
1610
+ const noltoDir = path5.join(projectDir, ".nolto");
1611
+ await mkdir3(noltoDir, { recursive: true });
1210
1612
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${message}
1211
1613
  `;
1212
1614
  await appendFile(logFilePath(projectDir), line, "utf8");
@@ -1413,7 +1815,7 @@ async function runFlushWorker(deps, projectDir) {
1413
1815
  await lock.release();
1414
1816
  }
1415
1817
  }
1416
- function register7(program, deps) {
1818
+ function register8(program, deps) {
1417
1819
  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
1820
  const projectDir = resolveQueueDir({
1419
1821
  flagDir: opts.queueDir,
@@ -1459,17 +1861,18 @@ function buildProgram(deps) {
1459
1861
  register4(program, deps);
1460
1862
  register5(program, deps);
1461
1863
  register6(program, deps);
1462
- registerQueue(program, deps);
1463
1864
  register7(program, deps);
1865
+ registerQueue(program, deps);
1866
+ register8(program, deps);
1464
1867
  return program;
1465
1868
  }
1466
1869
 
1467
1870
  // src/index.ts
1468
- var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
1871
+ var __dirname2 = path6.dirname(fileURLToPath2(import.meta.url));
1469
1872
  var require2 = createRequire2(import.meta.url);
1470
1873
  function getVersion() {
1471
1874
  try {
1472
- const pkgPath = path5.resolve(__dirname2, "../package.json");
1875
+ const pkgPath = path6.resolve(__dirname2, "../package.json");
1473
1876
  const pkg = require2(pkgPath);
1474
1877
  return pkg.version ?? "0.0.0";
1475
1878
  } 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.1",
4
4
  "description": "CLI for Nolto — register plans and update progress from your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",