@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 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.8.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}/api/cli/auth/start`, {
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) throw new Error(`server returned ${r.status}`);
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 sign-in: ${e.message}`);
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();