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