@opennous/cli 0.8.0 → 0.10.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/CHANGELOG.md +18 -0
- package/package.json +3 -2
- package/src/index.js +125 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @opennous/cli changelog
|
|
2
|
+
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Added — `nous vault`: on-device retrieval over your linked local vault
|
|
6
|
+
|
|
7
|
+
Once a folder is paired with `nous link`, an agent running on your own machine
|
|
8
|
+
(Claude Code, Hermes, …) can now read and search your private vault **locally** —
|
|
9
|
+
with no server round-trip. It's fresher (no sync lag), fuller (whole files, not
|
|
10
|
+
snippets), faster (no network), and private (the reads never leave the device).
|
|
11
|
+
This is the native counterpart to the `search_my_vault` MCP tool, which stays the
|
|
12
|
+
fallback for when you're *not* on the machine.
|
|
13
|
+
|
|
14
|
+
- `nous vault search <query>` — local keyword search over your vault notes
|
|
15
|
+
(filename-weighted, ranked, with snippets). Options: `--folder`, `--limit`, `--json`.
|
|
16
|
+
- `nous vault read <path>` — print a note's full content (e.g. `decisions/pricing`).
|
|
17
|
+
- `nous vault list [folder]` — list vault files, optionally within one folder.
|
|
18
|
+
- `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.0",
|
|
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
|
@@ -837,18 +837,25 @@ program
|
|
|
837
837
|
// freshly minted API key is saved to ~/.nous/config.json. No copy-paste. Shared
|
|
838
838
|
// by `nous login` and `nous init`.
|
|
839
839
|
// ---------------------------------------------------------------------------
|
|
840
|
-
async function browserLogin(url) {
|
|
840
|
+
async function browserLogin(url, opts = {}) {
|
|
841
841
|
const apiUrl = url || process.env.NOUS_API_URL || readConfig().apiUrl || "https://api.opennous.cloud";
|
|
842
|
+
// Default = plain browser sign-in. With { startPath, startBody } it redeems a team
|
|
843
|
+
// join token instead (nous join): same device flow, different start endpoint.
|
|
844
|
+
const startPath = opts.startPath || "/api/cli/auth/start";
|
|
845
|
+
const startBody = opts.startBody ? JSON.stringify(opts.startBody) : "{}";
|
|
842
846
|
|
|
843
847
|
let start;
|
|
844
848
|
try {
|
|
845
|
-
const r = await fetch(`${apiUrl}
|
|
846
|
-
method: "POST", headers: { "Content-Type": "application/json" }, body:
|
|
849
|
+
const r = await fetch(`${apiUrl}${startPath}`, {
|
|
850
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: startBody,
|
|
847
851
|
});
|
|
848
|
-
if (!r.ok)
|
|
852
|
+
if (!r.ok) {
|
|
853
|
+
const j = await r.json().catch(() => ({}));
|
|
854
|
+
throw new Error(j.error ? j.error.replace(/_/g, " ") : `server returned ${r.status}`);
|
|
855
|
+
}
|
|
849
856
|
start = await r.json();
|
|
850
857
|
} catch (e) {
|
|
851
|
-
console.error(`Couldn't start
|
|
858
|
+
console.error(`Couldn't start: ${e.message}`);
|
|
852
859
|
process.exit(1);
|
|
853
860
|
}
|
|
854
861
|
|
|
@@ -895,6 +902,16 @@ program
|
|
|
895
902
|
console.log("Your Nous tools are ready — try `get_workspace_status` in your agent.");
|
|
896
903
|
});
|
|
897
904
|
|
|
905
|
+
program
|
|
906
|
+
.command("join <token>")
|
|
907
|
+
.description("Join a team with an invite/join token and set up your own scoped workspace")
|
|
908
|
+
.option("--url <url>", "API base URL (default: https://api.opennous.cloud)")
|
|
909
|
+
.action(async (token, { url }) => {
|
|
910
|
+
await browserLogin(url, { startPath: "/api/cli/auth/join/start", startBody: { token } });
|
|
911
|
+
console.log("You're in — your agent now has its own member-scoped Nous workspace.");
|
|
912
|
+
console.log("Try `get_workspace_status` in your agent.");
|
|
913
|
+
});
|
|
914
|
+
|
|
898
915
|
// ---------------------------------------------------------------------------
|
|
899
916
|
// MCP registration — connect Nous to whatever agent is on this machine.
|
|
900
917
|
//
|
|
@@ -1260,5 +1277,108 @@ program
|
|
|
1260
1277
|
console.log("✓ Unlinked. Your files are still in Nous; this folder just stopped syncing.");
|
|
1261
1278
|
});
|
|
1262
1279
|
|
|
1280
|
+
// ---------------------------------------------------------------------------
|
|
1281
|
+
// nous vault — read + search the LINKED local vault ON-DEVICE. When an agent runs
|
|
1282
|
+
// where the vault is linked (`nous link`), it should read these files directly
|
|
1283
|
+
// instead of calling the search_my_vault MCP tool: fresher (no sync lag), fuller
|
|
1284
|
+
// (whole files, not snippets), faster (no network), and private (the reads never
|
|
1285
|
+
// leave the machine). This is the native personal-vault retrieval path.
|
|
1286
|
+
// ---------------------------------------------------------------------------
|
|
1287
|
+
function linkedVaultRoot() {
|
|
1288
|
+
const dir = readConfig().link?.dir;
|
|
1289
|
+
if (!dir || !existsSync(dir)) return null;
|
|
1290
|
+
return dir;
|
|
1291
|
+
}
|
|
1292
|
+
function requireVaultRoot() {
|
|
1293
|
+
const root = linkedVaultRoot();
|
|
1294
|
+
if (!root) { console.error("No local vault linked. Run `nous link <dir>` first."); process.exit(1); }
|
|
1295
|
+
return root;
|
|
1296
|
+
}
|
|
1297
|
+
// Resolve a 'folder/[subfolder/]name' argument to an absolute .md path under the vault.
|
|
1298
|
+
function resolveVaultRel(root, p) {
|
|
1299
|
+
const rel = String(p).replace(/^\/+/, "").endsWith(".md") ? String(p).replace(/^\/+/, "") : `${String(p).replace(/^\/+/, "")}.md`;
|
|
1300
|
+
return { rel, abs: join(root, rel.split("/").join(sep)) };
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const vaultCmd = program
|
|
1304
|
+
.command("vault")
|
|
1305
|
+
.description("Read and search your linked local vault — on-device, no server round-trip");
|
|
1306
|
+
|
|
1307
|
+
vaultCmd
|
|
1308
|
+
.command("path")
|
|
1309
|
+
.description("Print the linked vault folder (point your own Read/Grep at it)")
|
|
1310
|
+
.action(() => { console.log(requireVaultRoot()); });
|
|
1311
|
+
|
|
1312
|
+
vaultCmd
|
|
1313
|
+
.command("list [folder]")
|
|
1314
|
+
.description("List vault files, optionally within one folder")
|
|
1315
|
+
.option("--json", "Output JSON", false)
|
|
1316
|
+
.action((folder, { json }) => {
|
|
1317
|
+
const root = requireVaultRoot();
|
|
1318
|
+
let files = walkVaultMd(root).map((f) => f.rel);
|
|
1319
|
+
if (folder) files = files.filter((r) => r.split("/")[0] === folder);
|
|
1320
|
+
files.sort();
|
|
1321
|
+
if (json) { console.log(JSON.stringify(files)); return; }
|
|
1322
|
+
if (!files.length) { console.log(folder ? `No files in ${folder}.` : "Vault is empty."); return; }
|
|
1323
|
+
for (const r of files) console.log(` ${r}`);
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
vaultCmd
|
|
1327
|
+
.command("read <path>")
|
|
1328
|
+
.description("Print a vault file's full content (e.g. decisions/pricing)")
|
|
1329
|
+
.action((p) => {
|
|
1330
|
+
const root = requireVaultRoot();
|
|
1331
|
+
const { rel, abs } = resolveVaultRel(root, p);
|
|
1332
|
+
const content = safeRead(abs);
|
|
1333
|
+
if (content == null) { console.error(`Not found: ${rel}`); process.exit(1); }
|
|
1334
|
+
process.stdout.write(content.endsWith("\n") ? content : content + "\n");
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
vaultCmd
|
|
1338
|
+
.command("search <query...>")
|
|
1339
|
+
.description("Search the linked vault locally (on-device — never leaves your machine)")
|
|
1340
|
+
.option("--folder <folder>", "Restrict to one vault folder")
|
|
1341
|
+
.option("--limit <n>", "Max results", "6")
|
|
1342
|
+
.option("--json", "Output JSON", false)
|
|
1343
|
+
.action((queryParts, { folder, limit, json }) => {
|
|
1344
|
+
const root = requireVaultRoot();
|
|
1345
|
+
const query = queryParts.join(" ");
|
|
1346
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1347
|
+
if (!terms.length) { console.error("Pass a query, e.g. `nous vault search pricing objections`."); process.exit(1); }
|
|
1348
|
+
|
|
1349
|
+
let files = walkVaultMd(root);
|
|
1350
|
+
if (folder) files = files.filter((f) => f.rel.split("/")[0] === folder);
|
|
1351
|
+
|
|
1352
|
+
const hits = [];
|
|
1353
|
+
for (const f of files) {
|
|
1354
|
+
const content = safeRead(f.abs) ?? "";
|
|
1355
|
+
const hay = content.toLowerCase();
|
|
1356
|
+
const name = f.rel.toLowerCase();
|
|
1357
|
+
let score = 0;
|
|
1358
|
+
for (const t of terms) {
|
|
1359
|
+
if (name.includes(t)) score += 5; // a filename hit is a strong signal
|
|
1360
|
+
score += Math.min(hay.split(t).length - 1, 5); // content occurrences, capped
|
|
1361
|
+
}
|
|
1362
|
+
if (score <= 0) continue;
|
|
1363
|
+
// Snippet around the first matching term.
|
|
1364
|
+
let snippet = "";
|
|
1365
|
+
for (const t of terms) {
|
|
1366
|
+
const i = hay.indexOf(t);
|
|
1367
|
+
if (i >= 0) { snippet = content.slice(Math.max(0, i - 60), i + 140).replace(/\s+/g, " ").trim(); break; }
|
|
1368
|
+
}
|
|
1369
|
+
hits.push({ path: f.rel, score, snippet });
|
|
1370
|
+
}
|
|
1371
|
+
hits.sort((a, b) => b.score - a.score);
|
|
1372
|
+
const top = hits.slice(0, Math.min(Math.max(1, Number(limit) || 6), 30));
|
|
1373
|
+
|
|
1374
|
+
if (json) { console.log(JSON.stringify(top)); return; }
|
|
1375
|
+
if (!top.length) { console.log(`No local vault notes matched "${query}".`); return; }
|
|
1376
|
+
console.log(`Local vault notes matching "${query}":\n`);
|
|
1377
|
+
for (const h of top) {
|
|
1378
|
+
console.log(` ${h.path}`);
|
|
1379
|
+
if (h.snippet) console.log(` ${h.snippet}`);
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1263
1383
|
// ---------------------------------------------------------------------------
|
|
1264
1384
|
program.name("nous").version("0.8.0").parse();
|