podwatch 1.0.2
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 +28 -0
- package/README.md +92 -0
- package/bin/podwatch.js +10 -0
- package/dist/classifier.d.ts +22 -0
- package/dist/classifier.d.ts.map +1 -0
- package/dist/classifier.js +157 -0
- package/dist/classifier.js.map +1 -0
- package/dist/hooks/cost.d.ts +26 -0
- package/dist/hooks/cost.d.ts.map +1 -0
- package/dist/hooks/cost.js +107 -0
- package/dist/hooks/cost.js.map +1 -0
- package/dist/hooks/lifecycle.d.ts +16 -0
- package/dist/hooks/lifecycle.d.ts.map +1 -0
- package/dist/hooks/lifecycle.js +273 -0
- package/dist/hooks/lifecycle.js.map +1 -0
- package/dist/hooks/security.d.ts +19 -0
- package/dist/hooks/security.d.ts.map +1 -0
- package/dist/hooks/security.js +128 -0
- package/dist/hooks/security.js.map +1 -0
- package/dist/hooks/sessions.d.ts +10 -0
- package/dist/hooks/sessions.d.ts.map +1 -0
- package/dist/hooks/sessions.js +53 -0
- package/dist/hooks/sessions.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/redact.d.ts +35 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +372 -0
- package/dist/redact.js.map +1 -0
- package/dist/scanner.d.ts +27 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +117 -0
- package/dist/scanner.js.map +1 -0
- package/dist/transmitter.d.ts +58 -0
- package/dist/transmitter.d.ts.map +1 -0
- package/dist/transmitter.js +654 -0
- package/dist/transmitter.js.map +1 -0
- package/dist/types.d.ts +116 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/updater.d.ts +168 -0
- package/dist/updater.d.ts.map +1 -0
- package/dist/updater.js +579 -0
- package/dist/updater.js.map +1 -0
- package/lib/installer.js +599 -0
- package/openclaw.plugin.json +59 -0
- package/package.json +56 -0
- package/skills/podwatch/SKILL.md +112 -0
package/lib/installer.js
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync, spawn } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
|
|
9
|
+
// ── Colors ──────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const isTTY = process.stdout.isTTY;
|
|
12
|
+
|
|
13
|
+
const color = {
|
|
14
|
+
red: (s) => isTTY ? `\x1b[31m${s}\x1b[0m` : s,
|
|
15
|
+
green: (s) => isTTY ? `\x1b[32m${s}\x1b[0m` : s,
|
|
16
|
+
yellow: (s) => isTTY ? `\x1b[33m${s}\x1b[0m` : s,
|
|
17
|
+
cyan: (s) => isTTY ? `\x1b[36m${s}\x1b[0m` : s,
|
|
18
|
+
bold: (s) => isTTY ? `\x1b[1m${s}\x1b[0m` : s,
|
|
19
|
+
dim: (s) => isTTY ? `\x1b[2m${s}\x1b[0m` : s,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function log(msg) { console.log(msg); }
|
|
25
|
+
function info(msg) { console.log(color.cyan(msg)); }
|
|
26
|
+
function success(msg) { console.log(color.green(msg)); }
|
|
27
|
+
function warn(msg) { console.log(color.yellow(msg)); }
|
|
28
|
+
function fail(msg) {
|
|
29
|
+
console.error(color.red(`\n❌ ${msg}`));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sleep(ms) {
|
|
34
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function exec(cmd, opts = {}) {
|
|
38
|
+
try {
|
|
39
|
+
return execSync(cmd, {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
timeout: opts.timeout || 30000,
|
|
42
|
+
stdio: opts.stdio || 'pipe',
|
|
43
|
+
...opts,
|
|
44
|
+
}).trim();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (opts.allowFail) return null;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function showHelp() {
|
|
52
|
+
log('');
|
|
53
|
+
log(color.bold('podwatch') + ' — One-command Podwatch plugin installer for OpenClaw');
|
|
54
|
+
log('');
|
|
55
|
+
log(color.bold('Usage:'));
|
|
56
|
+
log(' npx podwatch <api-key>');
|
|
57
|
+
log(' podwatch <api-key>');
|
|
58
|
+
log('');
|
|
59
|
+
log(color.bold('Arguments:'));
|
|
60
|
+
log(' api-key Your Podwatch API key (starts with pw_)');
|
|
61
|
+
log(' Get one at https://podwatch.app/dashboard');
|
|
62
|
+
log('');
|
|
63
|
+
log(color.bold('What it does:'));
|
|
64
|
+
log(' 1. Validates your API key with the Podwatch server');
|
|
65
|
+
log(' 2. Checks OpenClaw is installed and running');
|
|
66
|
+
log(' 3. Checks OpenClaw version compatibility');
|
|
67
|
+
log(' 4. Backs up your current config');
|
|
68
|
+
log(' 5. Installs the Podwatch plugin');
|
|
69
|
+
log(' 6. Configures the plugin with your API key');
|
|
70
|
+
log(' 7. Restarts the gateway to activate');
|
|
71
|
+
log('');
|
|
72
|
+
log(color.bold('Options:'));
|
|
73
|
+
log(' --help, -h Show this help message');
|
|
74
|
+
log('');
|
|
75
|
+
log(color.bold('Compatibility:'));
|
|
76
|
+
log(' OS: Linux, macOS (Windows not supported)');
|
|
77
|
+
log(' OpenClaw: v2026.2.0 or later');
|
|
78
|
+
log(' Node.js: v16 or later');
|
|
79
|
+
log('');
|
|
80
|
+
log(color.dim('Docs: https://podwatch.app/docs'));
|
|
81
|
+
log('');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Config file discovery ───────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function findConfigPath() {
|
|
87
|
+
// 1. Env var
|
|
88
|
+
const envPath = process.env.OPENCLAW_CONFIG;
|
|
89
|
+
if (envPath && fs.existsSync(envPath)) return envPath;
|
|
90
|
+
|
|
91
|
+
const home = os.homedir();
|
|
92
|
+
|
|
93
|
+
// 2. Default location
|
|
94
|
+
const defaultPath = path.join(home, '.openclaw', 'openclaw.json');
|
|
95
|
+
if (fs.existsSync(defaultPath)) return defaultPath;
|
|
96
|
+
|
|
97
|
+
// 3. XDG config
|
|
98
|
+
const xdgPath = path.join(home, '.config', 'openclaw', 'openclaw.json');
|
|
99
|
+
if (fs.existsSync(xdgPath)) return xdgPath;
|
|
100
|
+
|
|
101
|
+
// 4. If none exist, use the default path (we'll create it)
|
|
102
|
+
return defaultPath;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── API key validation ──────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
function validateApiKeyRemote(apiKey) {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
const postData = JSON.stringify({ apiKey });
|
|
110
|
+
const url = new URL('https://podwatch.app/api/validate-key');
|
|
111
|
+
|
|
112
|
+
const req = https.request({
|
|
113
|
+
hostname: url.hostname,
|
|
114
|
+
port: 443,
|
|
115
|
+
path: url.pathname,
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
120
|
+
},
|
|
121
|
+
timeout: 10000,
|
|
122
|
+
}, (res) => {
|
|
123
|
+
let body = '';
|
|
124
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
125
|
+
res.on('end', () => {
|
|
126
|
+
if (res.statusCode === 200) {
|
|
127
|
+
resolve({ valid: true });
|
|
128
|
+
} else if (res.statusCode === 401 || res.statusCode === 404) {
|
|
129
|
+
resolve({ valid: false, reason: 'Invalid or expired API key' });
|
|
130
|
+
} else {
|
|
131
|
+
// Server error or unexpected — skip validation, don't block install
|
|
132
|
+
resolve({ valid: true, skipped: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
req.on('error', () => {
|
|
138
|
+
// Network error — skip validation, don't block install
|
|
139
|
+
resolve({ valid: true, skipped: true });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
req.on('timeout', () => {
|
|
143
|
+
req.destroy();
|
|
144
|
+
resolve({ valid: true, skipped: true });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
req.write(postData);
|
|
148
|
+
req.end();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Version parsing ─────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
// OpenClaw versions look like "2026.2.3-1" — we need >= 2026.2.0
|
|
155
|
+
function parseOpenClawVersion(versionStr) {
|
|
156
|
+
if (!versionStr) return null;
|
|
157
|
+
const match = versionStr.match(/^(\d{4})\.(\d+)\.(\d+)/);
|
|
158
|
+
if (!match) return null;
|
|
159
|
+
return {
|
|
160
|
+
year: parseInt(match[1], 10),
|
|
161
|
+
minor: parseInt(match[2], 10),
|
|
162
|
+
patch: parseInt(match[3], 10),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isVersionCompatible(version) {
|
|
167
|
+
if (!version) return null; // unknown — proceed with warning
|
|
168
|
+
// Minimum: 2026.2.0 (plugin system introduced)
|
|
169
|
+
if (version.year > 2026) return true;
|
|
170
|
+
if (version.year < 2026) return false;
|
|
171
|
+
if (version.minor >= 2) return true;
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Steps ───────────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
function stepValidateApiKey() {
|
|
178
|
+
log('');
|
|
179
|
+
info('🔍 Validating API key format...');
|
|
180
|
+
|
|
181
|
+
const apiKey = process.argv[2];
|
|
182
|
+
|
|
183
|
+
if (!apiKey) {
|
|
184
|
+
fail('No API key provided.\n\n Usage: npx podwatch <api-key>\n Get your key at: https://podwatch.app/dashboard');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (apiKey === '--help' || apiKey === '-h') {
|
|
188
|
+
showHelp();
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!apiKey.startsWith('pw_')) {
|
|
193
|
+
fail(`Invalid API key format: "${apiKey}"\n API keys must start with "pw_"\n Get your key at: https://podwatch.app/dashboard`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (apiKey.length < 8) {
|
|
197
|
+
fail('API key is too short. Check that you copied the full key.');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
success('✅ API key format valid');
|
|
201
|
+
return apiKey;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function stepValidateApiKeyRemote(apiKey) {
|
|
205
|
+
info('🔍 Validating API key with server...');
|
|
206
|
+
|
|
207
|
+
const result = await validateApiKeyRemote(apiKey);
|
|
208
|
+
|
|
209
|
+
if (result.skipped) {
|
|
210
|
+
warn('⚠️ Could not reach Podwatch server — skipping key validation');
|
|
211
|
+
warn(' (key will be validated when the plugin connects)');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!result.valid) {
|
|
216
|
+
fail(`API key is invalid: ${result.reason}\n Check your key at: https://podwatch.app/dashboard`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
success('✅ API key validated');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function stepCheckPlatform() {
|
|
223
|
+
if (process.platform === 'win32') {
|
|
224
|
+
fail(
|
|
225
|
+
'Windows is not supported yet.\n' +
|
|
226
|
+
' Podwatch requires a Unix-like environment (Linux or macOS).\n' +
|
|
227
|
+
' If you\'re using WSL, run this command inside WSL instead.'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function stepCheckPrerequisites() {
|
|
233
|
+
info('🔍 Checking prerequisites...');
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
exec('command -v openclaw');
|
|
237
|
+
} catch {
|
|
238
|
+
fail('OpenClaw not found. Install it first: https://docs.openclaw.ai');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
success('✅ OpenClaw found');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function stepCheckVersion() {
|
|
245
|
+
info('🔍 Checking OpenClaw version...');
|
|
246
|
+
|
|
247
|
+
let versionStr = null;
|
|
248
|
+
try {
|
|
249
|
+
versionStr = exec('openclaw --version', { allowFail: true, timeout: 10000 });
|
|
250
|
+
} catch {
|
|
251
|
+
// ignore
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!versionStr) {
|
|
255
|
+
warn('⚠️ Could not determine OpenClaw version — proceeding anyway');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
log(color.dim(` Version: ${versionStr}`));
|
|
260
|
+
|
|
261
|
+
const version = parseOpenClawVersion(versionStr);
|
|
262
|
+
const compatible = isVersionCompatible(version);
|
|
263
|
+
|
|
264
|
+
if (compatible === false) {
|
|
265
|
+
fail(
|
|
266
|
+
`OpenClaw ${versionStr} is too old.\n` +
|
|
267
|
+
' Podwatch requires OpenClaw v2026.2.0 or later.\n' +
|
|
268
|
+
' Update OpenClaw: openclaw update'
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (compatible === null) {
|
|
273
|
+
warn('⚠️ Unrecognized version format — proceeding anyway');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
success('✅ OpenClaw version compatible');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function stepCheckGateway() {
|
|
281
|
+
info('🔍 Checking gateway status...');
|
|
282
|
+
|
|
283
|
+
let running = isGatewayRunning();
|
|
284
|
+
|
|
285
|
+
if (!running) {
|
|
286
|
+
warn('⚠️ Gateway not running. Starting it...');
|
|
287
|
+
try {
|
|
288
|
+
exec('openclaw gateway start', { timeout: 15000, allowFail: true });
|
|
289
|
+
await sleep(5000);
|
|
290
|
+
running = isGatewayRunning();
|
|
291
|
+
} catch {
|
|
292
|
+
// ignore, we'll check below
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!running) {
|
|
297
|
+
fail(
|
|
298
|
+
'Could not start the OpenClaw gateway.\n' +
|
|
299
|
+
' Try manually:\n' +
|
|
300
|
+
' openclaw gateway start\n' +
|
|
301
|
+
' Then re-run this installer.'
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
success('✅ Gateway running');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isGatewayRunning() {
|
|
309
|
+
// Try openclaw status first
|
|
310
|
+
try {
|
|
311
|
+
const out = exec('openclaw status --json', { allowFail: true, timeout: 10000 });
|
|
312
|
+
if (out) {
|
|
313
|
+
try {
|
|
314
|
+
const status = JSON.parse(out);
|
|
315
|
+
if (status.running || status.gateway?.running || status.status === 'running') {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
// If output contains "running" loosely
|
|
320
|
+
if (out.toLowerCase().includes('running')) return true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
// fall through
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Fallback: pgrep
|
|
328
|
+
try {
|
|
329
|
+
const result = exec('pgrep -f openclaw', { allowFail: true });
|
|
330
|
+
return !!result;
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function copyDirSync(src, dest) {
|
|
337
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
338
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
339
|
+
for (const entry of entries) {
|
|
340
|
+
const srcPath = path.join(src, entry.name);
|
|
341
|
+
const destPath = path.join(dest, entry.name);
|
|
342
|
+
if (entry.isDirectory()) {
|
|
343
|
+
copyDirSync(srcPath, destPath);
|
|
344
|
+
} else {
|
|
345
|
+
fs.copyFileSync(srcPath, destPath);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function stepInstallPlugin() {
|
|
351
|
+
info('📦 Installing Podwatch plugin...');
|
|
352
|
+
|
|
353
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
354
|
+
const extensionsDir = path.join(os.homedir(), '.openclaw', 'extensions', 'podwatch');
|
|
355
|
+
|
|
356
|
+
// Files/dirs to copy from this package into the extensions dir
|
|
357
|
+
const distSrc = path.join(packageRoot, 'dist');
|
|
358
|
+
const manifestSrc = path.join(packageRoot, 'openclaw.plugin.json');
|
|
359
|
+
const pkgSrc = path.join(packageRoot, 'package.json');
|
|
360
|
+
const skillsSrc = path.join(packageRoot, 'skills');
|
|
361
|
+
|
|
362
|
+
// Validate source files exist
|
|
363
|
+
if (!fs.existsSync(distSrc)) {
|
|
364
|
+
fail('Plugin dist/ directory not found in package. The package may be corrupted.');
|
|
365
|
+
}
|
|
366
|
+
if (!fs.existsSync(manifestSrc)) {
|
|
367
|
+
fail('openclaw.plugin.json not found in package. The package may be corrupted.');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Create extensions dir
|
|
371
|
+
fs.mkdirSync(extensionsDir, { recursive: true });
|
|
372
|
+
|
|
373
|
+
// Copy dist/
|
|
374
|
+
const distDest = path.join(extensionsDir, 'dist');
|
|
375
|
+
if (fs.existsSync(distDest)) {
|
|
376
|
+
fs.rmSync(distDest, { recursive: true, force: true });
|
|
377
|
+
}
|
|
378
|
+
copyDirSync(distSrc, distDest);
|
|
379
|
+
log(color.dim(' Copied dist/'));
|
|
380
|
+
|
|
381
|
+
// Copy openclaw.plugin.json
|
|
382
|
+
fs.copyFileSync(manifestSrc, path.join(extensionsDir, 'openclaw.plugin.json'));
|
|
383
|
+
log(color.dim(' Copied openclaw.plugin.json'));
|
|
384
|
+
|
|
385
|
+
// Copy package.json
|
|
386
|
+
if (fs.existsSync(pkgSrc)) {
|
|
387
|
+
fs.copyFileSync(pkgSrc, path.join(extensionsDir, 'package.json'));
|
|
388
|
+
log(color.dim(' Copied package.json'));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Copy skills/ if present
|
|
392
|
+
if (fs.existsSync(skillsSrc)) {
|
|
393
|
+
const skillsDest = path.join(extensionsDir, 'skills');
|
|
394
|
+
if (fs.existsSync(skillsDest)) {
|
|
395
|
+
fs.rmSync(skillsDest, { recursive: true, force: true });
|
|
396
|
+
}
|
|
397
|
+
copyDirSync(skillsSrc, skillsDest);
|
|
398
|
+
log(color.dim(' Copied skills/'));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
success('✅ Plugin installed to ' + extensionsDir);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function stepBackupConfig(configPath) {
|
|
405
|
+
if (!fs.existsSync(configPath)) return;
|
|
406
|
+
|
|
407
|
+
info('💾 Backing up current config...');
|
|
408
|
+
|
|
409
|
+
const backupPath = configPath + '.bak';
|
|
410
|
+
try {
|
|
411
|
+
fs.copyFileSync(configPath, backupPath);
|
|
412
|
+
log(color.dim(` Backup: ${backupPath}`));
|
|
413
|
+
success('✅ Config backed up');
|
|
414
|
+
} catch (err) {
|
|
415
|
+
warn(`⚠️ Could not back up config: ${err.message}`);
|
|
416
|
+
warn(' Proceeding anyway — your config will be merged, not replaced');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function stepPatchConfig(apiKey) {
|
|
421
|
+
info('⚙️ Configuring plugin...');
|
|
422
|
+
|
|
423
|
+
const configPath = findConfigPath();
|
|
424
|
+
|
|
425
|
+
// Backup before modifying
|
|
426
|
+
stepBackupConfig(configPath);
|
|
427
|
+
|
|
428
|
+
// Read existing config or start fresh
|
|
429
|
+
let config = {};
|
|
430
|
+
if (fs.existsSync(configPath)) {
|
|
431
|
+
try {
|
|
432
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
433
|
+
config = JSON.parse(raw);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
fail(`Failed to parse config file at ${configPath}\n Error: ${err.message}\n Fix the JSON manually or delete it to start fresh.`);
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
// Ensure directory exists
|
|
439
|
+
const dir = path.dirname(configPath);
|
|
440
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
441
|
+
log(color.dim(` Creating config at ${configPath}`));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Merge diagnostics (required for cost tracking)
|
|
445
|
+
if (!config.diagnostics) config.diagnostics = {};
|
|
446
|
+
config.diagnostics.enabled = true;
|
|
447
|
+
|
|
448
|
+
// Merge plugins
|
|
449
|
+
if (!config.plugins) config.plugins = {};
|
|
450
|
+
if (!config.plugins.entries) config.plugins.entries = {};
|
|
451
|
+
|
|
452
|
+
// Merge podwatch entry (preserve other fields if they exist)
|
|
453
|
+
if (!config.plugins.entries.podwatch) config.plugins.entries.podwatch = {};
|
|
454
|
+
|
|
455
|
+
config.plugins.entries.podwatch.enabled = true;
|
|
456
|
+
|
|
457
|
+
if (!config.plugins.entries.podwatch.config) config.plugins.entries.podwatch.config = {};
|
|
458
|
+
config.plugins.entries.podwatch.config.apiKey = apiKey;
|
|
459
|
+
config.plugins.entries.podwatch.config.endpoint = 'https://podwatch.app/api';
|
|
460
|
+
config.plugins.entries.podwatch.config.enableBudgetEnforcement = true;
|
|
461
|
+
config.plugins.entries.podwatch.config.enableSecurityAlerts = true;
|
|
462
|
+
|
|
463
|
+
// Write back
|
|
464
|
+
try {
|
|
465
|
+
const output = JSON.stringify(config, null, 2) + '\n';
|
|
466
|
+
fs.writeFileSync(configPath, output, 'utf8');
|
|
467
|
+
} catch (err) {
|
|
468
|
+
fail(`Failed to write config file at ${configPath}\n Error: ${err.message}\n Check file permissions.`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
log(color.dim(` Config: ${configPath}`));
|
|
472
|
+
success('✅ Configuration saved');
|
|
473
|
+
|
|
474
|
+
return configPath;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function stepVerifyConfig(configPath) {
|
|
478
|
+
info('🔍 Verifying configuration...');
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
482
|
+
const config = JSON.parse(raw);
|
|
483
|
+
|
|
484
|
+
const checks = [
|
|
485
|
+
[config.plugins?.entries?.podwatch, 'podwatch entry not found'],
|
|
486
|
+
[config.plugins?.entries?.podwatch?.enabled, 'podwatch plugin not enabled'],
|
|
487
|
+
[config.plugins?.entries?.podwatch?.config?.apiKey, 'API key not found'],
|
|
488
|
+
[config.plugins?.entries?.podwatch?.config?.endpoint, 'endpoint not found'],
|
|
489
|
+
[config.plugins?.entries?.podwatch?.config?.enableBudgetEnforcement === true, 'budget enforcement not enabled'],
|
|
490
|
+
[config.plugins?.entries?.podwatch?.config?.enableSecurityAlerts === true, 'security alerts not enabled'],
|
|
491
|
+
[config.diagnostics?.enabled === true, 'diagnostics not enabled'],
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
for (const [condition, msg] of checks) {
|
|
495
|
+
if (!condition) {
|
|
496
|
+
fail(`Verification failed: ${msg} in config after writing.`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
success('✅ Configuration verified');
|
|
501
|
+
} catch (err) {
|
|
502
|
+
if (err.code === 'ENOENT') {
|
|
503
|
+
fail(`Config file not found at ${configPath} after writing.`);
|
|
504
|
+
}
|
|
505
|
+
fail(`Verification failed: ${err.message}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function stepRestartGateway() {
|
|
510
|
+
info('🔄 Restarting gateway to load plugin...');
|
|
511
|
+
log(color.dim(' Your agent will go quiet for ~10 seconds during restart.'));
|
|
512
|
+
|
|
513
|
+
// Background the restart so the installer can exit cleanly
|
|
514
|
+
// and the calling agent can relay the success message before dying
|
|
515
|
+
const child = spawn('bash', [
|
|
516
|
+
'-c',
|
|
517
|
+
'sleep 2 && openclaw gateway stop 2>/dev/null; sleep 3 && openclaw gateway start 2>/dev/null',
|
|
518
|
+
], {
|
|
519
|
+
detached: true,
|
|
520
|
+
stdio: 'ignore',
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
child.unref();
|
|
524
|
+
|
|
525
|
+
success('✅ Gateway restart scheduled (will happen in ~2 seconds)');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function stepFinalMessage() {
|
|
529
|
+
log('');
|
|
530
|
+
log(color.bold(color.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')));
|
|
531
|
+
log('');
|
|
532
|
+
success('🎉 Podwatch installed successfully!');
|
|
533
|
+
log('');
|
|
534
|
+
log(` ${color.bold('Dashboard:')} https://podwatch.app`);
|
|
535
|
+
log(` ${color.bold('Docs:')} https://podwatch.app/docs`);
|
|
536
|
+
log('');
|
|
537
|
+
log(color.dim(' Your agent will restart in a few seconds.'));
|
|
538
|
+
log(color.dim(' Events will appear on your dashboard within minutes.'));
|
|
539
|
+
log('');
|
|
540
|
+
log(color.bold(' Features enabled:'));
|
|
541
|
+
log(' • Cost tracking & monitoring');
|
|
542
|
+
log(' • Budget enforcement');
|
|
543
|
+
log(' • Security alerts');
|
|
544
|
+
log(' • Agent pulse (online status)');
|
|
545
|
+
log('');
|
|
546
|
+
log(color.bold(color.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')));
|
|
547
|
+
log('');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
551
|
+
|
|
552
|
+
async function run() {
|
|
553
|
+
log('');
|
|
554
|
+
log(color.bold(' 🛡️ Podwatch Installer'));
|
|
555
|
+
log(color.dim(' Agent security monitoring for OpenClaw'));
|
|
556
|
+
log('');
|
|
557
|
+
|
|
558
|
+
// Check for --help before anything
|
|
559
|
+
const arg = process.argv[2];
|
|
560
|
+
if (arg === '--help' || arg === '-h') {
|
|
561
|
+
showHelp();
|
|
562
|
+
process.exit(0);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Step 1: Platform check
|
|
566
|
+
stepCheckPlatform();
|
|
567
|
+
|
|
568
|
+
// Step 2: Validate API key format
|
|
569
|
+
const apiKey = stepValidateApiKey();
|
|
570
|
+
|
|
571
|
+
// Step 3: Validate API key with server
|
|
572
|
+
await stepValidateApiKeyRemote(apiKey);
|
|
573
|
+
|
|
574
|
+
// Step 4: Check prerequisites
|
|
575
|
+
stepCheckPrerequisites();
|
|
576
|
+
|
|
577
|
+
// Step 5: Check version compatibility
|
|
578
|
+
stepCheckVersion();
|
|
579
|
+
|
|
580
|
+
// Step 6: Check gateway
|
|
581
|
+
await stepCheckGateway();
|
|
582
|
+
|
|
583
|
+
// Step 7: Install plugin
|
|
584
|
+
stepInstallPlugin();
|
|
585
|
+
|
|
586
|
+
// Step 8: Patch config (includes backup)
|
|
587
|
+
const configPath = stepPatchConfig(apiKey);
|
|
588
|
+
|
|
589
|
+
// Step 9: Verify config
|
|
590
|
+
stepVerifyConfig(configPath);
|
|
591
|
+
|
|
592
|
+
// Step 10: Restart gateway (backgrounded)
|
|
593
|
+
stepRestartGateway();
|
|
594
|
+
|
|
595
|
+
// Step 11: Final message
|
|
596
|
+
stepFinalMessage();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
module.exports = { run };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "podwatch",
|
|
3
|
+
"name": "podwatch",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Cost monitoring, budget enforcement, and security alerts for OpenClaw agents",
|
|
6
|
+
"entry": "./dist/index.js",
|
|
7
|
+
"skills": ["./skills/podwatch"],
|
|
8
|
+
"configSchema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": false,
|
|
11
|
+
"properties": {
|
|
12
|
+
"apiKey": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Your Podwatch API key from podwatch.app/settings"
|
|
15
|
+
},
|
|
16
|
+
"endpoint": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Podwatch API endpoint",
|
|
19
|
+
"default": "https://podwatch.app/api"
|
|
20
|
+
},
|
|
21
|
+
"enableBudgetEnforcement": {
|
|
22
|
+
"type": "boolean",
|
|
23
|
+
"description": "Block tool calls when budget is exceeded",
|
|
24
|
+
"default": true
|
|
25
|
+
},
|
|
26
|
+
"enableSecurityAlerts": {
|
|
27
|
+
"type": "boolean",
|
|
28
|
+
"description": "Monitor for suspicious tool patterns",
|
|
29
|
+
"default": true
|
|
30
|
+
},
|
|
31
|
+
"pulseIntervalMs": {
|
|
32
|
+
"type": "number",
|
|
33
|
+
"description": "Pulse alive-ping interval in milliseconds",
|
|
34
|
+
"default": 300000
|
|
35
|
+
},
|
|
36
|
+
"heartbeatIntervalMs": {
|
|
37
|
+
"type": "number",
|
|
38
|
+
"description": "Deprecated — use pulseIntervalMs instead. Falls back to this if pulseIntervalMs not set.",
|
|
39
|
+
"default": 300000
|
|
40
|
+
},
|
|
41
|
+
"scanIntervalMs": {
|
|
42
|
+
"type": "number",
|
|
43
|
+
"description": "How often to scan installed skills/plugins (ms)",
|
|
44
|
+
"default": 21600000
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"required": []
|
|
48
|
+
},
|
|
49
|
+
"uiHints": {
|
|
50
|
+
"apiKey": { "label": "API Key", "sensitive": true, "placeholder": "pw_..." },
|
|
51
|
+
"endpoint": { "label": "API Endpoint" },
|
|
52
|
+
"enableBudgetEnforcement": { "label": "Budget Enforcement" },
|
|
53
|
+
"enableSecurityAlerts": { "label": "Security Alerts" },
|
|
54
|
+
"pulseIntervalMs": { "label": "Pulse Interval (ms)" },
|
|
55
|
+
"heartbeatIntervalMs": { "label": "Pulse Interval (ms) [deprecated]" },
|
|
56
|
+
"scanIntervalMs": { "label": "Scan Interval (ms)" }
|
|
57
|
+
},
|
|
58
|
+
"requiresDiagnostics": true
|
|
59
|
+
}
|