@opennous/cli 0.5.0 → 0.7.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/README.md +15 -6
- package/package.json +2 -2
- package/src/index.js +305 -14
package/README.md
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
# @opennous/cli
|
|
2
2
|
|
|
3
|
-
Nous CLI —
|
|
3
|
+
Nous CLI — set Nous up, connect it to your coding agent, and read the Context API from your terminal.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Get started
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
9
|
-
npx -y @opennous/cli@latest install
|
|
8
|
+
curl -fsSL https://get.opennous.cloud | sh
|
|
10
9
|
```
|
|
11
10
|
|
|
12
|
-
`nous
|
|
11
|
+
That installs the CLI and runs `nous init`, which signs you in (creating your account if you don't have one), registers the Nous MCP server with the agent on your machine, and hands off. Then tell your agent *"set up my Nous workspace"* — it reads your project, finds your ICP (or drafts one), and syncs it.
|
|
12
|
+
|
|
13
|
+
Prefer to run it yourself:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx -y @opennous/cli@latest init
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Event tracking
|
|
20
|
+
|
|
21
|
+
`nous track init` detects your stack (Next.js, Node, Python), writes a small `nous.js` (or `nous.py`) module with three helpers, and updates your `.env`:
|
|
13
22
|
|
|
14
23
|
- **`trackSignup({email, ...})`** — fires `interaction.signed_up` + initial `state.stage`
|
|
15
24
|
- **`handleStripeEvent(event, {customerEmail})`** — fires `interaction.subscription_started / updated / canceled` and flips `state.stage` (Customer / Churned)
|
|
16
25
|
- **`track(focus, property, value)`** — escape hatch for any other event
|
|
17
26
|
|
|
18
|
-
The generated module is plain code you own. Read it, edit it, or delete it.
|
|
27
|
+
The generated module is plain code you own. Read it, edit it, or delete it. (This was `nous install`; that name still works but is deprecated.)
|
|
19
28
|
|
|
20
29
|
## Other commands
|
|
21
30
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennous/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Nous CLI —
|
|
3
|
+
"version": "0.7.0",
|
|
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": {
|
|
7
7
|
"nous": "src/index.js"
|
package/src/index.js
CHANGED
|
@@ -10,10 +10,16 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } fr
|
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
import { join, resolve } from "path";
|
|
12
12
|
import { createInterface } from "readline";
|
|
13
|
-
import { spawn } from "child_process";
|
|
13
|
+
import { spawn, spawnSync } from "child_process";
|
|
14
14
|
|
|
15
15
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
16
16
|
|
|
17
|
+
// Recede, don't decorate. Secondary text in a terminal should be readable and
|
|
18
|
+
// quiet — and it must degrade to plain text when the output is piped, because a
|
|
19
|
+
// CI log full of escape codes is worse than no colour at all.
|
|
20
|
+
const COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
21
|
+
const dim = (t) => (COLOR ? `\x1b[2m${t}\x1b[0m` : t);
|
|
22
|
+
|
|
17
23
|
// Best-effort open the user's browser; if it fails they have the printed URL.
|
|
18
24
|
function openBrowser(url) {
|
|
19
25
|
const cmd = process.platform === "darwin" ? "open"
|
|
@@ -293,8 +299,17 @@ const NEXT_STEPS = {
|
|
|
293
299
|
3. Call handle_stripe_event(event, customer_email) inside your Stripe webhook view.`,
|
|
294
300
|
};
|
|
295
301
|
|
|
296
|
-
|
|
297
|
-
|
|
302
|
+
// `nous track init` — drop event tracking into the user's app. This used to be
|
|
303
|
+
// `nous install`, which collided head-on with the front-door meaning of "install":
|
|
304
|
+
// people typed `nous install` expecting to set Nous up and got an app-instrumentation
|
|
305
|
+
// scaffolder instead. The front door is now `nous init`. `install` stays as a hidden
|
|
306
|
+
// deprecated alias so nobody's script breaks, and it prints where the command went.
|
|
307
|
+
const trackCmd = program
|
|
308
|
+
.command("track")
|
|
309
|
+
.description("Instrument your app to send events to Nous");
|
|
310
|
+
|
|
311
|
+
function defineTrackInit(cmd) {
|
|
312
|
+
return cmd
|
|
298
313
|
.description("Drop Nous event tracking (signups + Stripe lifecycle) into the current project")
|
|
299
314
|
.option("--dir <path>", "Target project directory (default: cwd)", process.cwd())
|
|
300
315
|
.option("--lang <stack>", "Force language/stack: next | node-esm | node-cjs | python")
|
|
@@ -353,6 +368,17 @@ program
|
|
|
353
368
|
console.log(`\nDetected stack: ${stack}`);
|
|
354
369
|
console.log(`\n${NEXT_STEPS[stack] || NEXT_STEPS["node-esm"]}\n`);
|
|
355
370
|
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// The real command: `nous track init`.
|
|
374
|
+
defineTrackInit(trackCmd.command("init"));
|
|
375
|
+
|
|
376
|
+
// Deprecated alias: `nous install`. Kept so existing scripts don't break; it does the
|
|
377
|
+
// same thing but says where it moved, because "install" is now `nous init`.
|
|
378
|
+
defineTrackInit(program.command("install"))
|
|
379
|
+
.hook("preAction", () => {
|
|
380
|
+
console.error(dim("note: `nous install` is now `nous track init`. Run `nous init` to set up Nous itself.\n"));
|
|
381
|
+
});
|
|
356
382
|
|
|
357
383
|
// ---------------------------------------------------------------------------
|
|
358
384
|
// nous auth
|
|
@@ -383,9 +409,20 @@ program
|
|
|
383
409
|
if (key) {
|
|
384
410
|
console.log(`Logged in\nAPI URL: ${url}\nKey: ${key.slice(0, 8)}...`);
|
|
385
411
|
} else {
|
|
386
|
-
console.log("Not logged in. Run:
|
|
412
|
+
console.log("Not logged in. Run: npx @opennous/cli login");
|
|
387
413
|
}
|
|
388
414
|
})
|
|
415
|
+
)
|
|
416
|
+
.addCommand(
|
|
417
|
+
new Command("logout")
|
|
418
|
+
.description("Sign out — remove the saved API key")
|
|
419
|
+
.action(() => {
|
|
420
|
+
const cfg = readConfig();
|
|
421
|
+
if (!cfg.apiKey) { console.log("Not signed in."); return; }
|
|
422
|
+
delete cfg.apiKey;
|
|
423
|
+
writeConfig(cfg);
|
|
424
|
+
console.log("✓ Signed out — removed the saved API key from ~/.nous/config.json");
|
|
425
|
+
})
|
|
389
426
|
);
|
|
390
427
|
|
|
391
428
|
// ---------------------------------------------------------------------------
|
|
@@ -585,6 +622,118 @@ program
|
|
|
585
622
|
});
|
|
586
623
|
});
|
|
587
624
|
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
// nous audit — is this graph actually sound?
|
|
627
|
+
//
|
|
628
|
+
// The command that answers the only question a technical team has before trusting
|
|
629
|
+
// data they didn't collect themselves. It is deliberately a COMMAND and not a
|
|
630
|
+
// dashboard: a dashboard makes a claim about our own product and asks you to
|
|
631
|
+
// believe it; a command runs against your data, in your terminal, and returns an
|
|
632
|
+
// exit code. One of those can fail a build.
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
program
|
|
635
|
+
.command("audit")
|
|
636
|
+
.description("Audit your GTM data — what's broken, what's unsure, and what to do about it")
|
|
637
|
+
.option("--check <name>", "One check only: arriving, resolved, evidenced, current, reachable")
|
|
638
|
+
.option("--list", "List the records behind each finding")
|
|
639
|
+
.option("--fail-under <n>", "Exit 1 if health is below this percentage — for CI")
|
|
640
|
+
.option("--fix", "Show the repairs that can be made safely, without making them")
|
|
641
|
+
.option("--apply", "Actually make them (use with --fix)")
|
|
642
|
+
.option("--json", "Print the raw JSON response")
|
|
643
|
+
.action(async ({ check, list, failUnder, fix, apply, json }) => {
|
|
644
|
+
const api = apiClient();
|
|
645
|
+
|
|
646
|
+
// ── nous audit --fix ──
|
|
647
|
+
//
|
|
648
|
+
// Only the repairs that need no judgment: records sharing an email or a
|
|
649
|
+
// LinkedIn profile are the same human by definition. Everything else the audit
|
|
650
|
+
// finds needs a decision, a browser, or you — and a --fix that quietly did
|
|
651
|
+
// those too would be one nobody could trust.
|
|
652
|
+
if (fix) {
|
|
653
|
+
const out = await api.post("/v2/audit/fix", { apply: !!apply });
|
|
654
|
+
if (json) { console.log(JSON.stringify(out, null, 2)); return; }
|
|
655
|
+
|
|
656
|
+
if (out.dry_run) {
|
|
657
|
+
if (!out.would_merge) {
|
|
658
|
+
console.log("\n Nothing to repair automatically.\n");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
console.log(`\n ${out.would_merge} ${out.would_merge === 1 ? "merge" : "merges"} can be made safely:\n`);
|
|
662
|
+
for (const m of out.merges) {
|
|
663
|
+
console.log(` · ${m.who ?? m.keep} ${dim("— " + m.why)}`);
|
|
664
|
+
}
|
|
665
|
+
console.log(dim("\n Nothing changed. Re-run with --fix --apply to make them.\n"));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
console.log(`\n Merged ${out.merged}.${out.failed ? ` ${out.failed} failed.` : ""}\n`);
|
|
670
|
+
for (const f of out.failures ?? []) console.error(` ✗ ${f.who ?? f.keep}: ${f.error}`);
|
|
671
|
+
if (out.failed) process.exit(1);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const data = await api.get("/v2/audit", check ? { check } : {});
|
|
676
|
+
|
|
677
|
+
if (json) {
|
|
678
|
+
console.log(JSON.stringify(data, null, 2));
|
|
679
|
+
} else {
|
|
680
|
+
const checks = check ? [data.check] : data.checks;
|
|
681
|
+
const findings = data.findings ?? [];
|
|
682
|
+
|
|
683
|
+
console.log("");
|
|
684
|
+
for (const c of checks) {
|
|
685
|
+
const pct = String(c.pct).padStart(4);
|
|
686
|
+
const mark = c.pct === 100 ? "\u2713" : "\u2717";
|
|
687
|
+
console.log(` ${pct}% ${c.label.toUpperCase().padEnd(11)} ${c.summary} ${mark}`);
|
|
688
|
+
// What we deliberately did not count. An audit cannot itself be a black box.
|
|
689
|
+
if (c.note) console.log(` ${dim(c.note)}`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (!findings.length) {
|
|
693
|
+
console.log("\n Nothing to fix.\n");
|
|
694
|
+
} else {
|
|
695
|
+
console.log("");
|
|
696
|
+
for (const f of findings) {
|
|
697
|
+
console.log(` [${f.severity.toUpperCase()}] ${f.title}`);
|
|
698
|
+
console.log(` ${dim(f.detail)}`);
|
|
699
|
+
// Who can fix it, said plainly. Pretending we can re-authorise someone's
|
|
700
|
+
// Gmail is how a tool loses trust the first time it fails to.
|
|
701
|
+
const by = f.fixable_by === "human"
|
|
702
|
+
? "needs you"
|
|
703
|
+
: f.fixable_by === "agent_with_confirmation"
|
|
704
|
+
? "the agent, once you confirm"
|
|
705
|
+
: "the agent";
|
|
706
|
+
console.log(` \u2192 ${f.fix} (${by})`);
|
|
707
|
+
|
|
708
|
+
if (list && f.subjects?.length) {
|
|
709
|
+
for (const sub of f.subjects.slice(0, 20)) {
|
|
710
|
+
const line = sub.who
|
|
711
|
+
? (Array.isArray(sub.who) ? sub.who.join(" \u2194 ") : sub.who)
|
|
712
|
+
: Object.entries(sub).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
713
|
+
console.log(` \u00b7 ${line}${sub.why ? dim(" — " + sub.why) : ""}`);
|
|
714
|
+
}
|
|
715
|
+
if (f.subjects.length > 20) {
|
|
716
|
+
console.log(` \u00b7 ${dim(`and ${f.subjects.length - 20} more`)}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
console.log("");
|
|
720
|
+
}
|
|
721
|
+
if (!list) console.log(dim(" Add --list to see the records behind each finding.\n"));
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// The reason this is a command. An exit code can stop a deploy; a dashboard
|
|
726
|
+
// cannot. You would have known about a dead connector on day one, not day 48.
|
|
727
|
+
const health = check ? data.check.pct : data.health;
|
|
728
|
+
if (failUnder !== undefined) {
|
|
729
|
+
const floor = Number(failUnder);
|
|
730
|
+
if (health < floor) {
|
|
731
|
+
console.error(`\n FAIL: health ${health}% is below the threshold of ${floor}%\n`);
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
588
737
|
// ---------------------------------------------------------------------------
|
|
589
738
|
// nous verify <focus> <property> — re-check a claim before acting on it
|
|
590
739
|
// ---------------------------------------------------------------------------
|
|
@@ -721,20 +870,162 @@ program
|
|
|
721
870
|
console.log("Your Nous tools are ready — try `get_workspace_status` in your agent.");
|
|
722
871
|
});
|
|
723
872
|
|
|
724
|
-
//
|
|
725
|
-
//
|
|
873
|
+
// ---------------------------------------------------------------------------
|
|
874
|
+
// MCP registration — connect Nous to whatever agent is on this machine.
|
|
875
|
+
//
|
|
876
|
+
// We deliberately DO NOT use the Claude Code plugin/marketplace here. Two commands
|
|
877
|
+
// (`/plugin marketplace add`, `/plugin install`), Claude-Code-only, with a marketplace
|
|
878
|
+
// standing between a new user and the product — it's too much for a first run. The MCP
|
|
879
|
+
// server is the whole integration, and `claude mcp add` registers it in one line.
|
|
880
|
+
//
|
|
881
|
+
// The MCP resolves its key from ~/.nous/config.json on every call, so we never bake the
|
|
882
|
+
// key into any of these configs — sign-in already wrote it there.
|
|
883
|
+
// ---------------------------------------------------------------------------
|
|
884
|
+
|
|
885
|
+
const MCP_CMD = "npx";
|
|
886
|
+
const MCP_ARGS = ["-y", "@opennous/mcp"];
|
|
887
|
+
|
|
888
|
+
/** Is the `claude` CLI on PATH? Then registration is one command and needs no file. */
|
|
889
|
+
function hasClaudeCli() {
|
|
890
|
+
try {
|
|
891
|
+
const r = spawnSync("claude", ["--version"], { stdio: "ignore" });
|
|
892
|
+
return r.status === 0;
|
|
893
|
+
} catch { return false; }
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** Merge a `nous` server into a JSON MCP config file (Claude Code / Cursor shape). */
|
|
897
|
+
function writeJsonMcp(path, apiUrl) {
|
|
898
|
+
let cfg = {};
|
|
899
|
+
if (existsSync(path)) {
|
|
900
|
+
try { cfg = JSON.parse(readFileSync(path, "utf8")); } catch { cfg = {}; }
|
|
901
|
+
}
|
|
902
|
+
cfg.mcpServers = cfg.mcpServers || {};
|
|
903
|
+
cfg.mcpServers.nous = {
|
|
904
|
+
command: MCP_CMD,
|
|
905
|
+
args: MCP_ARGS,
|
|
906
|
+
// Only pin a non-default API URL (self-host). Cloud reads the key from the config file.
|
|
907
|
+
...(apiUrl && apiUrl !== "https://api.opennous.cloud" ? { env: { NOUS_API_URL: apiUrl } } : {}),
|
|
908
|
+
};
|
|
909
|
+
const dir = path.slice(0, path.lastIndexOf("/"));
|
|
910
|
+
if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
911
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Register the MCP server with the agent on this machine. Returns a short line describing
|
|
916
|
+
* what happened, for the caller to print.
|
|
917
|
+
*
|
|
918
|
+
* Order of preference:
|
|
919
|
+
* 1. `claude mcp add` if the Claude CLI is here — one command, no file to manage.
|
|
920
|
+
* 2. Write ./.mcp.json in the current project (Claude Code and most clients read it).
|
|
921
|
+
* We never fail the flow on this — a printed fallback is better than an abort.
|
|
922
|
+
*/
|
|
923
|
+
function registerMcp(apiUrl) {
|
|
924
|
+
if (hasClaudeCli()) {
|
|
925
|
+
const args = ["mcp", "add", "nous", "--scope", "user"];
|
|
926
|
+
if (apiUrl && apiUrl !== "https://api.opennous.cloud") args.push("-e", `NOUS_API_URL=${apiUrl}`);
|
|
927
|
+
args.push("--", MCP_CMD, ...MCP_ARGS);
|
|
928
|
+
const r = spawnSync("claude", args, { stdio: "ignore" });
|
|
929
|
+
if (r.status === 0) return "Registered Nous with Claude Code (claude mcp add).";
|
|
930
|
+
// add fails if a server named nous already exists — that's a success for our purposes.
|
|
931
|
+
return "Nous is already registered with Claude Code.";
|
|
932
|
+
}
|
|
933
|
+
const path = join(process.cwd(), ".mcp.json");
|
|
934
|
+
writeJsonMcp(path, apiUrl);
|
|
935
|
+
return `Wrote ${path} — your agent will pick up Nous from it.`;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// nous init — the one-line "get started": sign in, register the MCP, hand off to the agent.
|
|
939
|
+
// This is the command the install script and the marketing site point to.
|
|
726
940
|
program
|
|
727
941
|
.command("init")
|
|
728
|
-
.description("Get started — sign in
|
|
942
|
+
.description("Get started — sign in and connect Nous to your agent")
|
|
729
943
|
.option("--url <url>", "API base URL (default: https://api.opennous.cloud)")
|
|
730
|
-
.
|
|
944
|
+
.option("--no-register", "Skip registering the MCP server (just sign in)")
|
|
945
|
+
.action(async ({ url, register }) => {
|
|
731
946
|
await browserLogin(url);
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
947
|
+
|
|
948
|
+
const apiUrl = url || readConfig().apiUrl || "https://api.opennous.cloud";
|
|
949
|
+
if (register !== false) {
|
|
950
|
+
try {
|
|
951
|
+
console.log(`\n${registerMcp(apiUrl)}`);
|
|
952
|
+
} catch (e) {
|
|
953
|
+
// Never let registration take down sign-in — print the manual line instead.
|
|
954
|
+
console.log("\nCouldn't register the MCP automatically. Add it with:");
|
|
955
|
+
console.log(` claude mcp add nous -- ${MCP_CMD} ${MCP_ARGS.join(" ")}`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
console.log('\nYou\'re set. Tell your agent: "set up my Nous workspace" — it reads your');
|
|
960
|
+
console.log("project, finds your ICP (or drafts one), and syncs it. That's onboarding.\n");
|
|
961
|
+
console.log(dim("Docs: https://docs.opennous.cloud/mcp/introduction"));
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// nous pull — write the Vault into ./context/*.md and hand authorship to the repo.
|
|
965
|
+
//
|
|
966
|
+
// The bridge for the Google road. A user who set up in the browser has a Vault whose author
|
|
967
|
+
// is Nous (source 'nous', no file). The day they open a repo and connect an agent, that
|
|
968
|
+
// arrangement is backwards: they now live in files, and the Vault should mirror the files,
|
|
969
|
+
// not the other way round. `pull` flips it — it writes each context doc to disk and re-syncs
|
|
970
|
+
// it WITH its path, which sets source='claude_code'. From then on their repo is the author,
|
|
971
|
+
// exactly as if they'd started on the agent road.
|
|
972
|
+
//
|
|
973
|
+
// It only touches slots that exist, and by default won't clobber a file that's already there
|
|
974
|
+
// (the repo, once it's the author, wins) unless you pass --force.
|
|
975
|
+
const PULL_KINDS = { icp: "icp.md", positioning: "positioning.md", voice: "voice.md", outreach: "messaging.md" };
|
|
976
|
+
|
|
977
|
+
program
|
|
978
|
+
.command("pull")
|
|
979
|
+
.description("Write your Nous context docs into ./context and let your repo own them")
|
|
980
|
+
.option("--dir <path>", "Where to write the context/ folder (default: cwd)", process.cwd())
|
|
981
|
+
.option("--force", "Overwrite files that already exist", false)
|
|
982
|
+
.action(async ({ dir, force }) => {
|
|
983
|
+
const api = apiClient();
|
|
984
|
+
const { playbooks } = await api.get("/v2/playbooks");
|
|
985
|
+
const docs = (playbooks || []).filter((p) => PULL_KINDS[p.kind] && (p.body_md || "").trim());
|
|
986
|
+
if (!docs.length) {
|
|
987
|
+
console.log("Nothing to pull — your Vault has no context docs yet. Set one up in the app first.");
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const ctxDir = join(resolve(dir), "context");
|
|
992
|
+
if (!existsSync(ctxDir)) mkdirSync(ctxDir, { recursive: true });
|
|
993
|
+
|
|
994
|
+
let written = 0, skipped = 0, synced = 0;
|
|
995
|
+
for (const p of docs) {
|
|
996
|
+
const rel = join("context", PULL_KINDS[p.kind]);
|
|
997
|
+
const abs = join(resolve(dir), rel);
|
|
998
|
+
if (existsSync(abs) && !force) {
|
|
999
|
+
console.log(dim(` skip ${rel} (exists — pass --force to overwrite)`));
|
|
1000
|
+
skipped++;
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
writeFileSync(abs, p.body_md.trim() + "\n");
|
|
1004
|
+
written++;
|
|
1005
|
+
console.log(` write ${rel}`);
|
|
1006
|
+
// Re-sync WITH the path so the graph now knows the repo is the author.
|
|
1007
|
+
try {
|
|
1008
|
+
await api.post(`/v2/playbooks/${p.kind}`, { body_md: p.body_md, file_path: rel });
|
|
1009
|
+
synced++;
|
|
1010
|
+
} catch { /* the file is written; a failed authorship flip is non-fatal, re-run pull */ }
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
console.log(`\n✓ ${written} written${skipped ? `, ${skipped} skipped` : ""}. Your repo now owns your context.`);
|
|
1014
|
+
if (synced < written) console.log(dim(" (some didn't sync back — re-run `nous pull` to finish)"));
|
|
1015
|
+
console.log(dim("Edit these files and your agent syncs them with sync_playbook. Nous mirrors them."));
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// nous logout — top-level alias of `auth logout`. Removes the saved API key.
|
|
1019
|
+
program
|
|
1020
|
+
.command("logout")
|
|
1021
|
+
.description("Sign out — remove the saved API key")
|
|
1022
|
+
.action(() => {
|
|
1023
|
+
const cfg = readConfig();
|
|
1024
|
+
if (!cfg.apiKey) { console.log("Not signed in."); return; }
|
|
1025
|
+
delete cfg.apiKey;
|
|
1026
|
+
writeConfig(cfg);
|
|
1027
|
+
console.log("✓ Signed out — removed the saved API key from ~/.nous/config.json");
|
|
737
1028
|
});
|
|
738
1029
|
|
|
739
1030
|
// ---------------------------------------------------------------------------
|
|
740
|
-
program.name("nous").version("0.
|
|
1031
|
+
program.name("nous").version("0.7.0").parse();
|