@nocville/npc 0.3.1 → 0.4.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/README.md +32 -11
- package/dist/cjs/cli.js +53 -16
- package/dist/cjs/core/base-npc.js +1 -0
- package/dist/cjs/core/browser-connect.js +196 -0
- package/dist/cjs/core/config.js +23 -1
- package/dist/cjs/core/user-config.js +2 -0
- package/dist/cjs/doctor.js +19 -0
- package/dist/cjs/npcs/agent-common/runtime.js +54 -22
- package/dist/cjs/npcs/agent-common/session.js +3 -0
- package/dist/cjs/npcs/codex/watcher.js +1 -0
- package/dist/cli.js +53 -16
- package/dist/core/base-npc.js +1 -0
- package/dist/core/browser-connect.d.ts +16 -0
- package/dist/core/browser-connect.js +189 -0
- package/dist/core/config.js +23 -1
- package/dist/core/types.d.ts +2 -0
- package/dist/core/user-config.d.ts +1 -0
- package/dist/core/user-config.js +2 -0
- package/dist/doctor.js +19 -0
- package/dist/npcs/agent-common/runtime.js +54 -22
- package/dist/npcs/agent-common/session.js +3 -0
- package/dist/npcs/codex/watcher.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,18 +7,22 @@ a hand when it needs you, dozes when idle, and shows what it's doing on its spee
|
|
|
7
7
|
desk sign: `Editing src/GameScene.ts`, `Running pnpm test`, `Searching for 'reconnect'`,
|
|
8
8
|
`Needs your approval`, `Thinking…`. It leaves the office when the session ends.
|
|
9
9
|
|
|
10
|
-
Requires Node.js 20
|
|
10
|
+
Requires Node.js 20.12 or newer and a Nocville account.
|
|
11
11
|
|
|
12
12
|
## Quick start: AI coworkers
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
npx @nocville/npc setup
|
|
15
|
+
npx @nocville/npc@latest setup
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
Setup opens Nocville in your browser. Sign in, choose your team and office, and click
|
|
19
|
+
**Connect this computer**. If the browser asks for local network access, allow it so
|
|
20
|
+
it can return the connection to the setup command on this computer. Keep an existing
|
|
21
|
+
team member's office open during the first connection so encrypted publishing can
|
|
22
|
+
be enabled automatically.
|
|
23
|
+
|
|
24
|
+
Setup saves the publishing key and verified workspace settings in private files under
|
|
25
|
+
`~/.config/nocville/`, then hooks up whichever agents you have installed:
|
|
22
26
|
|
|
23
27
|
- **Claude Code**: adds hooks to `~/.claude/settings.json`. New sessions show up right away;
|
|
24
28
|
restart sessions that are already running.
|
|
@@ -31,10 +35,10 @@ up whichever agents you have installed:
|
|
|
31
35
|
Your existing hooks are left alone, and Nocville's hooks never answer permission prompts or
|
|
32
36
|
change what your agent does; they only report activity.
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
Using your team’s own Cloudflare hosting? Add `--api-url=https://your-office-host`.
|
|
39
|
+
Use `setup --reconnect` to choose a different office or renew the connection.
|
|
36
40
|
|
|
37
|
-
Prefer a global install? Run `npm i -g @nocville/npc`, then `nocville-npc setup
|
|
41
|
+
Prefer a global install? Run `npm i -g @nocville/npc`, then `nocville-npc setup`.
|
|
38
42
|
When setup runs through `npx`, it copies itself to `~/.config/nocville/runtime/` so the hooks
|
|
39
43
|
keep working after npm clears its cache.
|
|
40
44
|
|
|
@@ -75,8 +79,11 @@ checks the current folder (`--project=<dir>`, `--name=<session name>` to test ot
|
|
|
75
79
|
Gemini CLI and Cursor on their own. For Cursor, start a new chat after setup.
|
|
76
80
|
|
|
77
81
|
`nocville-npc uninstall` removes every Nocville hook. `nocville-npc init` only saves
|
|
78
|
-
credentials.
|
|
79
|
-
|
|
82
|
+
credentials. Cloudflare setup connects through your browser unless a complete encrypted
|
|
83
|
+
workspace is already configured. For automated installations, supply a trusted
|
|
84
|
+
`--workspace=/absolute/path/workspace.json` and `NOCVILLE_API_KEY`. Legacy self-hosted
|
|
85
|
+
servers still accept the hidden key prompt. `nocville-npc doctor` checks connectivity
|
|
86
|
+
and sends a test push.
|
|
80
87
|
|
|
81
88
|
### What you'll see
|
|
82
89
|
|
|
@@ -226,3 +233,17 @@ class MyNPC extends PollingNPC {
|
|
|
226
233
|
|
|
227
234
|
`nocville-npc --help` lists every command and flag, and `nocville-npc doctor` checks that your
|
|
228
235
|
key and connection work. More at [nocville.com](https://nocville.com).
|
|
236
|
+
|
|
237
|
+
## Encrypted Cloudflare workspaces
|
|
238
|
+
|
|
239
|
+
Run `npx @nocville/npc@latest setup` to configure encrypted publishing automatically.
|
|
240
|
+
Old keys from the former server deployment cannot publish to encrypted workspaces;
|
|
241
|
+
setup replaces that connection without removing other agents' hooks.
|
|
242
|
+
|
|
243
|
+
For managed automation, `--workspace`, `NOCVILLE_WORKSPACE`, and the saved workspace
|
|
244
|
+
path are supported in that order. Constructor `workspace` overrides them. The
|
|
245
|
+
workspace pins the HTTPS origin, deployment, team root and map ID. Reconnecting to
|
|
246
|
+
the same team cannot silently replace its pinned encryption identity. Local NPCs,
|
|
247
|
+
agent hooks and watchdogs share an enrolled producer profile for the credential.
|
|
248
|
+
The `doctor` command uses encrypted enrollment and an offline probe. The Node SDK
|
|
249
|
+
README describes private profile storage and the encryption boundary.
|
package/dist/cjs/cli.js
CHANGED
|
@@ -47,9 +47,9 @@ const USAGE = `
|
|
|
47
47
|
nocville-npc <command> [options]
|
|
48
48
|
|
|
49
49
|
Quick start (AI coworkers):
|
|
50
|
-
nocville-npc setup
|
|
50
|
+
nocville-npc setup Connect your office in the browser,
|
|
51
51
|
then hook up installed coding agents
|
|
52
|
-
|
|
52
|
+
nocville-npc setup --reconnect Choose an office and renew this connection
|
|
53
53
|
nocville-npc setup --api-key=<agent key> Same, non-interactive
|
|
54
54
|
nocville-npc uninstall Remove all Nocville hooks
|
|
55
55
|
|
|
@@ -96,6 +96,7 @@ Monitoring NPC types:
|
|
|
96
96
|
web-page-watcher Render a watched web page as a pixel-art TV
|
|
97
97
|
|
|
98
98
|
Common Options:
|
|
99
|
+
--workspace=<file> Verified producer workspace settings from team privacy settings.
|
|
99
100
|
--api-key=<key> Team API key (or NOCVILLE_API_KEY env)
|
|
100
101
|
--api-url=<url> Nocville API URL (or NOCVILLE_API_URL env).
|
|
101
102
|
Default: https://nocville.com. For local dev use
|
|
@@ -185,22 +186,27 @@ async function main() {
|
|
|
185
186
|
console.log(USAGE);
|
|
186
187
|
process.exit(1);
|
|
187
188
|
}
|
|
188
|
-
|
|
189
|
+
let config = (0, config_js_1.resolveConfig)(parsed.flags);
|
|
189
190
|
const logger = new logger_js_1.Logger('cli', config.debug);
|
|
190
191
|
// Setup-style commands resolve the key: --api-key > env > saved > hidden prompt.
|
|
191
192
|
const isAgentSetup = ['claude-code', 'codex', 'gemini', 'cursor'].includes(parsed.command) &&
|
|
192
193
|
parsed.subcommand === 'setup' &&
|
|
193
194
|
!parsed.flags['uninstall'];
|
|
194
195
|
if (isAgentSetup || parsed.command === 'setup') {
|
|
195
|
-
if (
|
|
196
|
-
const {
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
196
|
+
if (!config.workspace || !config.apiKey.includes('.team_') || parsed.flags['reconnect']) {
|
|
197
|
+
const { connectCloudflareOffice } = await Promise.resolve().then(() => __importStar(require('./core/browser-connect.js')));
|
|
198
|
+
if (await connectCloudflareOffice(config.apiUrl)) {
|
|
199
|
+
const { loadUserConfig } = await Promise.resolve().then(() => __importStar(require('./core/user-config.js')));
|
|
200
|
+
const saved = loadUserConfig();
|
|
201
|
+
for (const [flag, value] of Object.entries({
|
|
202
|
+
'api-key': saved.apiKey,
|
|
203
|
+
'api-url': saved.apiUrl,
|
|
204
|
+
map: saved.map,
|
|
205
|
+
workspace: saved.workspace,
|
|
206
|
+
}))
|
|
207
|
+
if (value)
|
|
208
|
+
parsed.flags[flag] = value;
|
|
209
|
+
config = (0, config_js_1.resolveConfig)(parsed.flags);
|
|
204
210
|
}
|
|
205
211
|
}
|
|
206
212
|
const key = await resolveSetupKey(parsed.flags);
|
|
@@ -208,6 +214,16 @@ async function main() {
|
|
|
208
214
|
return;
|
|
209
215
|
if (key !== undefined)
|
|
210
216
|
parsed.flags['api-key'] = key;
|
|
217
|
+
config = (0, config_js_1.resolveConfig)(parsed.flags);
|
|
218
|
+
if (config.workspace) {
|
|
219
|
+
const { saveUserConfig } = await Promise.resolve().then(() => __importStar(require('./core/user-config.js')));
|
|
220
|
+
saveUserConfig({
|
|
221
|
+
workspace: config.workspace,
|
|
222
|
+
apiUrl: config.apiUrl,
|
|
223
|
+
apiKey: config.apiKey,
|
|
224
|
+
map: config.map,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
211
227
|
}
|
|
212
228
|
// AI-agent integrations have their own config handling (hooks read the saved config).
|
|
213
229
|
if (parsed.command === 'claude-code') {
|
|
@@ -290,6 +306,10 @@ async function main() {
|
|
|
290
306
|
return;
|
|
291
307
|
}
|
|
292
308
|
// All other NPC types require api-url and token
|
|
309
|
+
if (!config.workspace &&
|
|
310
|
+
(config.apiKey.includes('.team_') ||
|
|
311
|
+
config.apiUrl.replace(/\/+$/, '') === 'https://nocville.com'))
|
|
312
|
+
throw new Error('Connect this computer to your office first: run npx @nocville/npc@latest setup. Existing legacy keys cannot publish encrypted NPC updates.');
|
|
293
313
|
if (!config.apiKey) {
|
|
294
314
|
console.error('Error: no API key — run `nocville-npc init --api-key=<key>`, or pass --api-key / NOCVILLE_API_KEY');
|
|
295
315
|
process.exit(1);
|
|
@@ -512,7 +532,20 @@ async function runInit(flags, config) {
|
|
|
512
532
|
let apiKey = (0, config_js_1.stringFlag)(flags, 'api-key');
|
|
513
533
|
const apiUrl = (0, config_js_1.stringFlag)(flags, 'api-url');
|
|
514
534
|
const map = (0, config_js_1.stringFlag)(flags, 'map');
|
|
515
|
-
|
|
535
|
+
const workspace = config.workspace;
|
|
536
|
+
if (workspace)
|
|
537
|
+
apiKey = apiKey || config.apiKey;
|
|
538
|
+
if (workspace && !apiKey) {
|
|
539
|
+
const { resolveApiKey } = await Promise.resolve().then(() => __importStar(require('./core/api-key.js')));
|
|
540
|
+
const resolved = await resolveApiKey({ flags });
|
|
541
|
+
if (!resolved.ok) {
|
|
542
|
+
console.error(resolved.error);
|
|
543
|
+
process.exitCode = 1;
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
apiKey = resolved.key;
|
|
547
|
+
}
|
|
548
|
+
if (!apiKey && !apiUrl && !map && !workspace) {
|
|
516
549
|
const saved = loadUserConfig();
|
|
517
550
|
if (saved.apiKey && !process.env['NOCVILLE_API_KEY']?.trim()) {
|
|
518
551
|
console.log('Usage: nocville-npc init --api-key=<key> [--api-url=<url>] [--map=<name>]');
|
|
@@ -528,7 +561,12 @@ async function runInit(flags, config) {
|
|
|
528
561
|
}
|
|
529
562
|
apiKey = resolved.key;
|
|
530
563
|
}
|
|
531
|
-
const path = saveUserConfig({
|
|
564
|
+
const path = saveUserConfig({
|
|
565
|
+
apiKey,
|
|
566
|
+
apiUrl: workspace ? config.apiUrl : apiUrl,
|
|
567
|
+
map: workspace ? config.map : map,
|
|
568
|
+
workspace,
|
|
569
|
+
});
|
|
532
570
|
const saved = loadUserConfig();
|
|
533
571
|
console.log(`✓ Saved ${path} (mode 0600)`);
|
|
534
572
|
console.log(` API: ${saved.apiUrl ?? 'https://nocville.com'} key: ${maskKey(saved.apiKey)}`);
|
|
@@ -620,8 +658,7 @@ async function runSetupAll(flags) {
|
|
|
620
658
|
console.log('· Cursor not found (~/.cursor missing) — skipped');
|
|
621
659
|
}
|
|
622
660
|
if (!any) {
|
|
623
|
-
console.
|
|
624
|
-
process.exitCode = 1;
|
|
661
|
+
console.log('No supported coding agents found. You can still run NPC commands on this computer.');
|
|
625
662
|
}
|
|
626
663
|
}
|
|
627
664
|
main().catch((err) => {
|
|
@@ -17,6 +17,7 @@ class BaseNPC {
|
|
|
17
17
|
// We can't access `this.name` here since it's abstract, so use a placeholder
|
|
18
18
|
this.logger = new logger_js_1.Logger('npc', config.debug);
|
|
19
19
|
this.client = new node_1.NocvilleClient({
|
|
20
|
+
workspace: config.workspace,
|
|
20
21
|
apiUrl: config.apiUrl,
|
|
21
22
|
apiKey: config.apiKey,
|
|
22
23
|
serviceId: config.serviceId,
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateConnection = validateConnection;
|
|
4
|
+
exports.browserConnection = browserConnection;
|
|
5
|
+
exports.saveBrowserConnection = saveBrowserConnection;
|
|
6
|
+
exports.openConnectionBrowser = openConnectionBrowser;
|
|
7
|
+
exports.connectCloudflareOffice = connectCloudflareOffice;
|
|
8
|
+
const node_http_1 = require("node:http");
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const node_child_process_1 = require("node:child_process");
|
|
11
|
+
const node_fs_1 = require("node:fs");
|
|
12
|
+
const node_path_1 = require("node:path");
|
|
13
|
+
const node_1 = require("@nocville/node");
|
|
14
|
+
const user_config_js_1 = require("./user-config.js");
|
|
15
|
+
function validateConnection(input, origin, deploymentId) {
|
|
16
|
+
const value = input;
|
|
17
|
+
const e = value?.workspace?.encryption;
|
|
18
|
+
if (typeof value?.apiKey !== 'string' ||
|
|
19
|
+
!/^nvk_[a-f0-9]{64}\.team_[A-Za-z0-9_-]{1,64}$/.test(value.apiKey) ||
|
|
20
|
+
value.workspace?.apiUrl !== origin ||
|
|
21
|
+
e?.deploymentId !== deploymentId ||
|
|
22
|
+
!/^team_[A-Za-z0-9_-]{1,64}$/.test(e?.teamId ?? '') ||
|
|
23
|
+
!/^map_[A-Za-z0-9_-]{1,128}$/.test(e?.mapId ?? '') ||
|
|
24
|
+
!/^[A-Za-z0-9_-]{43}$/.test(e?.trustedRoot ?? '') ||
|
|
25
|
+
!value.apiKey.endsWith('.' + e.teamId) ||
|
|
26
|
+
e.profileDirectory !== undefined)
|
|
27
|
+
throw new Error('Invalid office connection. Start setup again.');
|
|
28
|
+
return {
|
|
29
|
+
apiKey: value.apiKey,
|
|
30
|
+
workspace: {
|
|
31
|
+
apiUrl: origin,
|
|
32
|
+
encryption: {
|
|
33
|
+
deploymentId,
|
|
34
|
+
teamId: e.teamId,
|
|
35
|
+
mapId: e.mapId,
|
|
36
|
+
trustedRoot: e.trustedRoot,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** A one-use loopback receiver. Credentials never appear in a URL or terminal output. */
|
|
42
|
+
async function browserConnection(origin, deploymentId, save, timeoutMs = 300000) {
|
|
43
|
+
const target = new URL(origin);
|
|
44
|
+
if (target.origin !== origin ||
|
|
45
|
+
target.username ||
|
|
46
|
+
target.password ||
|
|
47
|
+
(target.protocol !== 'https:' &&
|
|
48
|
+
!(target.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(target.hostname))))
|
|
49
|
+
throw new Error('Use an HTTPS office address.');
|
|
50
|
+
const state = (0, node_crypto_1.randomBytes)(32).toString('hex');
|
|
51
|
+
let finish, fail;
|
|
52
|
+
const done = new Promise((resolve, reject) => {
|
|
53
|
+
finish = resolve;
|
|
54
|
+
fail = reject;
|
|
55
|
+
});
|
|
56
|
+
// A timeout may happen while the browser launcher is still opening.
|
|
57
|
+
void done.catch(() => { });
|
|
58
|
+
let consumed = false, port = 0;
|
|
59
|
+
const server = (0, node_http_1.createServer)(async (request, response) => {
|
|
60
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
61
|
+
if (request.headers.host !== `127.0.0.1:${port}` ||
|
|
62
|
+
request.url !== '/complete' ||
|
|
63
|
+
request.headers.origin !== origin) {
|
|
64
|
+
response.writeHead(403).end();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
response.setHeader('Access-Control-Allow-Origin', origin);
|
|
68
|
+
response.setHeader('Vary', 'Origin');
|
|
69
|
+
if (request.method === 'OPTIONS') {
|
|
70
|
+
response.setHeader('Access-Control-Allow-Methods', 'POST');
|
|
71
|
+
response.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
72
|
+
response.setHeader('Access-Control-Allow-Private-Network', 'true');
|
|
73
|
+
response.writeHead(204).end();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (request.method !== 'POST' ||
|
|
77
|
+
request.headers['content-type']?.split(';')[0] !== 'application/json') {
|
|
78
|
+
response.writeHead(415).end();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
let size = 0;
|
|
83
|
+
const chunks = [];
|
|
84
|
+
for await (const chunk of request) {
|
|
85
|
+
size += chunk.length;
|
|
86
|
+
if (size > 16384) {
|
|
87
|
+
response.writeHead(413).end();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
chunks.push(Buffer.from(chunk));
|
|
91
|
+
}
|
|
92
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
93
|
+
if (body.state !== state || consumed) {
|
|
94
|
+
response.writeHead(403).end();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const value = validateConnection(body, origin, deploymentId);
|
|
98
|
+
consumed = true;
|
|
99
|
+
await save(value);
|
|
100
|
+
response.once('finish', finish);
|
|
101
|
+
response.writeHead(200, { 'Content-Type': 'application/json' }).end('{"connected":true}');
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
response.writeHead(400).end('Could not save this office connection. Start setup again.');
|
|
105
|
+
if (consumed)
|
|
106
|
+
fail(new Error('Could not save the office connection. Start setup again.'));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
server.requestTimeout = 15000;
|
|
110
|
+
server.headersTimeout = 10000;
|
|
111
|
+
await new Promise((resolve, reject) => {
|
|
112
|
+
server.once('error', reject);
|
|
113
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
114
|
+
});
|
|
115
|
+
port = server.address().port;
|
|
116
|
+
const timer = setTimeout(() => fail(new Error('Office connection timed out. Run setup again.')), timeoutMs);
|
|
117
|
+
const url = new URL('/npcs/connect', origin);
|
|
118
|
+
url.searchParams.set('port', String(port));
|
|
119
|
+
url.searchParams.set('state', state);
|
|
120
|
+
return {
|
|
121
|
+
url: url.href,
|
|
122
|
+
done,
|
|
123
|
+
close: () => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
server.close();
|
|
126
|
+
server.closeAllConnections();
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function saveBrowserConnection(value) {
|
|
131
|
+
const saved = (0, user_config_js_1.loadUserConfig)();
|
|
132
|
+
if (saved.workspace) {
|
|
133
|
+
const old = (0, node_1.loadProducerWorkspace)(saved.workspace);
|
|
134
|
+
if (old.apiUrl === value.workspace.apiUrl &&
|
|
135
|
+
old.encryption.teamId === value.workspace.encryption.teamId &&
|
|
136
|
+
(old.encryption.deploymentId !== value.workspace.encryption.deploymentId ||
|
|
137
|
+
old.encryption.trustedRoot !== value.workspace.encryption.trustedRoot))
|
|
138
|
+
throw new Error('The office encryption identity changed. Reconnect only after checking with your team owner.');
|
|
139
|
+
}
|
|
140
|
+
const directory = (0, node_path_1.join)((0, node_path_1.dirname)((0, user_config_js_1.userConfigPath)()), 'workspaces');
|
|
141
|
+
(0, node_fs_1.mkdirSync)(directory, { recursive: true, mode: 0o700 });
|
|
142
|
+
const name = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(value.workspace)).digest('hex');
|
|
143
|
+
const workspace = (0, node_path_1.join)(directory, name + '.json');
|
|
144
|
+
const temporary = workspace + '.' + (0, node_crypto_1.randomBytes)(8).toString('hex') + '.tmp';
|
|
145
|
+
(0, node_fs_1.writeFileSync)(temporary, JSON.stringify(value.workspace, null, 2) + '\n', {
|
|
146
|
+
mode: 0o600,
|
|
147
|
+
flag: 'wx',
|
|
148
|
+
});
|
|
149
|
+
(0, node_fs_1.renameSync)(temporary, workspace);
|
|
150
|
+
(0, user_config_js_1.saveUserConfig)({
|
|
151
|
+
workspace,
|
|
152
|
+
apiUrl: value.workspace.apiUrl,
|
|
153
|
+
apiKey: value.apiKey,
|
|
154
|
+
map: value.workspace.encryption.mapId,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function openConnectionBrowser(url) {
|
|
158
|
+
const command = process.platform === 'darwin'
|
|
159
|
+
? 'open'
|
|
160
|
+
: process.platform === 'win32'
|
|
161
|
+
? 'rundll32.exe'
|
|
162
|
+
: 'xdg-open';
|
|
163
|
+
const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
|
|
164
|
+
const child = (0, node_child_process_1.spawn)(command, args, { stdio: 'ignore', detached: true });
|
|
165
|
+
child.on('error', () => { }); // The printed URL also supports systems without a browser launcher.
|
|
166
|
+
child.unref();
|
|
167
|
+
}
|
|
168
|
+
async function connectCloudflareOffice(apiUrl) {
|
|
169
|
+
const origin = new URL(apiUrl).origin;
|
|
170
|
+
const response = await fetch(new URL('/.well-known/nocville.json', origin), {
|
|
171
|
+
redirect: 'error',
|
|
172
|
+
signal: AbortSignal.timeout(8000),
|
|
173
|
+
});
|
|
174
|
+
if (response.status === 404)
|
|
175
|
+
return false;
|
|
176
|
+
if (!response.ok)
|
|
177
|
+
throw new Error('The office is unavailable. Try setup again shortly.');
|
|
178
|
+
const info = (await response.json());
|
|
179
|
+
if (!info.encryptedIngress)
|
|
180
|
+
return false;
|
|
181
|
+
if (typeof info.deploymentId !== 'string' || !/^[A-Za-z0-9:_-]{1,128}$/.test(info.deploymentId))
|
|
182
|
+
throw new Error('Invalid office identity.');
|
|
183
|
+
const connection = await browserConnection(origin, info.deploymentId, saveBrowserConnection);
|
|
184
|
+
console.log('Connect this computer in your browser. Sign in, choose your office, and click Connect.');
|
|
185
|
+
console.log(connection.url);
|
|
186
|
+
openConnectionBrowser(connection.url);
|
|
187
|
+
try {
|
|
188
|
+
await connection.done;
|
|
189
|
+
console.log('✓ Connected. Private NPC updates will be encrypted on this computer.');
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
connection.close();
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=browser-connect.js.map
|
package/dist/cjs/core/config.js
CHANGED
|
@@ -6,6 +6,8 @@ exports.stringFlag = stringFlag;
|
|
|
6
6
|
exports.boolFlag = boolFlag;
|
|
7
7
|
exports.listFlag = listFlag;
|
|
8
8
|
exports.numberFlag = numberFlag;
|
|
9
|
+
const node_1 = require("@nocville/node");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
9
11
|
const user_config_js_1 = require("./user-config.js");
|
|
10
12
|
/**
|
|
11
13
|
* Simple hand-rolled CLI arg parser.
|
|
@@ -56,9 +58,16 @@ function parseArgs(argv) {
|
|
|
56
58
|
*/
|
|
57
59
|
function resolveConfig(flags, overrides) {
|
|
58
60
|
const saved = (0, user_config_js_1.loadUserConfig)();
|
|
61
|
+
const workspacePath = overrides?.workspace ||
|
|
62
|
+
stringFlag(flags, 'workspace') ||
|
|
63
|
+
process.env['NOCVILLE_WORKSPACE'] ||
|
|
64
|
+
saved.workspace;
|
|
65
|
+
const workspace = workspacePath ? (0, node_path_1.resolve)(workspacePath) : undefined;
|
|
66
|
+
const enrollment = workspace ? (0, node_1.loadProducerWorkspace)(workspace) : undefined;
|
|
59
67
|
const apiUrl = overrides?.apiUrl ||
|
|
60
68
|
stringFlag(flags, 'api-url') ||
|
|
61
69
|
process.env['NOCVILLE_API_URL'] ||
|
|
70
|
+
enrollment?.apiUrl ||
|
|
62
71
|
saved.apiUrl ||
|
|
63
72
|
'https://nocville.com';
|
|
64
73
|
const apiKey = overrides?.apiKey ||
|
|
@@ -76,10 +85,23 @@ function resolveConfig(flags, overrides) {
|
|
|
76
85
|
const map = overrides?.map ||
|
|
77
86
|
stringFlag(flags, 'map') ||
|
|
78
87
|
process.env['NOCVILLE_MAP'] ||
|
|
88
|
+
enrollment?.encryption.mapId ||
|
|
79
89
|
saved.map ||
|
|
80
90
|
undefined;
|
|
81
91
|
const name = overrides?.name || stringFlag(flags, 'name') || process.env['NOCVILLE_NAME'] || undefined;
|
|
82
|
-
|
|
92
|
+
if (enrollment && apiUrl.replace(/\/+$/, '') !== enrollment.apiUrl)
|
|
93
|
+
throw new Error('API origin does not match the verified workspace');
|
|
94
|
+
if (enrollment && map && map !== enrollment.encryption.mapId)
|
|
95
|
+
throw new Error('Encrypted producers use the map ID in their workspace settings');
|
|
96
|
+
return {
|
|
97
|
+
apiUrl,
|
|
98
|
+
apiKey,
|
|
99
|
+
serviceId,
|
|
100
|
+
debug,
|
|
101
|
+
map: enrollment?.encryption.mapId ?? map,
|
|
102
|
+
name,
|
|
103
|
+
...(workspace ? { workspace } : {}),
|
|
104
|
+
};
|
|
83
105
|
}
|
|
84
106
|
function stringFlag(flags, key) {
|
|
85
107
|
const val = flags[key];
|
|
@@ -25,6 +25,8 @@ function loadUserConfig(path = userConfigPath()) {
|
|
|
25
25
|
try {
|
|
26
26
|
const raw = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf-8'));
|
|
27
27
|
const out = {};
|
|
28
|
+
if (typeof raw['workspace'] === 'string' && raw['workspace'])
|
|
29
|
+
out.workspace = raw['workspace'];
|
|
28
30
|
if (typeof raw['apiUrl'] === 'string' && raw['apiUrl'])
|
|
29
31
|
out.apiUrl = raw['apiUrl'];
|
|
30
32
|
if (typeof raw['apiKey'] === 'string' && raw['apiKey'])
|
package/dist/cjs/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runDoctor = runDoctor;
|
|
4
4
|
exports.hasCodexHookWatcherOverlap = hasCodexHookWatcherOverlap;
|
|
5
|
+
const node_1 = require("@nocville/node");
|
|
5
6
|
const node_crypto_1 = require("node:crypto");
|
|
6
7
|
const node_fs_1 = require("node:fs");
|
|
7
8
|
const liveness_js_1 = require("./npcs/agent-common/liveness.js");
|
|
@@ -43,6 +44,24 @@ async function runDoctor(config) {
|
|
|
43
44
|
if (!hasKey) {
|
|
44
45
|
console.log(' [SKIP] POST /api/v1/status — no --api-key / NOCVILLE_API_KEY set');
|
|
45
46
|
}
|
|
47
|
+
else if (config.workspace || config.apiKey.includes('.team_')) {
|
|
48
|
+
// Exercise the locally encrypted path; never send a plaintext probe to a v2 workspace.
|
|
49
|
+
try {
|
|
50
|
+
const client = new node_1.NocvilleClient({
|
|
51
|
+
workspace: config.workspace,
|
|
52
|
+
apiUrl: config.apiUrl,
|
|
53
|
+
apiKey: config.apiKey,
|
|
54
|
+
serviceId: 'nocville-doctor',
|
|
55
|
+
retries: 0,
|
|
56
|
+
});
|
|
57
|
+
const result = await client.pushStatus({ status: 'unknown', offline: true });
|
|
58
|
+
pushOk = result.success;
|
|
59
|
+
console.log(` [${pushOk ? 'OK' : 'FAIL'}] Encrypted producer: ${result.error ?? 'accepted'}`);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.log(` [FAIL] Encrypted producer: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
46
65
|
else {
|
|
47
66
|
const start = Date.now();
|
|
48
67
|
const t = withTimeout(5000);
|
|
@@ -71,24 +71,57 @@ function ensureStableCliPath(cliPath, _force = true) {
|
|
|
71
71
|
const pngRoot = findPackageRoot((0, node_path_1.dirname)(req.resolve('pngjs')), 'pngjs');
|
|
72
72
|
if (!pngRoot)
|
|
73
73
|
throw new Error('Cannot install hook runtime: pngjs is missing');
|
|
74
|
+
// Resolve the complete production dependency closure before copying. Encryption
|
|
75
|
+
// introduced nested SDK dependencies that the old two-package snapshot omitted.
|
|
76
|
+
const dependencies = new Map();
|
|
77
|
+
const collect = (root) => {
|
|
78
|
+
const manifest = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(root, 'package.json'), 'utf8'));
|
|
79
|
+
const previous = dependencies.get(manifest.name);
|
|
80
|
+
if (previous) {
|
|
81
|
+
if (readVersion(previous) !== manifest.version)
|
|
82
|
+
throw new Error(`Cannot install hook runtime: conflicting versions of ${manifest.name}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
dependencies.set(manifest.name, root);
|
|
86
|
+
const resolveDependency = (0, node_module_1.createRequire)((0, node_path_1.join)(root, 'package.json'));
|
|
87
|
+
for (const name of Object.keys(manifest.dependencies ?? {}).sort()) {
|
|
88
|
+
const dependency = findPackageRoot((0, node_path_1.dirname)(resolveDependency.resolve(name)), name);
|
|
89
|
+
if (!dependency)
|
|
90
|
+
throw new Error(`Cannot install hook runtime: ${name} is missing`);
|
|
91
|
+
collect(dependency);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
collect(sdkRoot);
|
|
95
|
+
collect(pngRoot);
|
|
96
|
+
const files = (root, prefix = '') => (0, node_fs_1.readdirSync)((0, node_path_1.join)(root, prefix), { withFileTypes: true })
|
|
97
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
98
|
+
.flatMap((entry) => {
|
|
99
|
+
if (entry.name === 'node_modules' ||
|
|
100
|
+
entry.name.startsWith('.') ||
|
|
101
|
+
entry.name.endsWith('.map'))
|
|
102
|
+
return [];
|
|
103
|
+
const relative = (0, node_path_1.join)(prefix, entry.name);
|
|
104
|
+
return (0, node_fs_1.statSync)((0, node_path_1.join)(root, relative)).isDirectory() ? files(root, relative) : [relative];
|
|
105
|
+
});
|
|
106
|
+
const dependencyFiles = [...dependencies].map(([name, root]) => ({
|
|
107
|
+
name,
|
|
108
|
+
root,
|
|
109
|
+
files: files(root),
|
|
110
|
+
}));
|
|
74
111
|
const version = readVersion(pkgRoot);
|
|
75
112
|
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
113
|
+
hash.update((0, node_fs_1.readFileSync)((0, node_path_1.join)(pkgRoot, 'package.json')));
|
|
114
|
+
for (const file of files((0, node_path_1.join)(pkgRoot, 'dist'))) {
|
|
115
|
+
hash.update(file);
|
|
116
|
+
hash.update((0, node_fs_1.readFileSync)((0, node_path_1.join)(pkgRoot, 'dist', file)));
|
|
117
|
+
}
|
|
118
|
+
for (const dependency of dependencyFiles) {
|
|
119
|
+
hash.update(dependency.name);
|
|
120
|
+
for (const file of dependency.files) {
|
|
121
|
+
hash.update(file);
|
|
122
|
+
hash.update((0, node_fs_1.readFileSync)((0, node_path_1.join)(dependency.root, file)));
|
|
84
123
|
}
|
|
85
|
-
};
|
|
86
|
-
for (const root of [pkgRoot, sdkRoot]) {
|
|
87
|
-
hash.update((0, node_fs_1.readFileSync)((0, node_path_1.join)(root, 'package.json')));
|
|
88
|
-
hashTree((0, node_path_1.join)(root, 'dist'));
|
|
89
124
|
}
|
|
90
|
-
hash.update((0, node_fs_1.readFileSync)((0, node_path_1.join)(pngRoot, 'package.json')));
|
|
91
|
-
hashTree((0, node_path_1.join)(pngRoot, 'lib'));
|
|
92
125
|
// Immutable snapshots also support multiple development builds of one version.
|
|
93
126
|
// Never remove files a running watchdog or the current pointer may still use.
|
|
94
127
|
const dest = (0, node_path_1.join)((0, node_path_1.dirname)((0, user_config_js_1.userConfigPath)()), 'runtime', `${version}-${hash.digest('hex').slice(0, 16)}`);
|
|
@@ -99,14 +132,13 @@ function ensureStableCliPath(cliPath, _force = true) {
|
|
|
99
132
|
(0, node_fs_1.mkdirSync)(stage, { recursive: true, mode: 0o700 });
|
|
100
133
|
(0, node_fs_1.cpSync)((0, node_path_1.join)(pkgRoot, 'package.json'), (0, node_path_1.join)(stage, 'package.json'));
|
|
101
134
|
(0, node_fs_1.cpSync)((0, node_path_1.join)(pkgRoot, 'dist'), (0, node_path_1.join)(stage, 'dist'), { recursive: true });
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
(0, node_fs_1.cpSync)((0, node_path_1.join)(pngRoot, 'lib'), (0, node_path_1.join)(pngDest, 'lib'), { recursive: true });
|
|
135
|
+
for (const dependency of dependencyFiles) {
|
|
136
|
+
const target = (0, node_path_1.join)(stage, 'node_modules', dependency.name);
|
|
137
|
+
for (const file of dependency.files) {
|
|
138
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)((0, node_path_1.join)(target, file)), { recursive: true });
|
|
139
|
+
(0, node_fs_1.cpSync)((0, node_path_1.join)(dependency.root, file), (0, node_path_1.join)(target, file), { dereference: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
110
142
|
try {
|
|
111
143
|
(0, node_fs_1.renameSync)(stage, dest);
|
|
112
144
|
}
|
|
@@ -263,6 +263,7 @@ function buildAgentPayload(adapter, state, config) {
|
|
|
263
263
|
}
|
|
264
264
|
async function defaultPush(config, serviceId, payload, timeoutMs) {
|
|
265
265
|
const client = new node_1.NocvilleClient({
|
|
266
|
+
workspace: config.workspace,
|
|
266
267
|
apiUrl: config.apiUrl,
|
|
267
268
|
apiKey: config.apiKey,
|
|
268
269
|
serviceId,
|
|
@@ -381,6 +382,8 @@ function spawnDetachedAgent(adapter, config, args, cliPath = process.argv[1]) {
|
|
|
381
382
|
if (!cliPath)
|
|
382
383
|
return undefined;
|
|
383
384
|
const env = { ...process.env };
|
|
385
|
+
if (config.workspace)
|
|
386
|
+
env['NOCVILLE_WORKSPACE'] = config.workspace;
|
|
384
387
|
env['NOCVILLE_API_URL'] = config.apiUrl;
|
|
385
388
|
env['NOCVILLE_API_KEY'] = config.apiKey;
|
|
386
389
|
if (config.map)
|
package/dist/cli.js
CHANGED
|
@@ -12,9 +12,9 @@ const USAGE = `
|
|
|
12
12
|
nocville-npc <command> [options]
|
|
13
13
|
|
|
14
14
|
Quick start (AI coworkers):
|
|
15
|
-
nocville-npc setup
|
|
15
|
+
nocville-npc setup Connect your office in the browser,
|
|
16
16
|
then hook up installed coding agents
|
|
17
|
-
|
|
17
|
+
nocville-npc setup --reconnect Choose an office and renew this connection
|
|
18
18
|
nocville-npc setup --api-key=<agent key> Same, non-interactive
|
|
19
19
|
nocville-npc uninstall Remove all Nocville hooks
|
|
20
20
|
|
|
@@ -61,6 +61,7 @@ Monitoring NPC types:
|
|
|
61
61
|
web-page-watcher Render a watched web page as a pixel-art TV
|
|
62
62
|
|
|
63
63
|
Common Options:
|
|
64
|
+
--workspace=<file> Verified producer workspace settings from team privacy settings.
|
|
64
65
|
--api-key=<key> Team API key (or NOCVILLE_API_KEY env)
|
|
65
66
|
--api-url=<url> Nocville API URL (or NOCVILLE_API_URL env).
|
|
66
67
|
Default: https://nocville.com. For local dev use
|
|
@@ -150,22 +151,27 @@ async function main() {
|
|
|
150
151
|
console.log(USAGE);
|
|
151
152
|
process.exit(1);
|
|
152
153
|
}
|
|
153
|
-
|
|
154
|
+
let config = resolveConfig(parsed.flags);
|
|
154
155
|
const logger = new Logger('cli', config.debug);
|
|
155
156
|
// Setup-style commands resolve the key: --api-key > env > saved > hidden prompt.
|
|
156
157
|
const isAgentSetup = ['claude-code', 'codex', 'gemini', 'cursor'].includes(parsed.command) &&
|
|
157
158
|
parsed.subcommand === 'setup' &&
|
|
158
159
|
!parsed.flags['uninstall'];
|
|
159
160
|
if (isAgentSetup || parsed.command === 'setup') {
|
|
160
|
-
if (
|
|
161
|
-
const {
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
161
|
+
if (!config.workspace || !config.apiKey.includes('.team_') || parsed.flags['reconnect']) {
|
|
162
|
+
const { connectCloudflareOffice } = await import('./core/browser-connect.js');
|
|
163
|
+
if (await connectCloudflareOffice(config.apiUrl)) {
|
|
164
|
+
const { loadUserConfig } = await import('./core/user-config.js');
|
|
165
|
+
const saved = loadUserConfig();
|
|
166
|
+
for (const [flag, value] of Object.entries({
|
|
167
|
+
'api-key': saved.apiKey,
|
|
168
|
+
'api-url': saved.apiUrl,
|
|
169
|
+
map: saved.map,
|
|
170
|
+
workspace: saved.workspace,
|
|
171
|
+
}))
|
|
172
|
+
if (value)
|
|
173
|
+
parsed.flags[flag] = value;
|
|
174
|
+
config = resolveConfig(parsed.flags);
|
|
169
175
|
}
|
|
170
176
|
}
|
|
171
177
|
const key = await resolveSetupKey(parsed.flags);
|
|
@@ -173,6 +179,16 @@ async function main() {
|
|
|
173
179
|
return;
|
|
174
180
|
if (key !== undefined)
|
|
175
181
|
parsed.flags['api-key'] = key;
|
|
182
|
+
config = resolveConfig(parsed.flags);
|
|
183
|
+
if (config.workspace) {
|
|
184
|
+
const { saveUserConfig } = await import('./core/user-config.js');
|
|
185
|
+
saveUserConfig({
|
|
186
|
+
workspace: config.workspace,
|
|
187
|
+
apiUrl: config.apiUrl,
|
|
188
|
+
apiKey: config.apiKey,
|
|
189
|
+
map: config.map,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
176
192
|
}
|
|
177
193
|
// AI-agent integrations have their own config handling (hooks read the saved config).
|
|
178
194
|
if (parsed.command === 'claude-code') {
|
|
@@ -255,6 +271,10 @@ async function main() {
|
|
|
255
271
|
return;
|
|
256
272
|
}
|
|
257
273
|
// All other NPC types require api-url and token
|
|
274
|
+
if (!config.workspace &&
|
|
275
|
+
(config.apiKey.includes('.team_') ||
|
|
276
|
+
config.apiUrl.replace(/\/+$/, '') === 'https://nocville.com'))
|
|
277
|
+
throw new Error('Connect this computer to your office first: run npx @nocville/npc@latest setup. Existing legacy keys cannot publish encrypted NPC updates.');
|
|
258
278
|
if (!config.apiKey) {
|
|
259
279
|
console.error('Error: no API key — run `nocville-npc init --api-key=<key>`, or pass --api-key / NOCVILLE_API_KEY');
|
|
260
280
|
process.exit(1);
|
|
@@ -477,7 +497,20 @@ async function runInit(flags, config) {
|
|
|
477
497
|
let apiKey = stringFlag(flags, 'api-key');
|
|
478
498
|
const apiUrl = stringFlag(flags, 'api-url');
|
|
479
499
|
const map = stringFlag(flags, 'map');
|
|
480
|
-
|
|
500
|
+
const workspace = config.workspace;
|
|
501
|
+
if (workspace)
|
|
502
|
+
apiKey = apiKey || config.apiKey;
|
|
503
|
+
if (workspace && !apiKey) {
|
|
504
|
+
const { resolveApiKey } = await import('./core/api-key.js');
|
|
505
|
+
const resolved = await resolveApiKey({ flags });
|
|
506
|
+
if (!resolved.ok) {
|
|
507
|
+
console.error(resolved.error);
|
|
508
|
+
process.exitCode = 1;
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
apiKey = resolved.key;
|
|
512
|
+
}
|
|
513
|
+
if (!apiKey && !apiUrl && !map && !workspace) {
|
|
481
514
|
const saved = loadUserConfig();
|
|
482
515
|
if (saved.apiKey && !process.env['NOCVILLE_API_KEY']?.trim()) {
|
|
483
516
|
console.log('Usage: nocville-npc init --api-key=<key> [--api-url=<url>] [--map=<name>]');
|
|
@@ -493,7 +526,12 @@ async function runInit(flags, config) {
|
|
|
493
526
|
}
|
|
494
527
|
apiKey = resolved.key;
|
|
495
528
|
}
|
|
496
|
-
const path = saveUserConfig({
|
|
529
|
+
const path = saveUserConfig({
|
|
530
|
+
apiKey,
|
|
531
|
+
apiUrl: workspace ? config.apiUrl : apiUrl,
|
|
532
|
+
map: workspace ? config.map : map,
|
|
533
|
+
workspace,
|
|
534
|
+
});
|
|
497
535
|
const saved = loadUserConfig();
|
|
498
536
|
console.log(`✓ Saved ${path} (mode 0600)`);
|
|
499
537
|
console.log(` API: ${saved.apiUrl ?? 'https://nocville.com'} key: ${maskKey(saved.apiKey)}`);
|
|
@@ -585,8 +623,7 @@ async function runSetupAll(flags) {
|
|
|
585
623
|
console.log('· Cursor not found (~/.cursor missing) — skipped');
|
|
586
624
|
}
|
|
587
625
|
if (!any) {
|
|
588
|
-
console.
|
|
589
|
-
process.exitCode = 1;
|
|
626
|
+
console.log('No supported coding agents found. You can still run NPC commands on this computer.');
|
|
590
627
|
}
|
|
591
628
|
}
|
|
592
629
|
main().catch((err) => {
|
package/dist/core/base-npc.js
CHANGED
|
@@ -14,6 +14,7 @@ export class BaseNPC {
|
|
|
14
14
|
// We can't access `this.name` here since it's abstract, so use a placeholder
|
|
15
15
|
this.logger = new Logger('npc', config.debug);
|
|
16
16
|
this.client = new NocvilleClient({
|
|
17
|
+
workspace: config.workspace,
|
|
17
18
|
apiUrl: config.apiUrl,
|
|
18
19
|
apiKey: config.apiKey,
|
|
19
20
|
serviceId: config.serviceId,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ProducerWorkspace } from '@nocville/node';
|
|
2
|
+
export interface BrowserConnection {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
workspace: ProducerWorkspace;
|
|
5
|
+
}
|
|
6
|
+
export declare function validateConnection(input: unknown, origin: string, deploymentId: string): BrowserConnection;
|
|
7
|
+
/** A one-use loopback receiver. Credentials never appear in a URL or terminal output. */
|
|
8
|
+
export declare function browserConnection(origin: string, deploymentId: string, save: (value: BrowserConnection) => void | Promise<void>, timeoutMs?: number): Promise<{
|
|
9
|
+
url: string;
|
|
10
|
+
done: Promise<void>;
|
|
11
|
+
close: () => void;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function saveBrowserConnection(value: BrowserConnection): void;
|
|
14
|
+
export declare function openConnectionBrowser(url: string): void;
|
|
15
|
+
export declare function connectCloudflareOffice(apiUrl: string): Promise<boolean>;
|
|
16
|
+
//# sourceMappingURL=browser-connect.d.ts.map
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { loadProducerWorkspace } from '@nocville/node';
|
|
7
|
+
import { loadUserConfig, saveUserConfig, userConfigPath } from './user-config.js';
|
|
8
|
+
export function validateConnection(input, origin, deploymentId) {
|
|
9
|
+
const value = input;
|
|
10
|
+
const e = value?.workspace?.encryption;
|
|
11
|
+
if (typeof value?.apiKey !== 'string' ||
|
|
12
|
+
!/^nvk_[a-f0-9]{64}\.team_[A-Za-z0-9_-]{1,64}$/.test(value.apiKey) ||
|
|
13
|
+
value.workspace?.apiUrl !== origin ||
|
|
14
|
+
e?.deploymentId !== deploymentId ||
|
|
15
|
+
!/^team_[A-Za-z0-9_-]{1,64}$/.test(e?.teamId ?? '') ||
|
|
16
|
+
!/^map_[A-Za-z0-9_-]{1,128}$/.test(e?.mapId ?? '') ||
|
|
17
|
+
!/^[A-Za-z0-9_-]{43}$/.test(e?.trustedRoot ?? '') ||
|
|
18
|
+
!value.apiKey.endsWith('.' + e.teamId) ||
|
|
19
|
+
e.profileDirectory !== undefined)
|
|
20
|
+
throw new Error('Invalid office connection. Start setup again.');
|
|
21
|
+
return {
|
|
22
|
+
apiKey: value.apiKey,
|
|
23
|
+
workspace: {
|
|
24
|
+
apiUrl: origin,
|
|
25
|
+
encryption: {
|
|
26
|
+
deploymentId,
|
|
27
|
+
teamId: e.teamId,
|
|
28
|
+
mapId: e.mapId,
|
|
29
|
+
trustedRoot: e.trustedRoot,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** A one-use loopback receiver. Credentials never appear in a URL or terminal output. */
|
|
35
|
+
export async function browserConnection(origin, deploymentId, save, timeoutMs = 300000) {
|
|
36
|
+
const target = new URL(origin);
|
|
37
|
+
if (target.origin !== origin ||
|
|
38
|
+
target.username ||
|
|
39
|
+
target.password ||
|
|
40
|
+
(target.protocol !== 'https:' &&
|
|
41
|
+
!(target.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(target.hostname))))
|
|
42
|
+
throw new Error('Use an HTTPS office address.');
|
|
43
|
+
const state = randomBytes(32).toString('hex');
|
|
44
|
+
let finish, fail;
|
|
45
|
+
const done = new Promise((resolve, reject) => {
|
|
46
|
+
finish = resolve;
|
|
47
|
+
fail = reject;
|
|
48
|
+
});
|
|
49
|
+
// A timeout may happen while the browser launcher is still opening.
|
|
50
|
+
void done.catch(() => { });
|
|
51
|
+
let consumed = false, port = 0;
|
|
52
|
+
const server = createServer(async (request, response) => {
|
|
53
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
54
|
+
if (request.headers.host !== `127.0.0.1:${port}` ||
|
|
55
|
+
request.url !== '/complete' ||
|
|
56
|
+
request.headers.origin !== origin) {
|
|
57
|
+
response.writeHead(403).end();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
response.setHeader('Access-Control-Allow-Origin', origin);
|
|
61
|
+
response.setHeader('Vary', 'Origin');
|
|
62
|
+
if (request.method === 'OPTIONS') {
|
|
63
|
+
response.setHeader('Access-Control-Allow-Methods', 'POST');
|
|
64
|
+
response.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
65
|
+
response.setHeader('Access-Control-Allow-Private-Network', 'true');
|
|
66
|
+
response.writeHead(204).end();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (request.method !== 'POST' ||
|
|
70
|
+
request.headers['content-type']?.split(';')[0] !== 'application/json') {
|
|
71
|
+
response.writeHead(415).end();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
let size = 0;
|
|
76
|
+
const chunks = [];
|
|
77
|
+
for await (const chunk of request) {
|
|
78
|
+
size += chunk.length;
|
|
79
|
+
if (size > 16384) {
|
|
80
|
+
response.writeHead(413).end();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
chunks.push(Buffer.from(chunk));
|
|
84
|
+
}
|
|
85
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
86
|
+
if (body.state !== state || consumed) {
|
|
87
|
+
response.writeHead(403).end();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const value = validateConnection(body, origin, deploymentId);
|
|
91
|
+
consumed = true;
|
|
92
|
+
await save(value);
|
|
93
|
+
response.once('finish', finish);
|
|
94
|
+
response.writeHead(200, { 'Content-Type': 'application/json' }).end('{"connected":true}');
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
response.writeHead(400).end('Could not save this office connection. Start setup again.');
|
|
98
|
+
if (consumed)
|
|
99
|
+
fail(new Error('Could not save the office connection. Start setup again.'));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
server.requestTimeout = 15000;
|
|
103
|
+
server.headersTimeout = 10000;
|
|
104
|
+
await new Promise((resolve, reject) => {
|
|
105
|
+
server.once('error', reject);
|
|
106
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
107
|
+
});
|
|
108
|
+
port = server.address().port;
|
|
109
|
+
const timer = setTimeout(() => fail(new Error('Office connection timed out. Run setup again.')), timeoutMs);
|
|
110
|
+
const url = new URL('/npcs/connect', origin);
|
|
111
|
+
url.searchParams.set('port', String(port));
|
|
112
|
+
url.searchParams.set('state', state);
|
|
113
|
+
return {
|
|
114
|
+
url: url.href,
|
|
115
|
+
done,
|
|
116
|
+
close: () => {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
server.close();
|
|
119
|
+
server.closeAllConnections();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function saveBrowserConnection(value) {
|
|
124
|
+
const saved = loadUserConfig();
|
|
125
|
+
if (saved.workspace) {
|
|
126
|
+
const old = loadProducerWorkspace(saved.workspace);
|
|
127
|
+
if (old.apiUrl === value.workspace.apiUrl &&
|
|
128
|
+
old.encryption.teamId === value.workspace.encryption.teamId &&
|
|
129
|
+
(old.encryption.deploymentId !== value.workspace.encryption.deploymentId ||
|
|
130
|
+
old.encryption.trustedRoot !== value.workspace.encryption.trustedRoot))
|
|
131
|
+
throw new Error('The office encryption identity changed. Reconnect only after checking with your team owner.');
|
|
132
|
+
}
|
|
133
|
+
const directory = join(dirname(userConfigPath()), 'workspaces');
|
|
134
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
135
|
+
const name = createHash('sha256').update(JSON.stringify(value.workspace)).digest('hex');
|
|
136
|
+
const workspace = join(directory, name + '.json');
|
|
137
|
+
const temporary = workspace + '.' + randomBytes(8).toString('hex') + '.tmp';
|
|
138
|
+
writeFileSync(temporary, JSON.stringify(value.workspace, null, 2) + '\n', {
|
|
139
|
+
mode: 0o600,
|
|
140
|
+
flag: 'wx',
|
|
141
|
+
});
|
|
142
|
+
renameSync(temporary, workspace);
|
|
143
|
+
saveUserConfig({
|
|
144
|
+
workspace,
|
|
145
|
+
apiUrl: value.workspace.apiUrl,
|
|
146
|
+
apiKey: value.apiKey,
|
|
147
|
+
map: value.workspace.encryption.mapId,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
export function openConnectionBrowser(url) {
|
|
151
|
+
const command = process.platform === 'darwin'
|
|
152
|
+
? 'open'
|
|
153
|
+
: process.platform === 'win32'
|
|
154
|
+
? 'rundll32.exe'
|
|
155
|
+
: 'xdg-open';
|
|
156
|
+
const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
|
|
157
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true });
|
|
158
|
+
child.on('error', () => { }); // The printed URL also supports systems without a browser launcher.
|
|
159
|
+
child.unref();
|
|
160
|
+
}
|
|
161
|
+
export async function connectCloudflareOffice(apiUrl) {
|
|
162
|
+
const origin = new URL(apiUrl).origin;
|
|
163
|
+
const response = await fetch(new URL('/.well-known/nocville.json', origin), {
|
|
164
|
+
redirect: 'error',
|
|
165
|
+
signal: AbortSignal.timeout(8000),
|
|
166
|
+
});
|
|
167
|
+
if (response.status === 404)
|
|
168
|
+
return false;
|
|
169
|
+
if (!response.ok)
|
|
170
|
+
throw new Error('The office is unavailable. Try setup again shortly.');
|
|
171
|
+
const info = (await response.json());
|
|
172
|
+
if (!info.encryptedIngress)
|
|
173
|
+
return false;
|
|
174
|
+
if (typeof info.deploymentId !== 'string' || !/^[A-Za-z0-9:_-]{1,128}$/.test(info.deploymentId))
|
|
175
|
+
throw new Error('Invalid office identity.');
|
|
176
|
+
const connection = await browserConnection(origin, info.deploymentId, saveBrowserConnection);
|
|
177
|
+
console.log('Connect this computer in your browser. Sign in, choose your office, and click Connect.');
|
|
178
|
+
console.log(connection.url);
|
|
179
|
+
openConnectionBrowser(connection.url);
|
|
180
|
+
try {
|
|
181
|
+
await connection.done;
|
|
182
|
+
console.log('✓ Connected. Private NPC updates will be encrypted on this computer.');
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
connection.close();
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=browser-connect.js.map
|
package/dist/core/config.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { loadProducerWorkspace } from '@nocville/node';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
1
3
|
import { loadUserConfig } from './user-config.js';
|
|
2
4
|
/**
|
|
3
5
|
* Simple hand-rolled CLI arg parser.
|
|
@@ -48,9 +50,16 @@ export function parseArgs(argv) {
|
|
|
48
50
|
*/
|
|
49
51
|
export function resolveConfig(flags, overrides) {
|
|
50
52
|
const saved = loadUserConfig();
|
|
53
|
+
const workspacePath = overrides?.workspace ||
|
|
54
|
+
stringFlag(flags, 'workspace') ||
|
|
55
|
+
process.env['NOCVILLE_WORKSPACE'] ||
|
|
56
|
+
saved.workspace;
|
|
57
|
+
const workspace = workspacePath ? resolve(workspacePath) : undefined;
|
|
58
|
+
const enrollment = workspace ? loadProducerWorkspace(workspace) : undefined;
|
|
51
59
|
const apiUrl = overrides?.apiUrl ||
|
|
52
60
|
stringFlag(flags, 'api-url') ||
|
|
53
61
|
process.env['NOCVILLE_API_URL'] ||
|
|
62
|
+
enrollment?.apiUrl ||
|
|
54
63
|
saved.apiUrl ||
|
|
55
64
|
'https://nocville.com';
|
|
56
65
|
const apiKey = overrides?.apiKey ||
|
|
@@ -68,10 +77,23 @@ export function resolveConfig(flags, overrides) {
|
|
|
68
77
|
const map = overrides?.map ||
|
|
69
78
|
stringFlag(flags, 'map') ||
|
|
70
79
|
process.env['NOCVILLE_MAP'] ||
|
|
80
|
+
enrollment?.encryption.mapId ||
|
|
71
81
|
saved.map ||
|
|
72
82
|
undefined;
|
|
73
83
|
const name = overrides?.name || stringFlag(flags, 'name') || process.env['NOCVILLE_NAME'] || undefined;
|
|
74
|
-
|
|
84
|
+
if (enrollment && apiUrl.replace(/\/+$/, '') !== enrollment.apiUrl)
|
|
85
|
+
throw new Error('API origin does not match the verified workspace');
|
|
86
|
+
if (enrollment && map && map !== enrollment.encryption.mapId)
|
|
87
|
+
throw new Error('Encrypted producers use the map ID in their workspace settings');
|
|
88
|
+
return {
|
|
89
|
+
apiUrl,
|
|
90
|
+
apiKey,
|
|
91
|
+
serviceId,
|
|
92
|
+
debug,
|
|
93
|
+
map: enrollment?.encryption.mapId ?? map,
|
|
94
|
+
name,
|
|
95
|
+
...(workspace ? { workspace } : {}),
|
|
96
|
+
};
|
|
75
97
|
}
|
|
76
98
|
export function stringFlag(flags, key) {
|
|
77
99
|
const val = flags[key];
|
package/dist/core/types.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export type Archetype = 'service' | 'database' | 'trading' | 'ai-agent' | 'pipel
|
|
|
6
6
|
/** A companion declaration is the SDK's CompanionSpec. */
|
|
7
7
|
export type CompanionDecl = CompanionSpec;
|
|
8
8
|
export interface NpcConfig {
|
|
9
|
+
/** Local verified workspace enrollment bundle. */
|
|
10
|
+
workspace?: string;
|
|
9
11
|
apiUrl: string;
|
|
10
12
|
apiKey: string;
|
|
11
13
|
serviceId: string;
|
package/dist/core/user-config.js
CHANGED
|
@@ -18,6 +18,8 @@ export function loadUserConfig(path = userConfigPath()) {
|
|
|
18
18
|
try {
|
|
19
19
|
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
20
20
|
const out = {};
|
|
21
|
+
if (typeof raw['workspace'] === 'string' && raw['workspace'])
|
|
22
|
+
out.workspace = raw['workspace'];
|
|
21
23
|
if (typeof raw['apiUrl'] === 'string' && raw['apiUrl'])
|
|
22
24
|
out.apiUrl = raw['apiUrl'];
|
|
23
25
|
if (typeof raw['apiKey'] === 'string' && raw['apiKey'])
|
package/dist/doctor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NocvilleClient } from '@nocville/node';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { readdirSync } from 'node:fs';
|
|
3
4
|
import { isAgentAlive } from './npcs/agent-common/liveness.js';
|
|
@@ -39,6 +40,24 @@ export async function runDoctor(config) {
|
|
|
39
40
|
if (!hasKey) {
|
|
40
41
|
console.log(' [SKIP] POST /api/v1/status — no --api-key / NOCVILLE_API_KEY set');
|
|
41
42
|
}
|
|
43
|
+
else if (config.workspace || config.apiKey.includes('.team_')) {
|
|
44
|
+
// Exercise the locally encrypted path; never send a plaintext probe to a v2 workspace.
|
|
45
|
+
try {
|
|
46
|
+
const client = new NocvilleClient({
|
|
47
|
+
workspace: config.workspace,
|
|
48
|
+
apiUrl: config.apiUrl,
|
|
49
|
+
apiKey: config.apiKey,
|
|
50
|
+
serviceId: 'nocville-doctor',
|
|
51
|
+
retries: 0,
|
|
52
|
+
});
|
|
53
|
+
const result = await client.pushStatus({ status: 'unknown', offline: true });
|
|
54
|
+
pushOk = result.success;
|
|
55
|
+
console.log(` [${pushOk ? 'OK' : 'FAIL'}] Encrypted producer: ${result.error ?? 'accepted'}`);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.log(` [FAIL] Encrypted producer: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
42
61
|
else {
|
|
43
62
|
const start = Date.now();
|
|
44
63
|
const t = withTimeout(5000);
|
|
@@ -66,24 +66,57 @@ export function ensureStableCliPath(cliPath, _force = true) {
|
|
|
66
66
|
const pngRoot = findPackageRoot(dirname(req.resolve('pngjs')), 'pngjs');
|
|
67
67
|
if (!pngRoot)
|
|
68
68
|
throw new Error('Cannot install hook runtime: pngjs is missing');
|
|
69
|
+
// Resolve the complete production dependency closure before copying. Encryption
|
|
70
|
+
// introduced nested SDK dependencies that the old two-package snapshot omitted.
|
|
71
|
+
const dependencies = new Map();
|
|
72
|
+
const collect = (root) => {
|
|
73
|
+
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
74
|
+
const previous = dependencies.get(manifest.name);
|
|
75
|
+
if (previous) {
|
|
76
|
+
if (readVersion(previous) !== manifest.version)
|
|
77
|
+
throw new Error(`Cannot install hook runtime: conflicting versions of ${manifest.name}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
dependencies.set(manifest.name, root);
|
|
81
|
+
const resolveDependency = createRequire(join(root, 'package.json'));
|
|
82
|
+
for (const name of Object.keys(manifest.dependencies ?? {}).sort()) {
|
|
83
|
+
const dependency = findPackageRoot(dirname(resolveDependency.resolve(name)), name);
|
|
84
|
+
if (!dependency)
|
|
85
|
+
throw new Error(`Cannot install hook runtime: ${name} is missing`);
|
|
86
|
+
collect(dependency);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
collect(sdkRoot);
|
|
90
|
+
collect(pngRoot);
|
|
91
|
+
const files = (root, prefix = '') => readdirSync(join(root, prefix), { withFileTypes: true })
|
|
92
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
93
|
+
.flatMap((entry) => {
|
|
94
|
+
if (entry.name === 'node_modules' ||
|
|
95
|
+
entry.name.startsWith('.') ||
|
|
96
|
+
entry.name.endsWith('.map'))
|
|
97
|
+
return [];
|
|
98
|
+
const relative = join(prefix, entry.name);
|
|
99
|
+
return statSync(join(root, relative)).isDirectory() ? files(root, relative) : [relative];
|
|
100
|
+
});
|
|
101
|
+
const dependencyFiles = [...dependencies].map(([name, root]) => ({
|
|
102
|
+
name,
|
|
103
|
+
root,
|
|
104
|
+
files: files(root),
|
|
105
|
+
}));
|
|
69
106
|
const version = readVersion(pkgRoot);
|
|
70
107
|
const hash = createHash('sha256');
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
hash.update(readFileSync(join(pkgRoot, 'package.json')));
|
|
109
|
+
for (const file of files(join(pkgRoot, 'dist'))) {
|
|
110
|
+
hash.update(file);
|
|
111
|
+
hash.update(readFileSync(join(pkgRoot, 'dist', file)));
|
|
112
|
+
}
|
|
113
|
+
for (const dependency of dependencyFiles) {
|
|
114
|
+
hash.update(dependency.name);
|
|
115
|
+
for (const file of dependency.files) {
|
|
116
|
+
hash.update(file);
|
|
117
|
+
hash.update(readFileSync(join(dependency.root, file)));
|
|
79
118
|
}
|
|
80
|
-
};
|
|
81
|
-
for (const root of [pkgRoot, sdkRoot]) {
|
|
82
|
-
hash.update(readFileSync(join(root, 'package.json')));
|
|
83
|
-
hashTree(join(root, 'dist'));
|
|
84
119
|
}
|
|
85
|
-
hash.update(readFileSync(join(pngRoot, 'package.json')));
|
|
86
|
-
hashTree(join(pngRoot, 'lib'));
|
|
87
120
|
// Immutable snapshots also support multiple development builds of one version.
|
|
88
121
|
// Never remove files a running watchdog or the current pointer may still use.
|
|
89
122
|
const dest = join(dirname(userConfigPath()), 'runtime', `${version}-${hash.digest('hex').slice(0, 16)}`);
|
|
@@ -94,14 +127,13 @@ export function ensureStableCliPath(cliPath, _force = true) {
|
|
|
94
127
|
mkdirSync(stage, { recursive: true, mode: 0o700 });
|
|
95
128
|
cpSync(join(pkgRoot, 'package.json'), join(stage, 'package.json'));
|
|
96
129
|
cpSync(join(pkgRoot, 'dist'), join(stage, 'dist'), { recursive: true });
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
cpSync(join(pngRoot, 'lib'), join(pngDest, 'lib'), { recursive: true });
|
|
130
|
+
for (const dependency of dependencyFiles) {
|
|
131
|
+
const target = join(stage, 'node_modules', dependency.name);
|
|
132
|
+
for (const file of dependency.files) {
|
|
133
|
+
mkdirSync(dirname(join(target, file)), { recursive: true });
|
|
134
|
+
cpSync(join(dependency.root, file), join(target, file), { dereference: true });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
105
137
|
try {
|
|
106
138
|
renameSync(stage, dest);
|
|
107
139
|
}
|
|
@@ -241,6 +241,7 @@ export function buildAgentPayload(adapter, state, config) {
|
|
|
241
241
|
}
|
|
242
242
|
async function defaultPush(config, serviceId, payload, timeoutMs) {
|
|
243
243
|
const client = new NocvilleClient({
|
|
244
|
+
workspace: config.workspace,
|
|
244
245
|
apiUrl: config.apiUrl,
|
|
245
246
|
apiKey: config.apiKey,
|
|
246
247
|
serviceId,
|
|
@@ -359,6 +360,8 @@ export function spawnDetachedAgent(adapter, config, args, cliPath = process.argv
|
|
|
359
360
|
if (!cliPath)
|
|
360
361
|
return undefined;
|
|
361
362
|
const env = { ...process.env };
|
|
363
|
+
if (config.workspace)
|
|
364
|
+
env['NOCVILLE_WORKSPACE'] = config.workspace;
|
|
362
365
|
env['NOCVILLE_API_URL'] = config.apiUrl;
|
|
363
366
|
env['NOCVILLE_API_KEY'] = config.apiKey;
|
|
364
367
|
if (config.map)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocville/npc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Put your Claude Code and Codex sessions (and your servers) in the Nocville office as live NPCs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"pngjs": "^7.0.0",
|
|
27
|
-
"@nocville/node": "^0.
|
|
27
|
+
"@nocville/node": "^0.4.1"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"puppeteer-core": "*"
|