@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 +29 -0
- package/package.json +3 -2
- package/src/index.js +108 -2
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.
|
|
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
|
-
|
|
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
|
|
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();
|