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.
Files changed (51) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +92 -0
  3. package/bin/podwatch.js +10 -0
  4. package/dist/classifier.d.ts +22 -0
  5. package/dist/classifier.d.ts.map +1 -0
  6. package/dist/classifier.js +157 -0
  7. package/dist/classifier.js.map +1 -0
  8. package/dist/hooks/cost.d.ts +26 -0
  9. package/dist/hooks/cost.d.ts.map +1 -0
  10. package/dist/hooks/cost.js +107 -0
  11. package/dist/hooks/cost.js.map +1 -0
  12. package/dist/hooks/lifecycle.d.ts +16 -0
  13. package/dist/hooks/lifecycle.d.ts.map +1 -0
  14. package/dist/hooks/lifecycle.js +273 -0
  15. package/dist/hooks/lifecycle.js.map +1 -0
  16. package/dist/hooks/security.d.ts +19 -0
  17. package/dist/hooks/security.d.ts.map +1 -0
  18. package/dist/hooks/security.js +128 -0
  19. package/dist/hooks/security.js.map +1 -0
  20. package/dist/hooks/sessions.d.ts +10 -0
  21. package/dist/hooks/sessions.d.ts.map +1 -0
  22. package/dist/hooks/sessions.js +53 -0
  23. package/dist/hooks/sessions.js.map +1 -0
  24. package/dist/index.d.ts +32 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +120 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/redact.d.ts +35 -0
  29. package/dist/redact.d.ts.map +1 -0
  30. package/dist/redact.js +372 -0
  31. package/dist/redact.js.map +1 -0
  32. package/dist/scanner.d.ts +27 -0
  33. package/dist/scanner.d.ts.map +1 -0
  34. package/dist/scanner.js +117 -0
  35. package/dist/scanner.js.map +1 -0
  36. package/dist/transmitter.d.ts +58 -0
  37. package/dist/transmitter.d.ts.map +1 -0
  38. package/dist/transmitter.js +654 -0
  39. package/dist/transmitter.js.map +1 -0
  40. package/dist/types.d.ts +116 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +9 -0
  43. package/dist/types.js.map +1 -0
  44. package/dist/updater.d.ts +168 -0
  45. package/dist/updater.d.ts.map +1 -0
  46. package/dist/updater.js +579 -0
  47. package/dist/updater.js.map +1 -0
  48. package/lib/installer.js +599 -0
  49. package/openclaw.plugin.json +59 -0
  50. package/package.json +56 -0
  51. package/skills/podwatch/SKILL.md +112 -0
@@ -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
+ }