@noir-ai/cli 1.8.0 → 1.9.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/bin.js CHANGED
@@ -1181,6 +1181,264 @@ function shouldShowBanner(opts) {
1181
1181
  return true;
1182
1182
  }
1183
1183
 
1184
+ // src/tui/commands/registry.ts
1185
+ var DESTRUCTIVE_PREFIXES = [
1186
+ ["context", "index"],
1187
+ ["context", "forget"],
1188
+ ["memory", "save"],
1189
+ ["memory", "forget"],
1190
+ ["memory", "consolidate"],
1191
+ ["task", "new"],
1192
+ ["task", "advance"],
1193
+ ["skills", "sync"],
1194
+ ["daemon", "start"],
1195
+ ["daemon", "stop"],
1196
+ ["daemon", "restart"],
1197
+ ["init"],
1198
+ ["create"],
1199
+ ["sync"],
1200
+ ["install"],
1201
+ ["update"]
1202
+ ];
1203
+ function isDestructive(argv) {
1204
+ return DESTRUCTIVE_PREFIXES.some((prefix) => {
1205
+ if (argv.length < prefix.length) return false;
1206
+ return prefix.every((tok, i) => argv[i] === tok);
1207
+ });
1208
+ }
1209
+ function capitalizeFirst(s) {
1210
+ if (s.length === 0) return s;
1211
+ return `${s[0]?.toUpperCase() ?? ""}${s.slice(1)}`;
1212
+ }
1213
+ function hasAction(cmd) {
1214
+ return cmd._actionHandler != null;
1215
+ }
1216
+ function collectLeaves(cmd, path, out) {
1217
+ const children = cmd.commands;
1218
+ const isLeaf = hasAction(cmd) && children.length === 0;
1219
+ if (isLeaf) {
1220
+ const argv = path;
1221
+ const id = argv.join(" ");
1222
+ const description = cmd.description() || argv.join(" ");
1223
+ out.push({
1224
+ id,
1225
+ label: argv.map(capitalizeFirst).join(": "),
1226
+ argv,
1227
+ description,
1228
+ category: argv[0] ?? "",
1229
+ keywords: argv,
1230
+ destructive: isDestructive(argv)
1231
+ });
1232
+ return;
1233
+ }
1234
+ for (const child of children) {
1235
+ collectLeaves(child, [...path, child.name()], out);
1236
+ }
1237
+ }
1238
+ function buildPaletteCommands(program2) {
1239
+ const out = [];
1240
+ collectLeaves(program2, [], out);
1241
+ return out;
1242
+ }
1243
+
1244
+ // src/tui/commands/sections.ts
1245
+ var HOME_SECTIONS = [
1246
+ {
1247
+ id: "status",
1248
+ label: "Status & context",
1249
+ hint: "snapshot \xB7 index \xB7 search \xB7 doctor",
1250
+ key: "1",
1251
+ items: [
1252
+ {
1253
+ id: "status",
1254
+ label: "Status",
1255
+ hint: "project + daemon + workflow + store snapshot (noir status)"
1256
+ },
1257
+ {
1258
+ id: "context index",
1259
+ label: "Index project",
1260
+ hint: "(re)index files into context \xB7 \u26A0 destructive",
1261
+ destructive: true
1262
+ },
1263
+ {
1264
+ id: "context search",
1265
+ label: "Search context",
1266
+ hint: "hybrid search over indexed files",
1267
+ needsArg: { prompt: "Context search query:", placeholder: "e.g. auth flow, deploy steps" }
1268
+ },
1269
+ {
1270
+ id: "doctor",
1271
+ label: "Doctor",
1272
+ hint: "environment + project health checks"
1273
+ },
1274
+ {
1275
+ id: "context status",
1276
+ label: "Context status",
1277
+ hint: "index freshness + counts"
1278
+ }
1279
+ ]
1280
+ },
1281
+ {
1282
+ id: "memory",
1283
+ label: "Memory",
1284
+ hint: "recall \xB7 save \xB7 sessions \xB7 forget \xB7 consolidate",
1285
+ key: "2",
1286
+ items: [
1287
+ {
1288
+ id: "memory recall",
1289
+ label: "Recall memory",
1290
+ hint: "search cross-session memory",
1291
+ needsArg: { prompt: "Recall query:", placeholder: "e.g. auth flow, deploy steps" }
1292
+ },
1293
+ {
1294
+ id: "memory save",
1295
+ label: "Save memory",
1296
+ hint: "save an observation \xB7 \u26A0 destructive",
1297
+ destructive: true,
1298
+ needsArg: { prompt: "Memory content:", placeholder: "what you want to remember" }
1299
+ },
1300
+ {
1301
+ id: "memory sessions",
1302
+ label: "Sessions",
1303
+ hint: "list recent memory sessions"
1304
+ },
1305
+ {
1306
+ id: "memory forget",
1307
+ label: "Forget memory",
1308
+ hint: "forget observations by id \xB7 \u26A0 destructive",
1309
+ destructive: true
1310
+ },
1311
+ {
1312
+ id: "memory consolidate",
1313
+ label: "Consolidate",
1314
+ hint: "fold observations into lessons \xB7 \u26A0 destructive",
1315
+ destructive: true
1316
+ }
1317
+ ]
1318
+ },
1319
+ {
1320
+ id: "workflow",
1321
+ label: "Workflow",
1322
+ hint: "next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
1323
+ key: "3",
1324
+ items: [
1325
+ {
1326
+ id: "task next",
1327
+ label: "Next task",
1328
+ hint: "suggest next phase + applicable skill"
1329
+ },
1330
+ {
1331
+ id: "task status",
1332
+ label: "Task status",
1333
+ hint: "active task status"
1334
+ },
1335
+ {
1336
+ id: "task advance",
1337
+ label: "Advance task",
1338
+ hint: "advance to next phase \xB7 \u26A0 destructive",
1339
+ destructive: true
1340
+ },
1341
+ {
1342
+ id: "handoff",
1343
+ label: "Handoff",
1344
+ hint: "ready-to-paste host prompt"
1345
+ },
1346
+ {
1347
+ id: "wrap",
1348
+ label: "Wrap (end session)",
1349
+ hint: "session-end handoff",
1350
+ dispatch: ["wrap", "--write"]
1351
+ }
1352
+ ]
1353
+ },
1354
+ {
1355
+ id: "setup",
1356
+ label: "Setup & maintenance",
1357
+ hint: "init \xB7 create \xB7 sync \xB7 skills \xB7 install \xB7 update",
1358
+ key: "4",
1359
+ items: [
1360
+ {
1361
+ id: "init",
1362
+ label: "Init",
1363
+ hint: "scaffold Noir in this project \xB7 \u26A0 destructive",
1364
+ destructive: true
1365
+ },
1366
+ {
1367
+ id: "create",
1368
+ label: "Create",
1369
+ hint: "bootstrap in a new/empty dir \xB7 \u26A0 destructive",
1370
+ destructive: true
1371
+ },
1372
+ {
1373
+ id: "sync",
1374
+ label: "Sync",
1375
+ hint: "re-emit managed files + skills \xB7 \u26A0 destructive",
1376
+ destructive: true
1377
+ },
1378
+ {
1379
+ id: "skills list",
1380
+ label: "Skills list",
1381
+ hint: "list installed skills"
1382
+ },
1383
+ {
1384
+ id: "skills sync",
1385
+ label: "Sync skills",
1386
+ hint: "re-emit builtin skills \xB7 \u26A0 destructive",
1387
+ destructive: true
1388
+ },
1389
+ {
1390
+ id: "update",
1391
+ label: "Update",
1392
+ hint: "check for a new version \xB7 \u26A0 destructive",
1393
+ destructive: true
1394
+ },
1395
+ {
1396
+ id: "install",
1397
+ label: "Install / migrate",
1398
+ hint: "native install / migrate \xB7 \u26A0 destructive",
1399
+ destructive: true
1400
+ }
1401
+ ]
1402
+ },
1403
+ {
1404
+ id: "dashboard",
1405
+ label: "Dashboard (full-screen)",
1406
+ hint: "noir tui \xB7 noir palette \xB7 home",
1407
+ key: "5",
1408
+ items: [
1409
+ {
1410
+ id: "tui",
1411
+ label: "Dashboard (full-screen)",
1412
+ hint: "live status \xB7 /command \xB7 Ctrl+K palette (noir tui)"
1413
+ },
1414
+ {
1415
+ id: "palette",
1416
+ label: "All commands (fuzzy palette)",
1417
+ hint: "fuzzy search over every command (noir palette)"
1418
+ }
1419
+ ]
1420
+ }
1421
+ ];
1422
+ async function resolveSections(commands) {
1423
+ const byId = new Map(commands.map((c2) => [c2.id, c2]));
1424
+ const out = [];
1425
+ for (const section of HOME_SECTIONS) {
1426
+ const items = [];
1427
+ for (const action of section.items) {
1428
+ const entry = byId.get(action.id);
1429
+ if (!entry) continue;
1430
+ items.push({
1431
+ ...action,
1432
+ // Resolve the dispatch argv from the registry (never hand-written).
1433
+ dispatch: action.dispatch ?? [...entry.argv]
1434
+ });
1435
+ }
1436
+ if (items.length === 0) continue;
1437
+ out.push({ ...section, items });
1438
+ }
1439
+ return out;
1440
+ }
1441
+
1184
1442
  // src/commands/home.ts
1185
1443
  function tryProject() {
1186
1444
  try {
@@ -1195,7 +1453,7 @@ function shouldShowMigrationBanner(rec, currentVersion) {
1195
1453
  if (rec.dismissedVersions?.includes(currentVersion)) return false;
1196
1454
  return true;
1197
1455
  }
1198
- var COMMANDS_HINT = "Commands: init \xB7 create \xB7 sync \xB7 status \xB7 context \xB7 memory \xB7 skills \xB7 task \xB7 daemon \xB7 doctor";
1456
+ var COMMANDS_HINT = "Commands: status \xB7 context \xB7 memory \xB7 task \xB7 skills \xB7 daemon \xB7 doctor \xB7 install \xB7 update \xB7 tui \xB7 palette";
1199
1457
  async function home(opts, deps) {
1200
1458
  if (isInteractive(opts) && opts.tui !== false) {
1201
1459
  await runMenu(opts, deps);
@@ -1207,6 +1465,138 @@ async function home(opts, deps) {
1207
1465
  }
1208
1466
  await deps.dispatch(["status"]);
1209
1467
  }
1468
+ async function pickSection(clack, sections, opts) {
1469
+ const result = await clack.selectKey({
1470
+ message: "What would you like to do?",
1471
+ options: [
1472
+ ...sections.map((s) => ({
1473
+ value: s.id,
1474
+ label: s.label,
1475
+ hint: s.hint
1476
+ })),
1477
+ { value: "exit", label: "Exit", hint: "leave the home menu" }
1478
+ ]
1479
+ });
1480
+ if (clack.isCancel(result)) {
1481
+ clack.cancel("Cancelled.");
1482
+ fail(EXIT.CANCELLED, "cancelled", opts);
1483
+ }
1484
+ return result;
1485
+ }
1486
+ async function pickAction(clack, section, sections, opts) {
1487
+ const idx = sections.findIndex((s) => s.id === section.id);
1488
+ const hasPrev = idx > 0;
1489
+ const hasNext = idx !== -1 && idx < sections.length - 1;
1490
+ const result = await clack.select({
1491
+ message: section.label,
1492
+ initialValue: section.items[0]?.id,
1493
+ options: [
1494
+ ...section.items.map((a) => ({
1495
+ value: a.id,
1496
+ label: a.label,
1497
+ hint: a.hint
1498
+ })),
1499
+ // Navigation options so the user can move across sections without
1500
+ // re-entering the section picker (the "lancar" navigation requirement).
1501
+ ...hasPrev ? [{ value: "prev", label: "\u2190 Prev section", hint: sections[idx - 1]?.label }] : [],
1502
+ ...hasNext ? [{ value: "next", label: "\u2192 Next section", hint: sections[idx + 1]?.label }] : [],
1503
+ { value: "back", label: "\u2191 Back to sections", hint: "return to the section picker" }
1504
+ ]
1505
+ });
1506
+ if (clack.isCancel(result)) {
1507
+ clack.cancel("Back to the home menu.");
1508
+ return "back";
1509
+ }
1510
+ if (opts.verbose) {
1511
+ process.stderr.write(`noir: home: section '${section.id}' selected
1512
+ `);
1513
+ }
1514
+ return result;
1515
+ }
1516
+ async function collectArg(clack, action, opts) {
1517
+ if (!action.needsArg) return null;
1518
+ const value = await clack.text({
1519
+ message: action.needsArg.prompt,
1520
+ placeholder: action.needsArg.placeholder
1521
+ });
1522
+ if (clack.isCancel(value)) {
1523
+ clack.cancel("Cancelled.");
1524
+ fail(EXIT.CANCELLED, "cancelled", opts);
1525
+ }
1526
+ return String(value);
1527
+ }
1528
+ async function argvForAction(clack, action, opts) {
1529
+ const base = action.dispatch ?? [action.id];
1530
+ let argv = [...base];
1531
+ if (action.needsArg) {
1532
+ const arg = await collectArg(clack, action, opts);
1533
+ if (arg === null) return null;
1534
+ argv = [...base, arg];
1535
+ }
1536
+ if (action.destructive || isDestructive(argv)) {
1537
+ const confirmed = await clack.confirm({
1538
+ message: `Run \`noir ${argv.join(" ")}\`? This may modify project files / the store.`
1539
+ });
1540
+ if (clack.isCancel(confirmed) || !confirmed) return null;
1541
+ }
1542
+ return argv;
1543
+ }
1544
+ async function runGroupedMenu(clack, opts, deps, sections) {
1545
+ let currentSection = null;
1546
+ while (true) {
1547
+ if (currentSection === null) {
1548
+ const picked = await pickSection(clack, sections, opts);
1549
+ if (clack.isCancel(picked) || picked === "exit") {
1550
+ clack.outro("bye");
1551
+ return;
1552
+ }
1553
+ const idx = sections.findIndex((s) => s.id === picked);
1554
+ if (idx === -1) {
1555
+ continue;
1556
+ }
1557
+ currentSection = idx;
1558
+ continue;
1559
+ }
1560
+ const section = sections[currentSection];
1561
+ if (!section) {
1562
+ currentSection = null;
1563
+ continue;
1564
+ }
1565
+ const chosen = await pickAction(clack, section, sections, opts);
1566
+ if (clack.isCancel(chosen)) {
1567
+ currentSection = null;
1568
+ continue;
1569
+ }
1570
+ switch (chosen) {
1571
+ case "back": {
1572
+ currentSection = null;
1573
+ continue;
1574
+ }
1575
+ case "next": {
1576
+ currentSection = (currentSection + 1) % sections.length;
1577
+ continue;
1578
+ }
1579
+ case "prev": {
1580
+ currentSection = (currentSection - 1 + sections.length) % sections.length;
1581
+ continue;
1582
+ }
1583
+ default: {
1584
+ const action = section.items.find((a) => a.id === chosen);
1585
+ if (!action) {
1586
+ currentSection = null;
1587
+ continue;
1588
+ }
1589
+ const argv = await argvForAction(clack, action, opts);
1590
+ if (argv === null) {
1591
+ continue;
1592
+ }
1593
+ await deps.dispatch(argv);
1594
+ clack.outro("done");
1595
+ return;
1596
+ }
1597
+ }
1598
+ }
1599
+ }
1210
1600
  async function runMenu(opts, deps) {
1211
1601
  const clack = await import("@clack/prompts");
1212
1602
  const project = tryProject();
@@ -1245,62 +1635,8 @@ ${COMMANDS_HINT}
1245
1635
  );
1246
1636
  }
1247
1637
  clack.intro(project ? `noir \u2014 ${project.name}` : "noir");
1248
- const choice = await clack.select({
1249
- message: "What would you like to do?",
1250
- initialValue: "status",
1251
- options: [
1252
- { value: "status", label: "Status", hint: "project + daemon + store snapshot" },
1253
- { value: "index", label: "Index project", hint: "(re)index files into context" },
1254
- { value: "recall", label: "Recall memory", hint: "search cross-session memory" },
1255
- { value: "next", label: "Next task", hint: "suggest next phase + skill" },
1256
- { value: "handoff", label: "Handoff", hint: "ready-to-paste host prompt" },
1257
- { value: "daemon", label: "Start daemon", hint: "foreground daemon" },
1258
- { value: "sync", label: "Sync skills", hint: "re-emit builtin skills" },
1259
- { value: "exit", label: "Exit" }
1260
- ]
1261
- });
1262
- if (clack.isCancel(choice)) {
1263
- clack.cancel("Cancelled.");
1264
- fail(EXIT.CANCELLED, "cancelled", opts);
1265
- }
1266
- const argv = await argvForChoice(choice, clack, opts);
1267
- if (argv === null) {
1268
- clack.outro("bye");
1269
- return;
1270
- }
1271
- await deps.dispatch(argv);
1272
- clack.outro("done");
1273
- }
1274
- async function argvForChoice(choice, clack, opts) {
1275
- switch (choice) {
1276
- case "status":
1277
- return ["status"];
1278
- case "index":
1279
- return ["context", "index"];
1280
- case "next":
1281
- return ["task", "next"];
1282
- case "handoff":
1283
- return ["handoff"];
1284
- case "daemon":
1285
- return ["daemon", "start"];
1286
- case "sync":
1287
- return ["sync"];
1288
- case "exit":
1289
- return null;
1290
- case "recall": {
1291
- const query = await clack.text({
1292
- message: "Recall query:",
1293
- placeholder: "e.g. auth flow, ContextEngine, deploy steps"
1294
- });
1295
- if (clack.isCancel(query)) {
1296
- clack.cancel("Cancelled.");
1297
- fail(EXIT.CANCELLED, "cancelled", opts);
1298
- }
1299
- return ["memory", "recall", String(query)];
1300
- }
1301
- default:
1302
- return null;
1303
- }
1638
+ const sections = await resolveSections(deps.commands ?? []);
1639
+ await runGroupedMenu(clack, opts, deps, sections);
1304
1640
  }
1305
1641
 
1306
1642
  // src/commands/memory.ts
@@ -1702,66 +2038,6 @@ async function skillsSync(opts) {
1702
2038
  }
1703
2039
  }
1704
2040
 
1705
- // src/tui/commands/registry.ts
1706
- var DESTRUCTIVE_PREFIXES = [
1707
- ["context", "index"],
1708
- ["context", "forget"],
1709
- ["memory", "save"],
1710
- ["memory", "forget"],
1711
- ["memory", "consolidate"],
1712
- ["task", "new"],
1713
- ["task", "advance"],
1714
- ["skills", "sync"],
1715
- ["daemon", "start"],
1716
- ["daemon", "stop"],
1717
- ["daemon", "restart"],
1718
- ["init"],
1719
- ["create"],
1720
- ["sync"],
1721
- ["install"],
1722
- ["update"]
1723
- ];
1724
- function isDestructive(argv) {
1725
- return DESTRUCTIVE_PREFIXES.some((prefix) => {
1726
- if (argv.length < prefix.length) return false;
1727
- return prefix.every((tok, i) => argv[i] === tok);
1728
- });
1729
- }
1730
- function capitalizeFirst(s) {
1731
- if (s.length === 0) return s;
1732
- return `${s[0]?.toUpperCase() ?? ""}${s.slice(1)}`;
1733
- }
1734
- function hasAction(cmd) {
1735
- return cmd._actionHandler != null;
1736
- }
1737
- function collectLeaves(cmd, path, out) {
1738
- const children = cmd.commands;
1739
- const isLeaf = hasAction(cmd) && children.length === 0;
1740
- if (isLeaf) {
1741
- const argv = path;
1742
- const id = argv.join(" ");
1743
- const description = cmd.description() || argv.join(" ");
1744
- out.push({
1745
- id,
1746
- label: argv.map(capitalizeFirst).join(": "),
1747
- argv,
1748
- description,
1749
- category: argv[0] ?? "",
1750
- keywords: argv,
1751
- destructive: isDestructive(argv)
1752
- });
1753
- return;
1754
- }
1755
- for (const child of children) {
1756
- collectLeaves(child, [...path, child.name()], out);
1757
- }
1758
- }
1759
- function buildPaletteCommands(program2) {
1760
- const out = [];
1761
- collectLeaves(program2, [], out);
1762
- return out;
1763
- }
1764
-
1765
2041
  // src/bin.ts
1766
2042
  function buildPaletteCommandsForTui() {
1767
2043
  return buildPaletteCommands(createProgram());
@@ -2201,16 +2477,25 @@ function createProgram() {
2201
2477
  };
2202
2478
  await runTui(opts, dispatch);
2203
2479
  });
2204
- const homeDeps = {
2205
- dispatch: async (argv) => {
2480
+ program2.command("palette").description("fuzzy command palette \u2014 run any noir command (Ink)").action(async (...args) => {
2481
+ const g = actionGlobals(args);
2482
+ const opts = toCliOptions(g);
2483
+ requireInteractive(opts, "`noir palette`");
2484
+ const tuiUrl = new URL("./tui/index.js", import.meta.url).href;
2485
+ const { runPalette } = await import(
2486
+ /* @vite-ignore */
2487
+ tuiUrl
2488
+ );
2489
+ const dispatch = async (argv) => {
2206
2490
  const sub = createProgram();
2207
2491
  try {
2208
2492
  await sub.parseAsync([...argv], { from: "user" });
2209
2493
  } catch (err) {
2210
2494
  handleError(err);
2211
2495
  }
2212
- }
2213
- };
2496
+ };
2497
+ await runPalette(opts, dispatch);
2498
+ });
2214
2499
  program2.action(async (...args) => {
2215
2500
  const cmd = trailingCmd(args);
2216
2501
  const leftovers = cmd.args;
@@ -2226,6 +2511,20 @@ function createProgram() {
2226
2511
  return program2;
2227
2512
  }
2228
2513
  var program = createProgram();
2514
+ var homeDeps = {
2515
+ dispatch: async (argv) => {
2516
+ const sub = createProgram();
2517
+ try {
2518
+ await sub.parseAsync([...argv], { from: "user" });
2519
+ } catch (err) {
2520
+ handleError(err);
2521
+ }
2522
+ },
2523
+ // The grouped home menu resolves its sections against the LIVE palette
2524
+ // registry (home-consolidation S1/S2) — the same source the TUI palette
2525
+ // uses — so the menu cannot drift from the commander tree.
2526
+ commands: buildPaletteCommands(createProgram())
2527
+ };
2229
2528
  async function run(argv = []) {
2230
2529
  try {
2231
2530
  await program.parseAsync([...argv], { from: "user" });