@opennous/cli 0.6.0 → 0.7.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.
Files changed (4) hide show
  1. package/LICENSE +216 -0
  2. package/README.md +15 -6
  3. package/package.json +4 -4
  4. package/src/index.js +306 -17
package/LICENSE ADDED
@@ -0,0 +1,216 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
203
+
204
+ Copyright 2026 Nous
205
+
206
+ Licensed under the Apache License, Version 2.0 (the "License");
207
+ you may not use this file except in compliance with the License.
208
+ You may obtain a copy of the License at
209
+
210
+ http://www.apache.org/licenses/LICENSE-2.0
211
+
212
+ Unless required by applicable law or agreed to in writing, software
213
+ distributed under the License is distributed on an "AS IS" BASIS,
214
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
215
+ See the License for the specific language governing permissions and
216
+ limitations under the License.
package/README.md CHANGED
@@ -1,21 +1,30 @@
1
1
  # @opennous/cli
2
2
 
3
- Nous CLI — install Nous event tracking into your app, plus terminal access to the Context API.
3
+ Nous CLI — set Nous up, connect it to your coding agent, and read the Context API from your terminal.
4
4
 
5
- ## Quick start
5
+ ## Get started
6
6
 
7
7
  ```bash
8
- # in your project root
9
- npx -y @opennous/cli@latest install
8
+ curl -fsSL https://api.opennous.cloud/install | sh
10
9
  ```
11
10
 
12
- `nous install` detects your stack (Next.js, Node, Python), writes a small `nous.js` (or `nous.py`) module with three helpers, and updates your `.env`:
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.6.0",
4
- "description": "Nous CLI — install Nous event tracking into your app, plus terminal access to the Context API",
3
+ "version": "0.7.1",
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"
@@ -12,10 +12,10 @@
12
12
  "start": "node src/index.js"
13
13
  },
14
14
  "keywords": ["nous", "crm", "cli", "agents", "memory", "observability", "stripe"],
15
- "license": "AGPL-3.0-only",
15
+ "license": "Apache-2.0",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "https://github.com/NousC/nous.git",
18
+ "url": "https://github.com/NousC/opennous.git",
19
19
  "directory": "apps/cli"
20
20
  },
21
21
  "homepage": "https://opennous.cloud",
package/src/index.js CHANGED
@@ -10,16 +10,43 @@ 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.
24
+ //
25
+ // Windows is the sharp edge here: `start` is a cmd.exe builtin, NOT an executable,
26
+ // so `spawn("start", ...)` fails with ENOENT — and that failure arrives as an async
27
+ // 'error' EVENT, not a throw, so a try/catch never sees it. An unhandled 'error' on
28
+ // a child process is an uncaught exception that kills the whole CLI before the
29
+ // device-auth wait. So: go through cmd.exe, and always attach an 'error' handler.
18
30
  function openBrowser(url) {
19
- const cmd = process.platform === "darwin" ? "open"
20
- : process.platform === "win32" ? "start"
21
- : "xdg-open";
22
- try { spawn(cmd, [url], { stdio: "ignore", detached: true }).unref(); } catch { /* printed URL is the fallback */ }
31
+ let cmd, args, opts = { stdio: "ignore", detached: true };
32
+ if (process.platform === "darwin") {
33
+ cmd = "open"; args = [url];
34
+ } else if (process.platform === "win32") {
35
+ // cmd.exe /c start "" "<url>". The empty "" is start's title argument (else a
36
+ // quoted URL is taken as the window title). windowsVerbatimArguments keeps the
37
+ // quotes we add, so an & in the auth URL's query string stays inside them and
38
+ // isn't parsed by cmd as a command separator.
39
+ cmd = process.env.ComSpec || "cmd.exe";
40
+ args = ["/c", "start", "", `"${url}"`];
41
+ opts.windowsVerbatimArguments = true;
42
+ } else {
43
+ cmd = "xdg-open"; args = [url];
44
+ }
45
+ try {
46
+ const child = spawn(cmd, args, opts);
47
+ child.on("error", () => { /* printed URL is the fallback — never crash on this */ });
48
+ child.unref();
49
+ } catch { /* printed URL is the fallback */ }
23
50
  }
24
51
 
25
52
  const CONFIG_PATH = join(homedir(), ".nous", "config.json");
@@ -293,8 +320,17 @@ const NEXT_STEPS = {
293
320
  3. Call handle_stripe_event(event, customer_email) inside your Stripe webhook view.`,
294
321
  };
295
322
 
296
- program
297
- .command("install")
323
+ // `nous track init` — drop event tracking into the user's app. This used to be
324
+ // `nous install`, which collided head-on with the front-door meaning of "install":
325
+ // people typed `nous install` expecting to set Nous up and got an app-instrumentation
326
+ // scaffolder instead. The front door is now `nous init`. `install` stays as a hidden
327
+ // deprecated alias so nobody's script breaks, and it prints where the command went.
328
+ const trackCmd = program
329
+ .command("track")
330
+ .description("Instrument your app to send events to Nous");
331
+
332
+ function defineTrackInit(cmd) {
333
+ return cmd
298
334
  .description("Drop Nous event tracking (signups + Stripe lifecycle) into the current project")
299
335
  .option("--dir <path>", "Target project directory (default: cwd)", process.cwd())
300
336
  .option("--lang <stack>", "Force language/stack: next | node-esm | node-cjs | python")
@@ -353,6 +389,17 @@ program
353
389
  console.log(`\nDetected stack: ${stack}`);
354
390
  console.log(`\n${NEXT_STEPS[stack] || NEXT_STEPS["node-esm"]}\n`);
355
391
  });
392
+ }
393
+
394
+ // The real command: `nous track init`.
395
+ defineTrackInit(trackCmd.command("init"));
396
+
397
+ // Deprecated alias: `nous install`. Kept so existing scripts don't break; it does the
398
+ // same thing but says where it moved, because "install" is now `nous init`.
399
+ defineTrackInit(program.command("install"))
400
+ .hook("preAction", () => {
401
+ console.error(dim("note: `nous install` is now `nous track init`. Run `nous init` to set up Nous itself.\n"));
402
+ });
356
403
 
357
404
  // ---------------------------------------------------------------------------
358
405
  // nous auth
@@ -596,6 +643,118 @@ program
596
643
  });
597
644
  });
598
645
 
646
+ // ---------------------------------------------------------------------------
647
+ // nous audit — is this graph actually sound?
648
+ //
649
+ // The command that answers the only question a technical team has before trusting
650
+ // data they didn't collect themselves. It is deliberately a COMMAND and not a
651
+ // dashboard: a dashboard makes a claim about our own product and asks you to
652
+ // believe it; a command runs against your data, in your terminal, and returns an
653
+ // exit code. One of those can fail a build.
654
+ // ---------------------------------------------------------------------------
655
+ program
656
+ .command("audit")
657
+ .description("Audit your GTM data — what's broken, what's unsure, and what to do about it")
658
+ .option("--check <name>", "One check only: arriving, resolved, evidenced, current, reachable")
659
+ .option("--list", "List the records behind each finding")
660
+ .option("--fail-under <n>", "Exit 1 if health is below this percentage — for CI")
661
+ .option("--fix", "Show the repairs that can be made safely, without making them")
662
+ .option("--apply", "Actually make them (use with --fix)")
663
+ .option("--json", "Print the raw JSON response")
664
+ .action(async ({ check, list, failUnder, fix, apply, json }) => {
665
+ const api = apiClient();
666
+
667
+ // ── nous audit --fix ──
668
+ //
669
+ // Only the repairs that need no judgment: records sharing an email or a
670
+ // LinkedIn profile are the same human by definition. Everything else the audit
671
+ // finds needs a decision, a browser, or you — and a --fix that quietly did
672
+ // those too would be one nobody could trust.
673
+ if (fix) {
674
+ const out = await api.post("/v2/audit/fix", { apply: !!apply });
675
+ if (json) { console.log(JSON.stringify(out, null, 2)); return; }
676
+
677
+ if (out.dry_run) {
678
+ if (!out.would_merge) {
679
+ console.log("\n Nothing to repair automatically.\n");
680
+ return;
681
+ }
682
+ console.log(`\n ${out.would_merge} ${out.would_merge === 1 ? "merge" : "merges"} can be made safely:\n`);
683
+ for (const m of out.merges) {
684
+ console.log(` · ${m.who ?? m.keep} ${dim("— " + m.why)}`);
685
+ }
686
+ console.log(dim("\n Nothing changed. Re-run with --fix --apply to make them.\n"));
687
+ return;
688
+ }
689
+
690
+ console.log(`\n Merged ${out.merged}.${out.failed ? ` ${out.failed} failed.` : ""}\n`);
691
+ for (const f of out.failures ?? []) console.error(` ✗ ${f.who ?? f.keep}: ${f.error}`);
692
+ if (out.failed) process.exit(1);
693
+ return;
694
+ }
695
+
696
+ const data = await api.get("/v2/audit", check ? { check } : {});
697
+
698
+ if (json) {
699
+ console.log(JSON.stringify(data, null, 2));
700
+ } else {
701
+ const checks = check ? [data.check] : data.checks;
702
+ const findings = data.findings ?? [];
703
+
704
+ console.log("");
705
+ for (const c of checks) {
706
+ const pct = String(c.pct).padStart(4);
707
+ const mark = c.pct === 100 ? "\u2713" : "\u2717";
708
+ console.log(` ${pct}% ${c.label.toUpperCase().padEnd(11)} ${c.summary} ${mark}`);
709
+ // What we deliberately did not count. An audit cannot itself be a black box.
710
+ if (c.note) console.log(` ${dim(c.note)}`);
711
+ }
712
+
713
+ if (!findings.length) {
714
+ console.log("\n Nothing to fix.\n");
715
+ } else {
716
+ console.log("");
717
+ for (const f of findings) {
718
+ console.log(` [${f.severity.toUpperCase()}] ${f.title}`);
719
+ console.log(` ${dim(f.detail)}`);
720
+ // Who can fix it, said plainly. Pretending we can re-authorise someone's
721
+ // Gmail is how a tool loses trust the first time it fails to.
722
+ const by = f.fixable_by === "human"
723
+ ? "needs you"
724
+ : f.fixable_by === "agent_with_confirmation"
725
+ ? "the agent, once you confirm"
726
+ : "the agent";
727
+ console.log(` \u2192 ${f.fix} (${by})`);
728
+
729
+ if (list && f.subjects?.length) {
730
+ for (const sub of f.subjects.slice(0, 20)) {
731
+ const line = sub.who
732
+ ? (Array.isArray(sub.who) ? sub.who.join(" \u2194 ") : sub.who)
733
+ : Object.entries(sub).map(([k, v]) => `${k}=${v}`).join(" ");
734
+ console.log(` \u00b7 ${line}${sub.why ? dim(" — " + sub.why) : ""}`);
735
+ }
736
+ if (f.subjects.length > 20) {
737
+ console.log(` \u00b7 ${dim(`and ${f.subjects.length - 20} more`)}`);
738
+ }
739
+ }
740
+ console.log("");
741
+ }
742
+ if (!list) console.log(dim(" Add --list to see the records behind each finding.\n"));
743
+ }
744
+ }
745
+
746
+ // The reason this is a command. An exit code can stop a deploy; a dashboard
747
+ // cannot. You would have known about a dead connector on day one, not day 48.
748
+ const health = check ? data.check.pct : data.health;
749
+ if (failUnder !== undefined) {
750
+ const floor = Number(failUnder);
751
+ if (health < floor) {
752
+ console.error(`\n FAIL: health ${health}% is below the threshold of ${floor}%\n`);
753
+ process.exit(1);
754
+ }
755
+ }
756
+ });
757
+
599
758
  // ---------------------------------------------------------------------------
600
759
  // nous verify <focus> <property> — re-check a claim before acting on it
601
760
  // ---------------------------------------------------------------------------
@@ -732,19 +891,149 @@ program
732
891
  console.log("Your Nous tools are ready — try `get_workspace_status` in your agent.");
733
892
  });
734
893
 
735
- // nous init — the one-line "get started": sign in, then connect to your agent.
736
- // This is the command the marketing site points to (`npx @opennous/cli init`).
894
+ // ---------------------------------------------------------------------------
895
+ // MCP registration — connect Nous to whatever agent is on this machine.
896
+ //
897
+ // We deliberately DO NOT use the Claude Code plugin/marketplace here. Two commands
898
+ // (`/plugin marketplace add`, `/plugin install`), Claude-Code-only, with a marketplace
899
+ // standing between a new user and the product — it's too much for a first run. The MCP
900
+ // server is the whole integration, and `claude mcp add` registers it in one line.
901
+ //
902
+ // The MCP resolves its key from ~/.nous/config.json on every call, so we never bake the
903
+ // key into any of these configs — sign-in already wrote it there.
904
+ // ---------------------------------------------------------------------------
905
+
906
+ const MCP_CMD = "npx";
907
+ const MCP_ARGS = ["-y", "@opennous/mcp"];
908
+
909
+ /** Is the `claude` CLI on PATH? Then registration is one command and needs no file. */
910
+ function hasClaudeCli() {
911
+ try {
912
+ const r = spawnSync("claude", ["--version"], { stdio: "ignore" });
913
+ return r.status === 0;
914
+ } catch { return false; }
915
+ }
916
+
917
+ /** Merge a `nous` server into a JSON MCP config file (Claude Code / Cursor shape). */
918
+ function writeJsonMcp(path, apiUrl) {
919
+ let cfg = {};
920
+ if (existsSync(path)) {
921
+ try { cfg = JSON.parse(readFileSync(path, "utf8")); } catch { cfg = {}; }
922
+ }
923
+ cfg.mcpServers = cfg.mcpServers || {};
924
+ cfg.mcpServers.nous = {
925
+ command: MCP_CMD,
926
+ args: MCP_ARGS,
927
+ // Only pin a non-default API URL (self-host). Cloud reads the key from the config file.
928
+ ...(apiUrl && apiUrl !== "https://api.opennous.cloud" ? { env: { NOUS_API_URL: apiUrl } } : {}),
929
+ };
930
+ const dir = path.slice(0, path.lastIndexOf("/"));
931
+ if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true });
932
+ writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
933
+ }
934
+
935
+ /**
936
+ * Register the MCP server with the agent on this machine. Returns a short line describing
937
+ * what happened, for the caller to print.
938
+ *
939
+ * Order of preference:
940
+ * 1. `claude mcp add` if the Claude CLI is here — one command, no file to manage.
941
+ * 2. Write ./.mcp.json in the current project (Claude Code and most clients read it).
942
+ * We never fail the flow on this — a printed fallback is better than an abort.
943
+ */
944
+ function registerMcp(apiUrl) {
945
+ if (hasClaudeCli()) {
946
+ const args = ["mcp", "add", "nous", "--scope", "user"];
947
+ if (apiUrl && apiUrl !== "https://api.opennous.cloud") args.push("-e", `NOUS_API_URL=${apiUrl}`);
948
+ args.push("--", MCP_CMD, ...MCP_ARGS);
949
+ const r = spawnSync("claude", args, { stdio: "ignore" });
950
+ if (r.status === 0) return "Registered Nous with Claude Code (claude mcp add).";
951
+ // add fails if a server named nous already exists — that's a success for our purposes.
952
+ return "Nous is already registered with Claude Code.";
953
+ }
954
+ const path = join(process.cwd(), ".mcp.json");
955
+ writeJsonMcp(path, apiUrl);
956
+ return `Wrote ${path} — your agent will pick up Nous from it.`;
957
+ }
958
+
959
+ // nous init — the one-line "get started": sign in, register the MCP, hand off to the agent.
960
+ // This is the command the install script and the marketing site point to.
737
961
  program
738
962
  .command("init")
739
- .description("Get started — sign in, then connect Nous to your agent")
963
+ .description("Get started — sign in and connect Nous to your agent")
740
964
  .option("--url <url>", "API base URL (default: https://api.opennous.cloud)")
741
- .action(async ({ url }) => {
965
+ .option("--no-register", "Skip registering the MCP server (just sign in)")
966
+ .action(async ({ url, register }) => {
742
967
  await browserLogin(url);
743
- console.log("\nNow add Nous to Claude Code:\n");
744
- console.log(" /plugin marketplace add NousC/nous");
745
- console.log(" /plugin install nous@nous-plugins\n");
746
- console.log('Then tell your agent: "set me up" — it onboards your workspace and builds your playbook.');
747
- console.log("\nDocs: https://docs.opennous.cloud/mcp/introduction");
968
+
969
+ const apiUrl = url || readConfig().apiUrl || "https://api.opennous.cloud";
970
+ if (register !== false) {
971
+ try {
972
+ console.log(`\n${registerMcp(apiUrl)}`);
973
+ } catch (e) {
974
+ // Never let registration take down sign-in — print the manual line instead.
975
+ console.log("\nCouldn't register the MCP automatically. Add it with:");
976
+ console.log(` claude mcp add nous -- ${MCP_CMD} ${MCP_ARGS.join(" ")}`);
977
+ }
978
+ }
979
+
980
+ console.log('\nYou\'re set. Tell your agent: "set up my GTM context layer and connect it to');
981
+ console.log("Nous\" — it audits your project's context, drafts what's missing, and syncs it.\n");
982
+ console.log(dim("Docs: https://docs.opennous.cloud/mcp/introduction"));
983
+ });
984
+
985
+ // nous pull — write the Vault into ./context/*.md and hand authorship to the repo.
986
+ //
987
+ // The bridge for the Google road. A user who set up in the browser has a Vault whose author
988
+ // is Nous (source 'nous', no file). The day they open a repo and connect an agent, that
989
+ // arrangement is backwards: they now live in files, and the Vault should mirror the files,
990
+ // not the other way round. `pull` flips it — it writes each context doc to disk and re-syncs
991
+ // it WITH its path, which sets source='claude_code'. From then on their repo is the author,
992
+ // exactly as if they'd started on the agent road.
993
+ //
994
+ // It only touches slots that exist, and by default won't clobber a file that's already there
995
+ // (the repo, once it's the author, wins) unless you pass --force.
996
+ const PULL_KINDS = { icp: "icp.md", positioning: "positioning.md", voice: "voice.md", outreach: "messaging.md" };
997
+
998
+ program
999
+ .command("pull")
1000
+ .description("Write your Nous context docs into ./context and let your repo own them")
1001
+ .option("--dir <path>", "Where to write the context/ folder (default: cwd)", process.cwd())
1002
+ .option("--force", "Overwrite files that already exist", false)
1003
+ .action(async ({ dir, force }) => {
1004
+ const api = apiClient();
1005
+ const { playbooks } = await api.get("/v2/playbooks");
1006
+ const docs = (playbooks || []).filter((p) => PULL_KINDS[p.kind] && (p.body_md || "").trim());
1007
+ if (!docs.length) {
1008
+ console.log("Nothing to pull — your Vault has no context docs yet. Set one up in the app first.");
1009
+ return;
1010
+ }
1011
+
1012
+ const ctxDir = join(resolve(dir), "context");
1013
+ if (!existsSync(ctxDir)) mkdirSync(ctxDir, { recursive: true });
1014
+
1015
+ let written = 0, skipped = 0, synced = 0;
1016
+ for (const p of docs) {
1017
+ const rel = join("context", PULL_KINDS[p.kind]);
1018
+ const abs = join(resolve(dir), rel);
1019
+ if (existsSync(abs) && !force) {
1020
+ console.log(dim(` skip ${rel} (exists — pass --force to overwrite)`));
1021
+ skipped++;
1022
+ continue;
1023
+ }
1024
+ writeFileSync(abs, p.body_md.trim() + "\n");
1025
+ written++;
1026
+ console.log(` write ${rel}`);
1027
+ // Re-sync WITH the path so the graph now knows the repo is the author.
1028
+ try {
1029
+ await api.post(`/v2/playbooks/${p.kind}`, { body_md: p.body_md, file_path: rel });
1030
+ synced++;
1031
+ } catch { /* the file is written; a failed authorship flip is non-fatal, re-run pull */ }
1032
+ }
1033
+
1034
+ console.log(`\n✓ ${written} written${skipped ? `, ${skipped} skipped` : ""}. Your repo now owns your context.`);
1035
+ if (synced < written) console.log(dim(" (some didn't sync back — re-run `nous pull` to finish)"));
1036
+ console.log(dim("Edit these files and your agent syncs them with sync_playbook. Nous mirrors them."));
748
1037
  });
749
1038
 
750
1039
  // nous logout — top-level alias of `auth logout`. Removes the saved API key.
@@ -760,4 +1049,4 @@ program
760
1049
  });
761
1050
 
762
1051
  // ---------------------------------------------------------------------------
763
- program.name("nous").version("0.6.0").parse();
1052
+ program.name("nous").version("0.7.0").parse();