@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.
- package/dist/index.js +448 -45
- 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
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
574
|
-
|
|
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
|
|
592
|
-
import
|
|
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 =
|
|
996
|
+
const absPath = path4.resolve(filePath);
|
|
595
997
|
let content;
|
|
596
998
|
try {
|
|
597
|
-
content = await
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1042
|
-
import
|
|
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
|
|
1447
|
+
return path5.join(projectDir, ".nolto", "pending.jsonl");
|
|
1046
1448
|
}
|
|
1047
1449
|
function lockFilePath(projectDir) {
|
|
1048
|
-
return
|
|
1450
|
+
return path5.join(projectDir, ".nolto", "flush.lock");
|
|
1049
1451
|
}
|
|
1050
1452
|
function logFilePath(projectDir) {
|
|
1051
|
-
return
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
1102
|
-
await
|
|
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
|
|
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 =
|
|
1140
|
-
await
|
|
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
|
|
1545
|
+
await writeFile3(tmpPath, content, "utf8");
|
|
1144
1546
|
renameSync(tmpPath, filePath);
|
|
1145
1547
|
}
|
|
1146
1548
|
async function acquireLock(projectDir) {
|
|
1147
|
-
const noltoDir =
|
|
1148
|
-
await
|
|
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
|
|
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 =
|
|
1209
|
-
await
|
|
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
|
|
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 =
|
|
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 =
|
|
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 {
|