@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.
- package/LICENSE +216 -0
- package/README.md +15 -6
- package/package.json +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 —
|
|
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://api.opennous.cloud/install | 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.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": "
|
|
15
|
+
"license": "Apache-2.0",
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|
|
18
|
-
"url": "https://github.com/NousC/
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
//
|
|
736
|
-
//
|
|
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
|
|
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
|
-
.
|
|
965
|
+
.option("--no-register", "Skip registering the MCP server (just sign in)")
|
|
966
|
+
.action(async ({ url, register }) => {
|
|
742
967
|
await browserLogin(url);
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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.
|
|
1052
|
+
program.name("nous").version("0.7.0").parse();
|