@opennous/cli 0.9.0 → 0.10.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/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # @opennous/cli changelog
2
+
3
+ ## 0.10.1
4
+
5
+ ### Changed — new default vault folder structure
6
+
7
+ `nous link` now scaffolds the vault with **inbox · projects · decisions · accounts ·
8
+ people · resources · archive**. `accounts/` and `people/` are where Nous files the raw
9
+ of each touch (email, meeting) by resolved account/person. Backward-compatible: the
10
+ legacy folders (thoughts/briefs/content/company) are still walked, watched, and searched,
11
+ so any files already in them keep syncing — only the folders created on a fresh link
12
+ changed.
13
+
14
+ ## 0.10.0
15
+
16
+ ### Added — `nous vault`: on-device retrieval over your linked local vault
17
+
18
+ Once a folder is paired with `nous link`, an agent running on your own machine
19
+ (Claude Code, Hermes, …) can now read and search your private vault **locally** —
20
+ with no server round-trip. It's fresher (no sync lag), fuller (whole files, not
21
+ snippets), faster (no network), and private (the reads never leave the device).
22
+ This is the native counterpart to the `search_my_vault` MCP tool, which stays the
23
+ fallback for when you're *not* on the machine.
24
+
25
+ - `nous vault search <query>` — local keyword search over your vault notes
26
+ (filename-weighted, ranked, with snippets). Options: `--folder`, `--limit`, `--json`.
27
+ - `nous vault read <path>` — print a note's full content (e.g. `decisions/pricing`).
28
+ - `nous vault list [folder]` — list vault files, optionally within one folder.
29
+ - `nous vault path` — print the linked folder, so you can point your own Grep/Read at it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennous/cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Nous CLI — set up Nous, connect it to your coding agent, and read the Context API from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "files": [
10
10
  "src",
11
- "README.md"
11
+ "README.md",
12
+ "CHANGELOG.md"
12
13
  ],
13
14
  "scripts": {
14
15
  "dev": "node src/index.js",
package/src/index.js CHANGED
@@ -1123,7 +1123,10 @@ program
1123
1123
  // run it once and leave it watching, or pass --once for a single reconcile.
1124
1124
  // ---------------------------------------------------------------------------
1125
1125
 
1126
- const VAULT_FOLDERS = ["inbox", "thoughts", "decisions", "projects", "briefs", "content", "company"];
1126
+ // Default vault structure (2026-08-21). New folders scaffold with these 7. The legacy
1127
+ // folders are still walked/watched/searched so existing files keep syncing.
1128
+ const DEFAULT_FOLDERS = ["inbox", "projects", "decisions", "accounts", "people", "resources", "archive"];
1129
+ const VAULT_FOLDERS = [...DEFAULT_FOLDERS, "thoughts", "briefs", "content", "company"];
1127
1130
 
1128
1131
  // A resilient API caller for the daemon — unlike apiClient(), a failed poll or push
1129
1132
  // must NOT exit the process; the loop should keep running and retry on the next tick.
@@ -1200,7 +1203,7 @@ program
1200
1203
  const root = resolve(dir);
1201
1204
  // Scaffold the folder so the desk has the same shape as the vault.
1202
1205
  if (!existsSync(root)) mkdirSync(root, { recursive: true });
1203
- for (const f of VAULT_FOLDERS) { const d = join(root, f); if (!existsSync(d)) mkdirSync(d, { recursive: true }); }
1206
+ for (const f of DEFAULT_FOLDERS) { const d = join(root, f); if (!existsSync(d)) mkdirSync(d, { recursive: true }); }
1204
1207
 
1205
1208
  const label = hostname() || "my machine";
1206
1209
  const linked = await call("POST", "/v2/personal/local/link", { body: { label } });
@@ -1277,5 +1280,108 @@ program
1277
1280
  console.log("✓ Unlinked. Your files are still in Nous; this folder just stopped syncing.");
1278
1281
  });
1279
1282
 
1283
+ // ---------------------------------------------------------------------------
1284
+ // nous vault — read + search the LINKED local vault ON-DEVICE. When an agent runs
1285
+ // where the vault is linked (`nous link`), it should read these files directly
1286
+ // instead of calling the search_my_vault MCP tool: fresher (no sync lag), fuller
1287
+ // (whole files, not snippets), faster (no network), and private (the reads never
1288
+ // leave the machine). This is the native personal-vault retrieval path.
1289
+ // ---------------------------------------------------------------------------
1290
+ function linkedVaultRoot() {
1291
+ const dir = readConfig().link?.dir;
1292
+ if (!dir || !existsSync(dir)) return null;
1293
+ return dir;
1294
+ }
1295
+ function requireVaultRoot() {
1296
+ const root = linkedVaultRoot();
1297
+ if (!root) { console.error("No local vault linked. Run `nous link <dir>` first."); process.exit(1); }
1298
+ return root;
1299
+ }
1300
+ // Resolve a 'folder/[subfolder/]name' argument to an absolute .md path under the vault.
1301
+ function resolveVaultRel(root, p) {
1302
+ const rel = String(p).replace(/^\/+/, "").endsWith(".md") ? String(p).replace(/^\/+/, "") : `${String(p).replace(/^\/+/, "")}.md`;
1303
+ return { rel, abs: join(root, rel.split("/").join(sep)) };
1304
+ }
1305
+
1306
+ const vaultCmd = program
1307
+ .command("vault")
1308
+ .description("Read and search your linked local vault — on-device, no server round-trip");
1309
+
1310
+ vaultCmd
1311
+ .command("path")
1312
+ .description("Print the linked vault folder (point your own Read/Grep at it)")
1313
+ .action(() => { console.log(requireVaultRoot()); });
1314
+
1315
+ vaultCmd
1316
+ .command("list [folder]")
1317
+ .description("List vault files, optionally within one folder")
1318
+ .option("--json", "Output JSON", false)
1319
+ .action((folder, { json }) => {
1320
+ const root = requireVaultRoot();
1321
+ let files = walkVaultMd(root).map((f) => f.rel);
1322
+ if (folder) files = files.filter((r) => r.split("/")[0] === folder);
1323
+ files.sort();
1324
+ if (json) { console.log(JSON.stringify(files)); return; }
1325
+ if (!files.length) { console.log(folder ? `No files in ${folder}.` : "Vault is empty."); return; }
1326
+ for (const r of files) console.log(` ${r}`);
1327
+ });
1328
+
1329
+ vaultCmd
1330
+ .command("read <path>")
1331
+ .description("Print a vault file's full content (e.g. decisions/pricing)")
1332
+ .action((p) => {
1333
+ const root = requireVaultRoot();
1334
+ const { rel, abs } = resolveVaultRel(root, p);
1335
+ const content = safeRead(abs);
1336
+ if (content == null) { console.error(`Not found: ${rel}`); process.exit(1); }
1337
+ process.stdout.write(content.endsWith("\n") ? content : content + "\n");
1338
+ });
1339
+
1340
+ vaultCmd
1341
+ .command("search <query...>")
1342
+ .description("Search the linked vault locally (on-device — never leaves your machine)")
1343
+ .option("--folder <folder>", "Restrict to one vault folder")
1344
+ .option("--limit <n>", "Max results", "6")
1345
+ .option("--json", "Output JSON", false)
1346
+ .action((queryParts, { folder, limit, json }) => {
1347
+ const root = requireVaultRoot();
1348
+ const query = queryParts.join(" ");
1349
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
1350
+ if (!terms.length) { console.error("Pass a query, e.g. `nous vault search pricing objections`."); process.exit(1); }
1351
+
1352
+ let files = walkVaultMd(root);
1353
+ if (folder) files = files.filter((f) => f.rel.split("/")[0] === folder);
1354
+
1355
+ const hits = [];
1356
+ for (const f of files) {
1357
+ const content = safeRead(f.abs) ?? "";
1358
+ const hay = content.toLowerCase();
1359
+ const name = f.rel.toLowerCase();
1360
+ let score = 0;
1361
+ for (const t of terms) {
1362
+ if (name.includes(t)) score += 5; // a filename hit is a strong signal
1363
+ score += Math.min(hay.split(t).length - 1, 5); // content occurrences, capped
1364
+ }
1365
+ if (score <= 0) continue;
1366
+ // Snippet around the first matching term.
1367
+ let snippet = "";
1368
+ for (const t of terms) {
1369
+ const i = hay.indexOf(t);
1370
+ if (i >= 0) { snippet = content.slice(Math.max(0, i - 60), i + 140).replace(/\s+/g, " ").trim(); break; }
1371
+ }
1372
+ hits.push({ path: f.rel, score, snippet });
1373
+ }
1374
+ hits.sort((a, b) => b.score - a.score);
1375
+ const top = hits.slice(0, Math.min(Math.max(1, Number(limit) || 6), 30));
1376
+
1377
+ if (json) { console.log(JSON.stringify(top)); return; }
1378
+ if (!top.length) { console.log(`No local vault notes matched "${query}".`); return; }
1379
+ console.log(`Local vault notes matching "${query}":\n`);
1380
+ for (const h of top) {
1381
+ console.log(` ${h.path}`);
1382
+ if (h.snippet) console.log(` ${h.snippet}`);
1383
+ }
1384
+ });
1385
+
1280
1386
  // ---------------------------------------------------------------------------
1281
1387
  program.name("nous").version("0.8.0").parse();