@nocville/npc 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -27
- package/dist/cjs/cli.js +32 -14
- package/dist/cjs/core/base-npc.js +9 -0
- package/dist/cjs/core/browser-connect.js +196 -0
- package/dist/cjs/core/pixel-art.js +4 -1
- package/dist/cjs/core/polling-npc.js +3 -2
- package/dist/cjs/npcs/agent-common/runtime.js +54 -22
- package/dist/cjs/npcs/agent-common/session.js +35 -9
- package/dist/cjs/npcs/agent-common/watchdog.js +4 -3
- package/dist/cjs/npcs/claude-code/defaults.js +2 -2
- package/dist/cjs/npcs/codex/defaults.js +2 -2
- package/dist/cjs/npcs/cursor/defaults.js +1 -1
- package/dist/cjs/npcs/cursor/hook-handler.js +1 -1
- package/dist/cjs/npcs/gemini/defaults.js +1 -1
- package/dist/cjs/npcs/web-page-watcher/pixelate.js +3 -2
- package/dist/cli.js +32 -14
- package/dist/core/base-npc.js +9 -0
- package/dist/core/browser-connect.d.ts +16 -0
- package/dist/core/browser-connect.js +189 -0
- package/dist/core/pixel-art.js +4 -1
- package/dist/core/polling-npc.d.ts +3 -2
- package/dist/core/polling-npc.js +3 -2
- package/dist/npcs/agent-common/runtime.js +54 -22
- package/dist/npcs/agent-common/session.js +35 -9
- package/dist/npcs/agent-common/watchdog.d.ts +2 -1
- package/dist/npcs/agent-common/watchdog.js +4 -3
- package/dist/npcs/claude-code/defaults.d.ts +1 -1
- package/dist/npcs/claude-code/defaults.js +2 -2
- package/dist/npcs/codex/defaults.d.ts +1 -1
- package/dist/npcs/codex/defaults.js +2 -2
- package/dist/npcs/cursor/defaults.js +1 -1
- package/dist/npcs/cursor/hook-handler.js +1 -1
- package/dist/npcs/gemini/defaults.js +1 -1
- package/dist/npcs/web-page-watcher/pixelate.js +3 -2
- 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
|
|
|
@@ -229,19 +236,14 @@ key and connection work. More at [nocville.com](https://nocville.com).
|
|
|
229
236
|
|
|
230
237
|
## Encrypted Cloudflare workspaces
|
|
231
238
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
the local NPCs using this credential, including coding-agent hooks and watchdogs.
|
|
244
|
-
The workspace pins the HTTPS origin and opaque map ID; changing either requires
|
|
245
|
-
matching verified workspace settings. The `doctor` command uses encrypted enrollment
|
|
246
|
-
and an offline probe for these workspaces. The Node SDK README describes private
|
|
247
|
-
profile storage and the encryption boundary.
|
|
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
|
|
|
@@ -186,22 +186,27 @@ async function main() {
|
|
|
186
186
|
console.log(USAGE);
|
|
187
187
|
process.exit(1);
|
|
188
188
|
}
|
|
189
|
-
|
|
189
|
+
let config = (0, config_js_1.resolveConfig)(parsed.flags);
|
|
190
190
|
const logger = new logger_js_1.Logger('cli', config.debug);
|
|
191
191
|
// Setup-style commands resolve the key: --api-key > env > saved > hidden prompt.
|
|
192
192
|
const isAgentSetup = ['claude-code', 'codex', 'gemini', 'cursor'].includes(parsed.command) &&
|
|
193
193
|
parsed.subcommand === 'setup' &&
|
|
194
194
|
!parsed.flags['uninstall'];
|
|
195
195
|
if (isAgentSetup || parsed.command === 'setup') {
|
|
196
|
-
if (
|
|
197
|
-
const {
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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);
|
|
205
210
|
}
|
|
206
211
|
}
|
|
207
212
|
const key = await resolveSetupKey(parsed.flags);
|
|
@@ -209,6 +214,16 @@ async function main() {
|
|
|
209
214
|
return;
|
|
210
215
|
if (key !== undefined)
|
|
211
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
|
+
}
|
|
212
227
|
}
|
|
213
228
|
// AI-agent integrations have their own config handling (hooks read the saved config).
|
|
214
229
|
if (parsed.command === 'claude-code') {
|
|
@@ -291,6 +306,10 @@ async function main() {
|
|
|
291
306
|
return;
|
|
292
307
|
}
|
|
293
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.');
|
|
294
313
|
if (!config.apiKey) {
|
|
295
314
|
console.error('Error: no API key — run `nocville-npc init --api-key=<key>`, or pass --api-key / NOCVILLE_API_KEY');
|
|
296
315
|
process.exit(1);
|
|
@@ -639,8 +658,7 @@ async function runSetupAll(flags) {
|
|
|
639
658
|
console.log('· Cursor not found (~/.cursor missing) — skipped');
|
|
640
659
|
}
|
|
641
660
|
if (!any) {
|
|
642
|
-
console.
|
|
643
|
-
process.exitCode = 1;
|
|
661
|
+
console.log('No supported coding agents found. You can still run NPC commands on this computer.');
|
|
644
662
|
}
|
|
645
663
|
}
|
|
646
664
|
main().catch((err) => {
|
|
@@ -139,6 +139,15 @@ class BaseNPC {
|
|
|
139
139
|
catch {
|
|
140
140
|
/* observer only */
|
|
141
141
|
}
|
|
142
|
+
if (!result.success && [401, 410].includes(result.statusCode ?? 0)) {
|
|
143
|
+
// A revoked key or an SDK version the server retired: retrying forever only
|
|
144
|
+
// costs requests. Stop; restarting after fixing it resumes monitoring.
|
|
145
|
+
this.logger.error(result.statusCode === 410
|
|
146
|
+
? 'This @nocville/npc version is no longer supported — upgrade it. Stopping.'
|
|
147
|
+
: 'API key rejected (HTTP 401) — check NOCVILLE_API_KEY / team settings. Stopping.');
|
|
148
|
+
void this.stop();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
142
151
|
if (!result.success) {
|
|
143
152
|
this.reportPushFailure(result.error);
|
|
144
153
|
}
|
|
@@ -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
|
|
@@ -4,6 +4,7 @@ exports.encodePixelArt = encodePixelArt;
|
|
|
4
4
|
exports.pixelArtFromPng = pixelArtFromPng;
|
|
5
5
|
exports.pixelDevice = pixelDevice;
|
|
6
6
|
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_zlib_1 = require("node:zlib");
|
|
7
8
|
const pngjs_1 = require("pngjs");
|
|
8
9
|
/**
|
|
9
10
|
* Helpers for custom pixel-art companions (`screenType: 'pixel-device'`): your NPC's own
|
|
@@ -68,7 +69,9 @@ function encodePixelArt(art) {
|
|
|
68
69
|
const fps = Math.min(12, Math.max(1, Math.round(art.fps ?? 4)));
|
|
69
70
|
tokens.push(`f${frames.length}`, `fps${fps}`);
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
+
// Deflated indices (`z`) are typically 15–25× smaller for pixel art.
|
|
73
|
+
tokens.push('z');
|
|
74
|
+
const encoded = (0, node_zlib_1.deflateRawSync)(pixels, { level: 9 }).toString('base64');
|
|
72
75
|
if (encoded.length > MAX_VALUE)
|
|
73
76
|
throw new Error('Pixel art is too large to send; use fewer frames or a smaller size.');
|
|
74
77
|
return {
|
|
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PollingNPC = exports.MIN_POLL_INTERVAL_MS = void 0;
|
|
4
4
|
const base_npc_js_1 = require("./base-npc.js");
|
|
5
5
|
/**
|
|
6
|
-
* Lower bound for any poll interval, so a typo can't
|
|
7
|
-
*
|
|
6
|
+
* Lower bound for any poll interval, so a typo can't hammer the watched source.
|
|
7
|
+
* The SDK sends changed status at most every 10 s and heartbeats every 30 s;
|
|
8
|
+
* the server limits producers per key, team and NPC and answers 429.
|
|
8
9
|
*/
|
|
9
10
|
exports.MIN_POLL_INTERVAL_MS = 5000;
|
|
10
11
|
/** How long `stop()` waits for an in-flight collection before pushing offline anyway. */
|
|
@@ -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
|
}
|
|
@@ -261,17 +261,43 @@ function buildAgentPayload(adapter, state, config) {
|
|
|
261
261
|
map: config.map,
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Clients reused within one process (a long-lived watchdog): a fresh client
|
|
266
|
+
* re-runs enrollment, manifest, version and session requests on every push.
|
|
267
|
+
* Any failure (e.g. a hook process took over the producer session) drops it.
|
|
268
|
+
*/
|
|
269
|
+
const clients = new Map();
|
|
264
270
|
async function defaultPush(config, serviceId, payload, timeoutMs) {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
271
|
+
const key = JSON.stringify([
|
|
272
|
+
config.workspace,
|
|
273
|
+
config.apiUrl,
|
|
274
|
+
config.apiKey,
|
|
269
275
|
serviceId,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
276
|
+
timeoutMs,
|
|
277
|
+
]);
|
|
278
|
+
let client = clients.get(key);
|
|
279
|
+
if (!client) {
|
|
280
|
+
client = new node_1.NocvilleClient({
|
|
281
|
+
workspace: config.workspace,
|
|
282
|
+
apiUrl: config.apiUrl,
|
|
283
|
+
apiKey: config.apiKey,
|
|
284
|
+
serviceId,
|
|
285
|
+
timeout: timeoutMs,
|
|
286
|
+
retries: 0,
|
|
287
|
+
debug: config.debug,
|
|
288
|
+
});
|
|
289
|
+
clients.set(key, client);
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
const result = await client.pushStatus(payload);
|
|
293
|
+
if (!result.success)
|
|
294
|
+
clients.delete(key);
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
clients.delete(key);
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
275
301
|
}
|
|
276
302
|
/**
|
|
277
303
|
* Push the session's current state if it has unpushed changes (or is offline),
|
|
@@ -7,7 +7,8 @@ const liveness_js_1 = require("./liveness.js");
|
|
|
7
7
|
const lock_js_1 = require("./lock.js");
|
|
8
8
|
const session_js_1 = require("./session.js");
|
|
9
9
|
exports.WATCHDOG_TICK_MS = 15_000;
|
|
10
|
-
|
|
10
|
+
/** Liveness only (status changes push at once); each heartbeat is a billed request. */
|
|
11
|
+
exports.WATCHDOG_HEARTBEAT_MS = 300_000;
|
|
11
12
|
exports.WATCHDOG_OFFLINE_RETRY_MS = 15 * 60_000;
|
|
12
13
|
async function ensureWatchdog(adapter, config, sessionId, cliPath, lockWaitMs) {
|
|
13
14
|
if (adapter.processNames?.length === 0 ||
|
|
@@ -42,7 +43,7 @@ async function runWatchdog(adapter, config, sessionId, deps = {}) {
|
|
|
42
43
|
const delivery = {
|
|
43
44
|
...deps,
|
|
44
45
|
onResult: (result) => {
|
|
45
|
-
if (!result.success && [400, 401, 403, 404].includes(result.statusCode ?? 0))
|
|
46
|
+
if (!result.success && [400, 401, 403, 404, 410].includes(result.statusCode ?? 0))
|
|
46
47
|
rejected = true;
|
|
47
48
|
deps.onResult?.(result);
|
|
48
49
|
},
|
|
@@ -123,7 +124,7 @@ async function runWatchdog(adapter, config, sessionId, deps = {}) {
|
|
|
123
124
|
...deps,
|
|
124
125
|
force: true,
|
|
125
126
|
onResult: (result) => {
|
|
126
|
-
if (!result.success && [400, 401, 403, 404].includes(result.statusCode ?? 0))
|
|
127
|
+
if (!result.success && [400, 401, 403, 404, 410].includes(result.statusCode ?? 0))
|
|
127
128
|
heartbeatRejected = true;
|
|
128
129
|
deps.onResult?.(result);
|
|
129
130
|
},
|
|
@@ -8,8 +8,8 @@ exports.DEFAULTS = {
|
|
|
8
8
|
stateFilePrefix: 'session-',
|
|
9
9
|
stateFileMaxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
10
10
|
httpTimeout: 4000,
|
|
11
|
-
/** Watchdog-backed sessions become unknown after
|
|
12
|
-
heartbeatSeconds:
|
|
11
|
+
/** Watchdog-backed sessions become unknown after twelve minutes without a heartbeat. */
|
|
12
|
+
heartbeatSeconds: 360,
|
|
13
13
|
/** Number of recent activity lines kept in the desk-sign ticker. */
|
|
14
14
|
activityBufferSize: 8,
|
|
15
15
|
/** Coalesce same-phase activity pushes closer together than this. */
|
|
@@ -8,8 +8,8 @@ exports.CODEX_DEFAULTS = {
|
|
|
8
8
|
/** Hook-mode per-session state (shared with the watcher to avoid double pushes). */
|
|
9
9
|
stateDir: (0, node_path_1.join)((0, node_os_1.tmpdir)(), 'nocville-codex'),
|
|
10
10
|
stateFileMaxAge: 24 * 60 * 60 * 1000,
|
|
11
|
-
/** Watchdog-backed sessions become unknown after
|
|
12
|
-
heartbeatSeconds:
|
|
11
|
+
/** Watchdog-backed sessions become unknown after twelve minutes without a heartbeat. */
|
|
12
|
+
heartbeatSeconds: 360,
|
|
13
13
|
minPushGapMs: 1500,
|
|
14
14
|
hookTimeoutSeconds: 10,
|
|
15
15
|
/** Watcher: Codex session rollout logs. */
|
|
@@ -6,7 +6,7 @@ const node_path_1 = require("node:path");
|
|
|
6
6
|
exports.CURSOR_DEFAULTS = {
|
|
7
7
|
stateDir: (0, node_path_1.join)((0, node_os_1.tmpdir)(), 'nocville-cursor'),
|
|
8
8
|
stateFileMaxAge: 24 * 60 * 60 * 1000,
|
|
9
|
-
heartbeatSeconds:
|
|
9
|
+
heartbeatSeconds: 360,
|
|
10
10
|
minPushGapMs: 1500,
|
|
11
11
|
hookTimeoutSeconds: 10,
|
|
12
12
|
};
|