@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.
- package/dist/index.js +414 -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,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
|
|
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
|
|
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
|
|
574
|
-
|
|
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
|
|
592
|
-
import
|
|
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 =
|
|
962
|
+
const absPath = path4.resolve(filePath);
|
|
595
963
|
let content;
|
|
596
964
|
try {
|
|
597
|
-
content = await
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1042
|
-
import
|
|
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
|
|
1413
|
+
return path5.join(projectDir, ".nolto", "pending.jsonl");
|
|
1046
1414
|
}
|
|
1047
1415
|
function lockFilePath(projectDir) {
|
|
1048
|
-
return
|
|
1416
|
+
return path5.join(projectDir, ".nolto", "flush.lock");
|
|
1049
1417
|
}
|
|
1050
1418
|
function logFilePath(projectDir) {
|
|
1051
|
-
return
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
1102
|
-
await
|
|
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
|
|
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 =
|
|
1140
|
-
await
|
|
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
|
|
1511
|
+
await writeFile3(tmpPath, content, "utf8");
|
|
1144
1512
|
renameSync(tmpPath, filePath);
|
|
1145
1513
|
}
|
|
1146
1514
|
async function acquireLock(projectDir) {
|
|
1147
|
-
const noltoDir =
|
|
1148
|
-
await
|
|
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
|
|
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 =
|
|
1209
|
-
await
|
|
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
|
|
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 =
|
|
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 =
|
|
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 {
|