@oked/openclaw-cli 0.1.0 → 0.1.3

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 CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 OKed
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OKed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,31 +1,31 @@
1
- # @oked/openclaw-cli
2
-
3
- Installer CLI for the [`@oked/openclaw`](../openclaw) plugin. Wraps `openclaw plugins install`, writes the OKed entry into `~/.openclaw/openclaw.json`, and restarts the gateway.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install -g @oked/openclaw @oked/openclaw-cli
9
- oked-openclaw init
10
- ```
11
-
12
- `init` will:
13
-
14
- 1. Locate the plugin source (sibling monorepo package or the global install).
15
- 2. Run `openclaw plugins install --link <path>`.
16
- 3. Prompt for `OKED_API_KEY` and `minTier` (defaults to `review`).
17
- 4. Write `plugins.allow: ["oked"]` and `plugins.entries.oked: { enabled, apiKey, backendUrl, minTier }` into `openclaw.json`.
18
- 5. Detect the OpenClaw daemon (systemd / pm2 / launchd / bare process) and offer to restart it.
19
-
20
- ## Subcommands
21
-
22
- | Command | Description |
23
- |---|---|
24
- | `oked-openclaw init` | Install the plugin and configure openclaw.json |
25
- | `oked-openclaw status` | Show install state, config, and backend reachability |
26
- | `oked-openclaw test` | Send a test approval request via the OKed SDK |
27
- | `oked-openclaw uninstall` | Remove the OKed entry and uninstall the plugin |
28
-
29
- ## License
30
-
31
- [MIT](./LICENSE)
1
+ # @oked/openclaw-cli
2
+
3
+ Installer CLI for the [`@oked/openclaw`](../openclaw) plugin. Wraps `openclaw plugins install`, writes the OKed entry into `~/.openclaw/openclaw.json`, and restarts the gateway.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @oked/openclaw @oked/openclaw-cli
9
+ oked-openclaw init
10
+ ```
11
+
12
+ `init` will:
13
+
14
+ 1. Locate the plugin source (sibling monorepo package or the global install).
15
+ 2. Run `openclaw plugins install --link <path>`.
16
+ 3. Prompt for `OKED_API_KEY` and `minTier` (defaults to `review`).
17
+ 4. Write `plugins.allow: ["oked"]` and `plugins.entries.oked: { enabled, apiKey, backendUrl, minTier }` into `openclaw.json`.
18
+ 5. Detect the OpenClaw daemon (systemd / pm2 / launchd / bare process) and offer to restart it.
19
+
20
+ ## Subcommands
21
+
22
+ | Command | Description |
23
+ |---|---|
24
+ | `oked-openclaw init` | Install the plugin and configure openclaw.json |
25
+ | `oked-openclaw status` | Show install state, config, and backend reachability |
26
+ | `oked-openclaw test` | Send a test approval request via the OKed SDK |
27
+ | `oked-openclaw uninstall` | Remove the OKed entry and uninstall the plugin |
28
+
29
+ ## License
30
+
31
+ [MIT](./LICENSE)
@@ -1,558 +1,558 @@
1
- #!/usr/bin/env node
2
- /**
3
- * @oked/openclaw-cli - installer for the @oked/openclaw plugin.
4
- *
5
- * Subcommands:
6
- * oked-openclaw init Install the plugin via `openclaw plugins install`,
7
- * persist API key + minTier into ~/.openclaw/openclaw.json,
8
- * and restart the OpenClaw gateway daemon.
9
- * oked-openclaw status Print install state, config, and backend reachability.
10
- * oked-openclaw test Send a test approval request to verify SDK + Telegram.
11
- * oked-openclaw uninstall Remove the OKed entry from openclaw.json and uninstall the plugin.
12
- */
13
-
14
- import { readFile, writeFile, mkdir, access, chmod } from 'node:fs/promises';
15
- import { homedir, hostname, platform } from 'node:os';
16
- import path from 'node:path';
17
- import { fileURLToPath } from 'node:url';
18
- import readline from 'node:readline/promises';
19
- import { stdin as input, stdout as output } from 'node:process';
20
- import { spawn, spawnSync } from 'node:child_process';
21
-
22
- import { OKedClient } from '@oked/sdk';
23
-
24
- const OPENCLAW_DIR = path.join(homedir(), '.openclaw');
25
- const OPENCLAW_CONFIG = path.join(OPENCLAW_DIR, 'openclaw.json');
26
- const OKED_DIR = path.join(homedir(), '.oked');
27
- const OKED_CONFIG = path.join(OKED_DIR, 'config.json');
28
- const DEFAULT_BACKEND_URL = process.env.OKED_BACKEND_URL || 'https://api.oked.ai';
29
- const CLIENT_VERSION = '0.1.0';
30
-
31
- async function chmodOwnerOnly(file) {
32
- try { await chmod(file, 0o600); } catch { /* Windows */ }
33
- }
34
-
35
- async function writeOkedConfig(apiKey, backendUrl) {
36
- await mkdir(OKED_DIR, { recursive: true });
37
- let existing = {};
38
- try {
39
- existing = JSON.parse(await readFile(OKED_CONFIG, 'utf8')) || {};
40
- if (typeof existing !== 'object') existing = {};
41
- } catch {
42
- existing = {};
43
- }
44
- const payload = { ...existing, apiKey, backendUrl };
45
- await writeFile(OKED_CONFIG, JSON.stringify(payload, null, 2) + '\n', 'utf8');
46
- await chmodOwnerOnly(OKED_CONFIG);
47
- }
48
-
49
- function openBrowser(url) {
50
- const plat = platform();
51
- let cmd;
52
- let args;
53
- if (plat === 'darwin') {
54
- cmd = 'open'; args = [url];
55
- } else if (plat === 'win32') {
56
- cmd = 'cmd'; args = ['/c', 'start', '', url];
57
- } else {
58
- cmd = 'xdg-open'; args = [url];
59
- }
60
- try {
61
- spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
62
- } catch { /* best effort */ }
63
- }
64
-
65
- async function deviceCodePair() {
66
- const codeRes = await fetch(`${DEFAULT_BACKEND_URL}/api/v1/device/code`, {
67
- method: 'POST',
68
- headers: { 'Content-Type': 'application/json' },
69
- body: JSON.stringify({
70
- client_type: 'openclaw',
71
- hostname: hostname(),
72
- client_version: CLIENT_VERSION,
73
- }),
74
- });
75
- if (!codeRes.ok) {
76
- throw new Error(`Pairing request failed: ${codeRes.status} ${await codeRes.text()}`);
77
- }
78
- const code = await codeRes.json();
79
-
80
- console.log('');
81
- console.log(' To pair this device, open in your browser:');
82
- console.log(` ${code.verification_uri_complete}`);
83
- console.log('');
84
- console.log(` Or visit ${code.verification_uri} and enter the code:`);
85
- console.log(` ${code.user_code}`);
86
- console.log('');
87
- console.log(' Waiting for confirmation...');
88
-
89
- openBrowser(code.verification_uri_complete);
90
-
91
- const deadline = Date.now() + (code.expires_in || 600) * 1000;
92
- const intervalMs = Math.max(1, code.interval || 3) * 1000;
93
- while (Date.now() < deadline) {
94
- await new Promise((r) => setTimeout(r, intervalMs));
95
- const pollRes = await fetch(`${DEFAULT_BACKEND_URL}/api/v1/device/poll`, {
96
- method: 'POST',
97
- headers: { 'Content-Type': 'application/json' },
98
- body: JSON.stringify({ device_code: code.device_code }),
99
- });
100
- if (pollRes.status === 410) {
101
- throw new Error('Pairing code expired. Run init again.');
102
- }
103
- if (!pollRes.ok) continue;
104
- const body = await pollRes.json();
105
- if (body.status === 'approved' && body.api_key) {
106
- return body.api_key;
107
- }
108
- }
109
- throw new Error('Pairing timed out. Run init again.');
110
- }
111
-
112
- // --- helpers --------------------------------------------------------------
113
-
114
- function maskKey(key) {
115
- if (!key) return '(not set)';
116
- if (key.length <= 8) return '****';
117
- return `${key.slice(0, 4)}...${key.slice(-4)}`;
118
- }
119
-
120
- function run(cmd, args, opts = {}) {
121
- const r = spawnSync(cmd, args, { encoding: 'utf8', ...opts });
122
- return {
123
- code: r.status ?? -1,
124
- stdout: (r.stdout || '').trim(),
125
- stderr: (r.stderr || '').trim(),
126
- error: r.error,
127
- };
128
- }
129
-
130
- function commandExists(cmd) {
131
- const probe = platform() === 'win32' ? 'where' : 'which';
132
- return run(probe, [cmd]).code === 0;
133
- }
134
-
135
- async function fileExists(p) {
136
- try { await access(p); return true; } catch { return false; }
137
- }
138
-
139
- async function readConfig() {
140
- try {
141
- const raw = await readFile(OPENCLAW_CONFIG, 'utf8');
142
- return JSON.parse(raw);
143
- } catch (err) {
144
- if (err.code === 'ENOENT') return {};
145
- throw new Error(`Could not parse ${OPENCLAW_CONFIG}: ${err.message}`);
146
- }
147
- }
148
-
149
- async function writeConfig(cfg) {
150
- await mkdir(OPENCLAW_DIR, { recursive: true });
151
- await writeFile(OPENCLAW_CONFIG, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
152
- await chmodOwnerOnly(OPENCLAW_CONFIG);
153
- }
154
-
155
- async function prompt(question) {
156
- if (!process.stdin.isTTY) return '';
157
- const rl = readline.createInterface({ input, output });
158
- const a = await rl.question(question);
159
- rl.close();
160
- return a.trim();
161
- }
162
-
163
- async function confirm(question, defaultYes = true) {
164
- const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
165
- const a = (await prompt(question + suffix)).toLowerCase();
166
- if (!a) return defaultYes;
167
- return a === 'y' || a === 'yes';
168
- }
169
-
170
- function runStreaming(cmd, args) {
171
- return new Promise((resolve) => {
172
- const child = spawn(cmd, args, { stdio: 'inherit' });
173
- child.on('close', (code) => resolve(code ?? -1));
174
- child.on('error', (err) => {
175
- console.error(` Failed to spawn ${cmd}: ${err.message}`);
176
- resolve(-1);
177
- });
178
- });
179
- }
180
-
181
- // --- plugin path discovery ------------------------------------------------
182
-
183
- async function findPluginPath() {
184
- // Sibling package in the same monorepo. Walk up from this file looking for
185
- // packages/openclaw with an openclaw.plugin.json manifest.
186
- let here = path.dirname(fileURLToPath(import.meta.url));
187
- for (let i = 0; i < 6; i++) {
188
- const candidate = path.resolve(here, '..', 'openclaw');
189
- if (await fileExists(path.join(candidate, 'openclaw.plugin.json'))) {
190
- return candidate;
191
- }
192
- here = path.dirname(here);
193
- }
194
- // Fallback: try to resolve via node_modules of the global install.
195
- const npmRoot = run('npm', ['root', '-g']);
196
- if (npmRoot.code === 0) {
197
- const guess = path.join(npmRoot.stdout, '@oked', 'openclaw');
198
- if (await fileExists(path.join(guess, 'openclaw.plugin.json'))) return guess;
199
- }
200
- return null;
201
- }
202
-
203
- // --- daemon detection -----------------------------------------------------
204
-
205
- function detectDaemon() {
206
- if (commandExists('systemctl')) {
207
- const userUnits = run('systemctl', ['--user', 'list-units', '--type=service', '--no-legend', '--plain']);
208
- if (userUnits.code === 0) {
209
- const line = userUnits.stdout.split('\n').find((l) => /openclaw/i.test(l));
210
- if (line) {
211
- const unit = line.trim().split(/\s+/)[0];
212
- return {
213
- kind: 'systemd-user',
214
- unit,
215
- label: `systemd --user unit ${unit}`,
216
- restartCmd: ['systemctl', ['--user', 'restart', unit]],
217
- };
218
- }
219
- }
220
- const sysUnits = run('systemctl', ['list-units', '--type=service', '--no-legend', '--plain']);
221
- if (sysUnits.code === 0) {
222
- const line = sysUnits.stdout.split('\n').find((l) => /openclaw/i.test(l));
223
- if (line) {
224
- const unit = line.trim().split(/\s+/)[0];
225
- return {
226
- kind: 'systemd-system',
227
- unit,
228
- label: `systemd unit ${unit} (sudo required)`,
229
- restartCmd: ['sudo', ['systemctl', 'restart', unit]],
230
- };
231
- }
232
- }
233
- }
234
- if (commandExists('pm2')) {
235
- const list = run('pm2', ['jlist']);
236
- if (list.code === 0 && /openclaw/i.test(list.stdout)) {
237
- try {
238
- const procs = JSON.parse(list.stdout);
239
- const proc = procs.find((p) => /openclaw/i.test(p.name || ''));
240
- if (proc) return { kind: 'pm2', label: `pm2 process ${proc.name}`, restartCmd: ['pm2', ['restart', proc.name]] };
241
- } catch { /* ignore */ }
242
- }
243
- }
244
- if (platform() === 'darwin' && commandExists('launchctl')) {
245
- const list = run('launchctl', ['list']);
246
- if (list.code === 0) {
247
- const line = list.stdout.split('\n').find((l) => /openclaw/i.test(l));
248
- if (line) {
249
- const label = line.trim().split(/\s+/).pop();
250
- return {
251
- kind: 'launchd',
252
- label: `launchd job ${label}`,
253
- restartCmd: ['launchctl', ['kickstart', '-k', `gui/${process.getuid()}/${label}`]],
254
- };
255
- }
256
- }
257
- }
258
- if (commandExists('pgrep')) {
259
- const pg = run('pgrep', ['-af', 'openclaw']);
260
- if (pg.code === 0 && pg.stdout) {
261
- const lines = pg.stdout.split('\n').filter(Boolean);
262
- const pids = lines.map((l) => l.split(/\s+/)[0]);
263
- return {
264
- kind: 'process',
265
- label: `${pids.length} bare process(es): ${pids.join(', ')}`,
266
- restartCmd: null,
267
- pids,
268
- commands: lines.map((l) => l.split(/\s+/).slice(1).join(' ')),
269
- };
270
- }
271
- }
272
- return null;
273
- }
274
-
275
- // --- commands -------------------------------------------------------------
276
-
277
- async function cmdInit() {
278
- console.log('\nOKed -> OpenClaw plugin installer\n');
279
-
280
- if (!commandExists('openclaw')) {
281
- console.error('The "openclaw" command was not found on your PATH.');
282
- console.error('Install OpenClaw first, then re-run "oked-openclaw init".');
283
- process.exit(1);
284
- }
285
-
286
- // 1. Find the plugin source.
287
- process.stdout.write('1. Locating @oked/openclaw plugin... ');
288
- const pluginPath = await findPluginPath();
289
- if (!pluginPath) {
290
- console.log('failed');
291
- console.error('\nCould not find a sibling packages/openclaw with openclaw.plugin.json.');
292
- console.error('If you cloned the monorepo, run this from inside it. Otherwise install');
293
- console.error('@oked/openclaw globally first: npm install -g @oked/openclaw\n');
294
- process.exit(1);
295
- }
296
- console.log('found');
297
- console.log(` Path: ${pluginPath}\n`);
298
-
299
- // 2. Register the plugin with OpenClaw.
300
- console.log('2. Installing into OpenClaw...');
301
- const installArgs = ['plugins', 'install', '--link', pluginPath];
302
- console.log(` $ openclaw ${installArgs.join(' ')}`);
303
- const installCode = await runStreaming('openclaw', installArgs);
304
- if (installCode !== 0) {
305
- console.error(`\n X openclaw plugins install exited ${installCode}.`);
306
- process.exit(installCode);
307
- }
308
- console.log('');
309
-
310
- // 3. API key - reuse existing if present, otherwise pair this device.
311
- const existing = await readConfig();
312
- const existingEntry = existing?.plugins?.entries?.oked || {};
313
- let apiKey = process.env.OKED_API_KEY || existingEntry.apiKey || '';
314
- if (apiKey) {
315
- console.log(`3. Using existing API key: ${maskKey(apiKey)}`);
316
- } else {
317
- console.log('3. Pairing this device with OKed...');
318
- try {
319
- apiKey = await deviceCodePair();
320
- console.log(` Paired. API key: ${maskKey(apiKey)}`);
321
- await writeOkedConfig(apiKey, DEFAULT_BACKEND_URL);
322
- console.log(` Saved to ${OKED_CONFIG}`);
323
- } catch (err) {
324
- console.error(` ${err.message}`);
325
- process.exit(1);
326
- }
327
- }
328
-
329
- let minTier = process.env.OKED_MIN_TIER || existingEntry.minTier || '';
330
- if (!minTier && process.stdin.isTTY) {
331
- const a = await prompt(` minTier (review|warning|high_stakes) [review]: `);
332
- minTier = (a || 'review').toLowerCase();
333
- }
334
- if (!['review', 'warning', 'high_stakes', 'safe'].includes(minTier)) {
335
- minTier = 'review';
336
- }
337
-
338
- // 4. Update openclaw.json - apiKey/minTier are now declared in the plugin's
339
- // configSchema, so OpenClaw 2026.5+ will accept them under entries.oked.
340
- process.stdout.write('4. Updating ~/.openclaw/openclaw.json... ');
341
- const cfg = existing && typeof existing === 'object' ? existing : {};
342
- cfg.plugins = cfg.plugins || {};
343
- const allow = Array.isArray(cfg.plugins.allow) ? cfg.plugins.allow : [];
344
- if (!allow.includes('oked')) allow.push('oked');
345
- cfg.plugins.allow = allow;
346
- cfg.plugins.entries = cfg.plugins.entries || {};
347
- cfg.plugins.entries.oked = {
348
- ...(cfg.plugins.entries.oked || {}),
349
- enabled: true,
350
- apiKey,
351
- backendUrl: DEFAULT_BACKEND_URL,
352
- minTier,
353
- };
354
- await writeConfig(cfg);
355
- console.log('done');
356
- console.log(` File: ${OPENCLAW_CONFIG}\n`);
357
-
358
- // 5. Restart the daemon.
359
- console.log('5. Detecting OpenClaw process...');
360
- const daemon = detectDaemon();
361
- if (!daemon) {
362
- console.log(' No running OpenClaw process found.');
363
- console.log(' Start (or restart) OpenClaw the way you normally do, then run "oked-openclaw test".\n');
364
- return;
365
- }
366
- console.log(` Found: ${daemon.label}`);
367
-
368
- if (!daemon.restartCmd) {
369
- console.log('\n Bare processes can be stopped but not safely auto-relaunched.');
370
- if (daemon.commands?.length) {
371
- console.log(' Commands currently running:');
372
- daemon.commands.forEach((c) => console.log(` ${c}`));
373
- }
374
- const ok = await confirm(' Send SIGTERM to the process(es) above?', false);
375
- if (ok) {
376
- for (const pid of daemon.pids) {
377
- const r = run('kill', [pid]);
378
- console.log(` kill ${pid}: ${r.code === 0 ? 'sent' : `failed (${r.stderr})`}`);
379
- }
380
- console.log('\n Now relaunch OpenClaw the same way you started it, then run "oked-openclaw test".\n');
381
- } else {
382
- console.log(' Skipped. Restart OpenClaw manually, then run "oked-openclaw test".\n');
383
- }
384
- return;
385
- }
386
-
387
- const [cmd, args] = daemon.restartCmd;
388
- const cmdline = `${cmd} ${args.join(' ')}`;
389
- const ok = await confirm(`\n Run: ${cmdline} ?`, true);
390
- if (!ok) {
391
- console.log(` Skipped. To restart manually: ${cmdline}\n`);
392
- return;
393
- }
394
- console.log(`\n $ ${cmdline}`);
395
- const code = await runStreaming(cmd, args);
396
- if (code === 0) {
397
- console.log('\n OK Restart command exited 0.');
398
- console.log(' Run "oked-openclaw test" or trigger a real OpenClaw tool call to verify.\n');
399
- } else {
400
- console.log(`\n X Restart command exited ${code}. Restart OpenClaw manually and try again.\n`);
401
- process.exit(code);
402
- }
403
- }
404
-
405
- async function cmdStatus() {
406
- const cfg = await readConfig().catch(() => ({}));
407
- const entry = cfg?.plugins?.entries?.oked || {};
408
- const allowed = Array.isArray(cfg?.plugins?.allow) && cfg.plugins.allow.includes('oked');
409
- const apiKey = entry.apiKey || process.env.OKED_API_KEY || '';
410
- const backendUrl = entry.backendUrl || process.env.OKED_BACKEND_URL || DEFAULT_BACKEND_URL;
411
-
412
- console.log(`Config file : ${OPENCLAW_CONFIG}`);
413
- console.log(`Plugin allowed: ${allowed ? 'yes' : 'no'}`);
414
- console.log(`Plugin enabled: ${entry.enabled ? 'yes' : 'no'}`);
415
- console.log(`API key : ${maskKey(apiKey)}`);
416
- console.log(`minTier : ${entry.minTier || '(default review)'}`);
417
-
418
- if (commandExists('openclaw')) {
419
- const r = run('openclaw', ['plugins', 'list']);
420
- const installed = /\boked\b/.test(r.stdout) && !/\boked\b.*not\s+found/i.test(r.stdout);
421
- console.log(`Plugin installed: ${installed ? 'yes' : 'no - run "oked-openclaw init"'}`);
422
- }
423
-
424
- const daemon = detectDaemon();
425
- console.log(`Daemon : ${daemon ? daemon.label : 'not detected'}`);
426
-
427
- const client = new OKedClient({ apiKey, backendUrl });
428
- console.log(`Backend URL : ${client.backendUrl}`);
429
- try {
430
- const ok = await client.ping();
431
- console.log(`Backend reach : ${ok ? 'ok' : 'unreachable'}`);
432
- } catch (err) {
433
- console.log(`Backend reach : error - ${err.message}`);
434
- }
435
- }
436
-
437
- async function cmdTest() {
438
- const cfg = await readConfig().catch(() => ({}));
439
- const entry = cfg?.plugins?.entries?.oked || {};
440
- const apiKey = entry.apiKey || process.env.OKED_API_KEY || '';
441
- const backendUrl = entry.backendUrl || process.env.OKED_BACKEND_URL || DEFAULT_BACKEND_URL;
442
-
443
- if (!apiKey) {
444
- console.error('No API key found. Run "oked-openclaw init" first.');
445
- process.exit(1);
446
- }
447
-
448
- const client = new OKedClient({ apiKey, backendUrl });
449
- console.log('');
450
- console.log('Sending test approval request...');
451
- console.log(` Dashboard: ${client.backendUrl}/dashboard`);
452
- console.log('');
453
- console.log(' Approve or deny it from your dashboard or Telegram.');
454
- console.log(' Waiting...');
455
- console.log('');
456
-
457
- try {
458
- const result = await client.approve({
459
- action: 'oked-openclaw-test',
460
- description: 'Test approval from "oked-openclaw test" - approve or deny to verify your setup works.',
461
- tier: 'high_stakes',
462
- session_id: `openclaw-test-${Date.now()}`,
463
- cwd: process.cwd(),
464
- });
465
- if (result.approved) {
466
- console.log(' OK Approved! OKed is working end-to-end.\n');
467
- console.log(' Note: this only verifies the SDK + backend + Telegram path.');
468
- console.log(' Trigger a real OpenClaw tool call to confirm the plugin is loaded.\n');
469
- } else {
470
- console.log(` X ${result.decision}. OKed is working - the request was ${result.decision}.\n`);
471
- }
472
- } catch (err) {
473
- console.error(` Error: ${err.message}\n`);
474
- process.exit(1);
475
- }
476
- }
477
-
478
- async function cmdUninstall() {
479
- let cfg;
480
- try {
481
- await access(OPENCLAW_CONFIG);
482
- cfg = await readConfig();
483
- } catch {
484
- console.log('No openclaw.json found - nothing to remove.');
485
- }
486
-
487
- if (cfg) {
488
- let changed = false;
489
- if (Array.isArray(cfg?.plugins?.allow)) {
490
- const filtered = cfg.plugins.allow.filter((n) => n !== 'oked');
491
- if (filtered.length !== cfg.plugins.allow.length) {
492
- cfg.plugins.allow = filtered;
493
- changed = true;
494
- }
495
- }
496
- if (cfg?.plugins?.entries?.oked) {
497
- delete cfg.plugins.entries.oked;
498
- changed = true;
499
- }
500
- if (changed) {
501
- await writeConfig(cfg);
502
- console.log(`OK Removed OKed entry from ${OPENCLAW_CONFIG}`);
503
- } else {
504
- console.log('No OKed entry was present in openclaw.json.');
505
- }
506
- }
507
-
508
- if (commandExists('openclaw')) {
509
- console.log('Running: openclaw plugins uninstall oked');
510
- await runStreaming('openclaw', ['plugins', 'uninstall', 'oked']);
511
- }
512
- console.log(' Restart the OpenClaw daemon to fully unload the plugin.');
513
- }
514
-
515
- function usage() {
516
- console.log(`oked-openclaw - installer for the @oked/openclaw plugin
517
-
518
- Usage:
519
- oked-openclaw init Install the plugin and configure openclaw.json
520
- oked-openclaw status Show install state + backend reachability
521
- oked-openclaw uninstall Remove the OKed entry and uninstall the plugin
522
- oked-openclaw test Send a test approval request via the OKed SDK
523
- oked-openclaw help Show this message
524
- `);
525
- }
526
-
527
- async function main() {
528
- const cmd = process.argv[2];
529
- switch (cmd) {
530
- case 'init':
531
- await cmdInit();
532
- break;
533
- case 'status':
534
- await cmdStatus();
535
- break;
536
- case 'uninstall':
537
- await cmdUninstall();
538
- break;
539
- case 'test':
540
- await cmdTest();
541
- break;
542
- case 'help':
543
- case '--help':
544
- case '-h':
545
- case undefined:
546
- usage();
547
- break;
548
- default:
549
- console.error(`Unknown command: ${cmd}`);
550
- usage();
551
- process.exit(1);
552
- }
553
- }
554
-
555
- main().catch((err) => {
556
- console.error(`oked-openclaw: ${err.message}`);
557
- process.exit(1);
558
- });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @oked/openclaw-cli - installer for the @oked/openclaw plugin.
4
+ *
5
+ * Subcommands:
6
+ * oked-openclaw init Install the plugin via `openclaw plugins install`,
7
+ * persist API key + minTier into ~/.openclaw/openclaw.json,
8
+ * and restart the OpenClaw gateway daemon.
9
+ * oked-openclaw status Print install state, config, and backend reachability.
10
+ * oked-openclaw test Send a test approval request to verify SDK + Telegram.
11
+ * oked-openclaw uninstall Remove the OKed entry from openclaw.json and uninstall the plugin.
12
+ */
13
+
14
+ import { readFile, writeFile, mkdir, access, chmod } from 'node:fs/promises';
15
+ import { homedir, hostname, platform } from 'node:os';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import readline from 'node:readline/promises';
19
+ import { stdin as input, stdout as output } from 'node:process';
20
+ import { spawn, spawnSync } from 'node:child_process';
21
+
22
+ import { OKedClient } from '@oked/sdk';
23
+
24
+ const OPENCLAW_DIR = path.join(homedir(), '.openclaw');
25
+ const OPENCLAW_CONFIG = path.join(OPENCLAW_DIR, 'openclaw.json');
26
+ const OKED_DIR = path.join(homedir(), '.oked');
27
+ const OKED_CONFIG = path.join(OKED_DIR, 'config.json');
28
+ const DEFAULT_BACKEND_URL = process.env.OKED_BACKEND_URL || 'https://api.oked.ai';
29
+ const CLIENT_VERSION = '0.1.0';
30
+
31
+ async function chmodOwnerOnly(file) {
32
+ try { await chmod(file, 0o600); } catch { /* Windows */ }
33
+ }
34
+
35
+ async function writeOkedConfig(apiKey, backendUrl) {
36
+ await mkdir(OKED_DIR, { recursive: true });
37
+ let existing = {};
38
+ try {
39
+ existing = JSON.parse(await readFile(OKED_CONFIG, 'utf8')) || {};
40
+ if (typeof existing !== 'object') existing = {};
41
+ } catch {
42
+ existing = {};
43
+ }
44
+ const payload = { ...existing, apiKey, backendUrl };
45
+ await writeFile(OKED_CONFIG, JSON.stringify(payload, null, 2) + '\n', 'utf8');
46
+ await chmodOwnerOnly(OKED_CONFIG);
47
+ }
48
+
49
+ function openBrowser(url) {
50
+ const plat = platform();
51
+ let cmd;
52
+ let args;
53
+ if (plat === 'darwin') {
54
+ cmd = 'open'; args = [url];
55
+ } else if (plat === 'win32') {
56
+ cmd = 'cmd'; args = ['/c', 'start', '', url];
57
+ } else {
58
+ cmd = 'xdg-open'; args = [url];
59
+ }
60
+ try {
61
+ spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
62
+ } catch { /* best effort */ }
63
+ }
64
+
65
+ async function deviceCodePair() {
66
+ const codeRes = await fetch(`${DEFAULT_BACKEND_URL}/api/v1/device/code`, {
67
+ method: 'POST',
68
+ headers: { 'Content-Type': 'application/json' },
69
+ body: JSON.stringify({
70
+ client_type: 'openclaw',
71
+ hostname: hostname(),
72
+ client_version: CLIENT_VERSION,
73
+ }),
74
+ });
75
+ if (!codeRes.ok) {
76
+ throw new Error(`Pairing request failed: ${codeRes.status} ${await codeRes.text()}`);
77
+ }
78
+ const code = await codeRes.json();
79
+
80
+ console.log('');
81
+ console.log(' To pair this device, open in your browser:');
82
+ console.log(` ${code.verification_uri_complete}`);
83
+ console.log('');
84
+ console.log(` Or visit ${code.verification_uri} and enter the code:`);
85
+ console.log(` ${code.user_code}`);
86
+ console.log('');
87
+ console.log(' Waiting for confirmation...');
88
+
89
+ openBrowser(code.verification_uri_complete);
90
+
91
+ const deadline = Date.now() + (code.expires_in || 600) * 1000;
92
+ const intervalMs = Math.max(1, code.interval || 3) * 1000;
93
+ while (Date.now() < deadline) {
94
+ await new Promise((r) => setTimeout(r, intervalMs));
95
+ const pollRes = await fetch(`${DEFAULT_BACKEND_URL}/api/v1/device/poll`, {
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/json' },
98
+ body: JSON.stringify({ device_code: code.device_code }),
99
+ });
100
+ if (pollRes.status === 410) {
101
+ throw new Error('Pairing code expired. Run init again.');
102
+ }
103
+ if (!pollRes.ok) continue;
104
+ const body = await pollRes.json();
105
+ if (body.status === 'approved' && body.api_key) {
106
+ return body.api_key;
107
+ }
108
+ }
109
+ throw new Error('Pairing timed out. Run init again.');
110
+ }
111
+
112
+ // --- helpers --------------------------------------------------------------
113
+
114
+ function maskKey(key) {
115
+ if (!key) return '(not set)';
116
+ if (key.length <= 8) return '****';
117
+ return `${key.slice(0, 4)}...${key.slice(-4)}`;
118
+ }
119
+
120
+ function run(cmd, args, opts = {}) {
121
+ const r = spawnSync(cmd, args, { encoding: 'utf8', ...opts });
122
+ return {
123
+ code: r.status ?? -1,
124
+ stdout: (r.stdout || '').trim(),
125
+ stderr: (r.stderr || '').trim(),
126
+ error: r.error,
127
+ };
128
+ }
129
+
130
+ function commandExists(cmd) {
131
+ const probe = platform() === 'win32' ? 'where' : 'which';
132
+ return run(probe, [cmd]).code === 0;
133
+ }
134
+
135
+ async function fileExists(p) {
136
+ try { await access(p); return true; } catch { return false; }
137
+ }
138
+
139
+ async function readConfig() {
140
+ try {
141
+ const raw = await readFile(OPENCLAW_CONFIG, 'utf8');
142
+ return JSON.parse(raw);
143
+ } catch (err) {
144
+ if (err.code === 'ENOENT') return {};
145
+ throw new Error(`Could not parse ${OPENCLAW_CONFIG}: ${err.message}`);
146
+ }
147
+ }
148
+
149
+ async function writeConfig(cfg) {
150
+ await mkdir(OPENCLAW_DIR, { recursive: true });
151
+ await writeFile(OPENCLAW_CONFIG, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
152
+ await chmodOwnerOnly(OPENCLAW_CONFIG);
153
+ }
154
+
155
+ async function prompt(question) {
156
+ if (!process.stdin.isTTY) return '';
157
+ const rl = readline.createInterface({ input, output });
158
+ const a = await rl.question(question);
159
+ rl.close();
160
+ return a.trim();
161
+ }
162
+
163
+ async function confirm(question, defaultYes = true) {
164
+ const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
165
+ const a = (await prompt(question + suffix)).toLowerCase();
166
+ if (!a) return defaultYes;
167
+ return a === 'y' || a === 'yes';
168
+ }
169
+
170
+ function runStreaming(cmd, args) {
171
+ return new Promise((resolve) => {
172
+ const child = spawn(cmd, args, { stdio: 'inherit' });
173
+ child.on('close', (code) => resolve(code ?? -1));
174
+ child.on('error', (err) => {
175
+ console.error(` Failed to spawn ${cmd}: ${err.message}`);
176
+ resolve(-1);
177
+ });
178
+ });
179
+ }
180
+
181
+ // --- plugin path discovery ------------------------------------------------
182
+
183
+ async function findPluginPath() {
184
+ // Sibling package in the same monorepo. Walk up from this file looking for
185
+ // packages/openclaw with an openclaw.plugin.json manifest.
186
+ let here = path.dirname(fileURLToPath(import.meta.url));
187
+ for (let i = 0; i < 6; i++) {
188
+ const candidate = path.resolve(here, '..', 'openclaw');
189
+ if (await fileExists(path.join(candidate, 'openclaw.plugin.json'))) {
190
+ return candidate;
191
+ }
192
+ here = path.dirname(here);
193
+ }
194
+ // Fallback: try to resolve via node_modules of the global install.
195
+ const npmRoot = run('npm', ['root', '-g']);
196
+ if (npmRoot.code === 0) {
197
+ const guess = path.join(npmRoot.stdout, '@oked', 'openclaw');
198
+ if (await fileExists(path.join(guess, 'openclaw.plugin.json'))) return guess;
199
+ }
200
+ return null;
201
+ }
202
+
203
+ // --- daemon detection -----------------------------------------------------
204
+
205
+ function detectDaemon() {
206
+ if (commandExists('systemctl')) {
207
+ const userUnits = run('systemctl', ['--user', 'list-units', '--type=service', '--no-legend', '--plain']);
208
+ if (userUnits.code === 0) {
209
+ const line = userUnits.stdout.split('\n').find((l) => /openclaw/i.test(l));
210
+ if (line) {
211
+ const unit = line.trim().split(/\s+/)[0];
212
+ return {
213
+ kind: 'systemd-user',
214
+ unit,
215
+ label: `systemd --user unit ${unit}`,
216
+ restartCmd: ['systemctl', ['--user', 'restart', unit]],
217
+ };
218
+ }
219
+ }
220
+ const sysUnits = run('systemctl', ['list-units', '--type=service', '--no-legend', '--plain']);
221
+ if (sysUnits.code === 0) {
222
+ const line = sysUnits.stdout.split('\n').find((l) => /openclaw/i.test(l));
223
+ if (line) {
224
+ const unit = line.trim().split(/\s+/)[0];
225
+ return {
226
+ kind: 'systemd-system',
227
+ unit,
228
+ label: `systemd unit ${unit} (sudo required)`,
229
+ restartCmd: ['sudo', ['systemctl', 'restart', unit]],
230
+ };
231
+ }
232
+ }
233
+ }
234
+ if (commandExists('pm2')) {
235
+ const list = run('pm2', ['jlist']);
236
+ if (list.code === 0 && /openclaw/i.test(list.stdout)) {
237
+ try {
238
+ const procs = JSON.parse(list.stdout);
239
+ const proc = procs.find((p) => /openclaw/i.test(p.name || ''));
240
+ if (proc) return { kind: 'pm2', label: `pm2 process ${proc.name}`, restartCmd: ['pm2', ['restart', proc.name]] };
241
+ } catch { /* ignore */ }
242
+ }
243
+ }
244
+ if (platform() === 'darwin' && commandExists('launchctl')) {
245
+ const list = run('launchctl', ['list']);
246
+ if (list.code === 0) {
247
+ const line = list.stdout.split('\n').find((l) => /openclaw/i.test(l));
248
+ if (line) {
249
+ const label = line.trim().split(/\s+/).pop();
250
+ return {
251
+ kind: 'launchd',
252
+ label: `launchd job ${label}`,
253
+ restartCmd: ['launchctl', ['kickstart', '-k', `gui/${process.getuid()}/${label}`]],
254
+ };
255
+ }
256
+ }
257
+ }
258
+ if (commandExists('pgrep')) {
259
+ const pg = run('pgrep', ['-af', 'openclaw']);
260
+ if (pg.code === 0 && pg.stdout) {
261
+ const lines = pg.stdout.split('\n').filter(Boolean);
262
+ const pids = lines.map((l) => l.split(/\s+/)[0]);
263
+ return {
264
+ kind: 'process',
265
+ label: `${pids.length} bare process(es): ${pids.join(', ')}`,
266
+ restartCmd: null,
267
+ pids,
268
+ commands: lines.map((l) => l.split(/\s+/).slice(1).join(' ')),
269
+ };
270
+ }
271
+ }
272
+ return null;
273
+ }
274
+
275
+ // --- commands -------------------------------------------------------------
276
+
277
+ async function cmdInit() {
278
+ console.log('\nOKed -> OpenClaw plugin installer\n');
279
+
280
+ if (!commandExists('openclaw')) {
281
+ console.error('The "openclaw" command was not found on your PATH.');
282
+ console.error('Install OpenClaw first, then re-run "oked-openclaw init".');
283
+ process.exit(1);
284
+ }
285
+
286
+ // 1. Find the plugin source.
287
+ process.stdout.write('1. Locating @oked/openclaw plugin... ');
288
+ const pluginPath = await findPluginPath();
289
+ if (!pluginPath) {
290
+ console.log('failed');
291
+ console.error('\nCould not find a sibling packages/openclaw with openclaw.plugin.json.');
292
+ console.error('If you cloned the monorepo, run this from inside it. Otherwise install');
293
+ console.error('@oked/openclaw globally first: npm install -g @oked/openclaw\n');
294
+ process.exit(1);
295
+ }
296
+ console.log('found');
297
+ console.log(` Path: ${pluginPath}\n`);
298
+
299
+ // 2. Register the plugin with OpenClaw.
300
+ console.log('2. Installing into OpenClaw...');
301
+ const installArgs = ['plugins', 'install', '--link', pluginPath];
302
+ console.log(` $ openclaw ${installArgs.join(' ')}`);
303
+ const installCode = await runStreaming('openclaw', installArgs);
304
+ if (installCode !== 0) {
305
+ console.error(`\n X openclaw plugins install exited ${installCode}.`);
306
+ process.exit(installCode);
307
+ }
308
+ console.log('');
309
+
310
+ // 3. API key - reuse existing if present, otherwise pair this device.
311
+ const existing = await readConfig();
312
+ const existingEntry = existing?.plugins?.entries?.oked || {};
313
+ let apiKey = process.env.OKED_API_KEY || existingEntry.apiKey || '';
314
+ if (apiKey) {
315
+ console.log(`3. Using existing API key: ${maskKey(apiKey)}`);
316
+ } else {
317
+ console.log('3. Pairing this device with OKed...');
318
+ try {
319
+ apiKey = await deviceCodePair();
320
+ console.log(` Paired. API key: ${maskKey(apiKey)}`);
321
+ await writeOkedConfig(apiKey, DEFAULT_BACKEND_URL);
322
+ console.log(` Saved to ${OKED_CONFIG}`);
323
+ } catch (err) {
324
+ console.error(` ${err.message}`);
325
+ process.exit(1);
326
+ }
327
+ }
328
+
329
+ let minTier = process.env.OKED_MIN_TIER || existingEntry.minTier || '';
330
+ if (!minTier && process.stdin.isTTY) {
331
+ const a = await prompt(` minTier (review|warning|high_stakes) [review]: `);
332
+ minTier = (a || 'review').toLowerCase();
333
+ }
334
+ if (!['review', 'warning', 'high_stakes', 'safe'].includes(minTier)) {
335
+ minTier = 'review';
336
+ }
337
+
338
+ // 4. Update openclaw.json - apiKey/minTier are now declared in the plugin's
339
+ // configSchema, so OpenClaw 2026.5+ will accept them under entries.oked.
340
+ process.stdout.write('4. Updating ~/.openclaw/openclaw.json... ');
341
+ const cfg = existing && typeof existing === 'object' ? existing : {};
342
+ cfg.plugins = cfg.plugins || {};
343
+ const allow = Array.isArray(cfg.plugins.allow) ? cfg.plugins.allow : [];
344
+ if (!allow.includes('oked')) allow.push('oked');
345
+ cfg.plugins.allow = allow;
346
+ cfg.plugins.entries = cfg.plugins.entries || {};
347
+ cfg.plugins.entries.oked = {
348
+ ...(cfg.plugins.entries.oked || {}),
349
+ enabled: true,
350
+ apiKey,
351
+ backendUrl: DEFAULT_BACKEND_URL,
352
+ minTier,
353
+ };
354
+ await writeConfig(cfg);
355
+ console.log('done');
356
+ console.log(` File: ${OPENCLAW_CONFIG}\n`);
357
+
358
+ // 5. Restart the daemon.
359
+ console.log('5. Detecting OpenClaw process...');
360
+ const daemon = detectDaemon();
361
+ if (!daemon) {
362
+ console.log(' No running OpenClaw process found.');
363
+ console.log(' Start (or restart) OpenClaw the way you normally do, then run "oked-openclaw test".\n');
364
+ return;
365
+ }
366
+ console.log(` Found: ${daemon.label}`);
367
+
368
+ if (!daemon.restartCmd) {
369
+ console.log('\n Bare processes can be stopped but not safely auto-relaunched.');
370
+ if (daemon.commands?.length) {
371
+ console.log(' Commands currently running:');
372
+ daemon.commands.forEach((c) => console.log(` ${c}`));
373
+ }
374
+ const ok = await confirm(' Send SIGTERM to the process(es) above?', false);
375
+ if (ok) {
376
+ for (const pid of daemon.pids) {
377
+ const r = run('kill', [pid]);
378
+ console.log(` kill ${pid}: ${r.code === 0 ? 'sent' : `failed (${r.stderr})`}`);
379
+ }
380
+ console.log('\n Now relaunch OpenClaw the same way you started it, then run "oked-openclaw test".\n');
381
+ } else {
382
+ console.log(' Skipped. Restart OpenClaw manually, then run "oked-openclaw test".\n');
383
+ }
384
+ return;
385
+ }
386
+
387
+ const [cmd, args] = daemon.restartCmd;
388
+ const cmdline = `${cmd} ${args.join(' ')}`;
389
+ const ok = await confirm(`\n Run: ${cmdline} ?`, true);
390
+ if (!ok) {
391
+ console.log(` Skipped. To restart manually: ${cmdline}\n`);
392
+ return;
393
+ }
394
+ console.log(`\n $ ${cmdline}`);
395
+ const code = await runStreaming(cmd, args);
396
+ if (code === 0) {
397
+ console.log('\n OK Restart command exited 0.');
398
+ console.log(' Run "oked-openclaw test" or trigger a real OpenClaw tool call to verify.\n');
399
+ } else {
400
+ console.log(`\n X Restart command exited ${code}. Restart OpenClaw manually and try again.\n`);
401
+ process.exit(code);
402
+ }
403
+ }
404
+
405
+ async function cmdStatus() {
406
+ const cfg = await readConfig().catch(() => ({}));
407
+ const entry = cfg?.plugins?.entries?.oked || {};
408
+ const allowed = Array.isArray(cfg?.plugins?.allow) && cfg.plugins.allow.includes('oked');
409
+ const apiKey = entry.apiKey || process.env.OKED_API_KEY || '';
410
+ const backendUrl = entry.backendUrl || process.env.OKED_BACKEND_URL || DEFAULT_BACKEND_URL;
411
+
412
+ console.log(`Config file : ${OPENCLAW_CONFIG}`);
413
+ console.log(`Plugin allowed: ${allowed ? 'yes' : 'no'}`);
414
+ console.log(`Plugin enabled: ${entry.enabled ? 'yes' : 'no'}`);
415
+ console.log(`API key : ${maskKey(apiKey)}`);
416
+ console.log(`minTier : ${entry.minTier || '(default review)'}`);
417
+
418
+ if (commandExists('openclaw')) {
419
+ const r = run('openclaw', ['plugins', 'list']);
420
+ const installed = /\boked\b/.test(r.stdout) && !/\boked\b.*not\s+found/i.test(r.stdout);
421
+ console.log(`Plugin installed: ${installed ? 'yes' : 'no - run "oked-openclaw init"'}`);
422
+ }
423
+
424
+ const daemon = detectDaemon();
425
+ console.log(`Daemon : ${daemon ? daemon.label : 'not detected'}`);
426
+
427
+ const client = new OKedClient({ apiKey, backendUrl });
428
+ console.log(`Backend URL : ${client.backendUrl}`);
429
+ try {
430
+ const ok = await client.ping();
431
+ console.log(`Backend reach : ${ok ? 'ok' : 'unreachable'}`);
432
+ } catch (err) {
433
+ console.log(`Backend reach : error - ${err.message}`);
434
+ }
435
+ }
436
+
437
+ async function cmdTest() {
438
+ const cfg = await readConfig().catch(() => ({}));
439
+ const entry = cfg?.plugins?.entries?.oked || {};
440
+ const apiKey = entry.apiKey || process.env.OKED_API_KEY || '';
441
+ const backendUrl = entry.backendUrl || process.env.OKED_BACKEND_URL || DEFAULT_BACKEND_URL;
442
+
443
+ if (!apiKey) {
444
+ console.error('No API key found. Run "oked-openclaw init" first.');
445
+ process.exit(1);
446
+ }
447
+
448
+ const client = new OKedClient({ apiKey, backendUrl });
449
+ console.log('');
450
+ console.log('Sending test approval request...');
451
+ console.log(` Dashboard: ${client.backendUrl}/dashboard`);
452
+ console.log('');
453
+ console.log(' Approve or deny it from your dashboard or Telegram.');
454
+ console.log(' Waiting...');
455
+ console.log('');
456
+
457
+ try {
458
+ const result = await client.approve({
459
+ action: 'oked-openclaw-test',
460
+ description: 'Test approval from "oked-openclaw test" - approve or deny to verify your setup works.',
461
+ tier: 'high_stakes',
462
+ session_id: `openclaw-test-${Date.now()}`,
463
+ cwd: process.cwd(),
464
+ });
465
+ if (result.approved) {
466
+ console.log(' OK Approved! OKed is working end-to-end.\n');
467
+ console.log(' Note: this only verifies the SDK + backend + Telegram path.');
468
+ console.log(' Trigger a real OpenClaw tool call to confirm the plugin is loaded.\n');
469
+ } else {
470
+ console.log(` X ${result.decision}. OKed is working - the request was ${result.decision}.\n`);
471
+ }
472
+ } catch (err) {
473
+ console.error(` Error: ${err.message}\n`);
474
+ process.exit(1);
475
+ }
476
+ }
477
+
478
+ async function cmdUninstall() {
479
+ let cfg;
480
+ try {
481
+ await access(OPENCLAW_CONFIG);
482
+ cfg = await readConfig();
483
+ } catch {
484
+ console.log('No openclaw.json found - nothing to remove.');
485
+ }
486
+
487
+ if (cfg) {
488
+ let changed = false;
489
+ if (Array.isArray(cfg?.plugins?.allow)) {
490
+ const filtered = cfg.plugins.allow.filter((n) => n !== 'oked');
491
+ if (filtered.length !== cfg.plugins.allow.length) {
492
+ cfg.plugins.allow = filtered;
493
+ changed = true;
494
+ }
495
+ }
496
+ if (cfg?.plugins?.entries?.oked) {
497
+ delete cfg.plugins.entries.oked;
498
+ changed = true;
499
+ }
500
+ if (changed) {
501
+ await writeConfig(cfg);
502
+ console.log(`OK Removed OKed entry from ${OPENCLAW_CONFIG}`);
503
+ } else {
504
+ console.log('No OKed entry was present in openclaw.json.');
505
+ }
506
+ }
507
+
508
+ if (commandExists('openclaw')) {
509
+ console.log('Running: openclaw plugins uninstall oked');
510
+ await runStreaming('openclaw', ['plugins', 'uninstall', 'oked']);
511
+ }
512
+ console.log(' Restart the OpenClaw daemon to fully unload the plugin.');
513
+ }
514
+
515
+ function usage() {
516
+ console.log(`oked-openclaw - installer for the @oked/openclaw plugin
517
+
518
+ Usage:
519
+ oked-openclaw init Install the plugin and configure openclaw.json
520
+ oked-openclaw status Show install state + backend reachability
521
+ oked-openclaw uninstall Remove the OKed entry and uninstall the plugin
522
+ oked-openclaw test Send a test approval request via the OKed SDK
523
+ oked-openclaw help Show this message
524
+ `);
525
+ }
526
+
527
+ async function main() {
528
+ const cmd = process.argv[2];
529
+ switch (cmd) {
530
+ case 'init':
531
+ await cmdInit();
532
+ break;
533
+ case 'status':
534
+ await cmdStatus();
535
+ break;
536
+ case 'uninstall':
537
+ await cmdUninstall();
538
+ break;
539
+ case 'test':
540
+ await cmdTest();
541
+ break;
542
+ case 'help':
543
+ case '--help':
544
+ case '-h':
545
+ case undefined:
546
+ usage();
547
+ break;
548
+ default:
549
+ console.error(`Unknown command: ${cmd}`);
550
+ usage();
551
+ process.exit(1);
552
+ }
553
+ }
554
+
555
+ main().catch((err) => {
556
+ console.error(`oked-openclaw: ${err.message}`);
557
+ process.exit(1);
558
+ });
package/package.json CHANGED
@@ -1,41 +1,41 @@
1
- {
2
- "name": "@oked/openclaw-cli",
3
- "version": "0.1.0",
4
- "description": "Installer CLI for the @oked/openclaw plugin. Wraps `openclaw plugins install` and configures the plugin entry in openclaw.json.",
5
- "type": "module",
6
- "bin": {
7
- "oked-openclaw": "./bin/oked-openclaw.mjs"
8
- },
9
- "files": [
10
- "bin",
11
- "README.md"
12
- ],
13
- "scripts": {
14
- "test": "node --check bin/oked-openclaw.mjs"
15
- },
16
- "dependencies": {
17
- "@oked/sdk": "^0.1.0"
18
- },
19
- "keywords": [
20
- "openclaw",
21
- "openclaw-cli",
22
- "oked",
23
- "installer"
24
- ],
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/oked-ai/oked-sdk.git",
28
- "directory": "packages/openclaw-cli"
29
- },
30
- "bugs": {
31
- "url": "https://github.com/oked-ai/oked-sdk/issues"
32
- },
33
- "homepage": "https://github.com/oked-ai/oked-sdk/tree/main/packages/openclaw-cli#readme",
34
- "license": "MIT",
35
- "engines": {
36
- "node": ">=18"
37
- },
38
- "publishConfig": {
39
- "access": "public"
40
- }
41
- }
1
+ {
2
+ "name": "@oked/openclaw-cli",
3
+ "version": "0.1.3",
4
+ "description": "Installer CLI for the @oked/openclaw plugin. Wraps `openclaw plugins install` and configures the plugin entry in openclaw.json.",
5
+ "type": "module",
6
+ "bin": {
7
+ "oked-openclaw": "./bin/oked-openclaw.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "test": "node --check bin/oked-openclaw.mjs"
15
+ },
16
+ "dependencies": {
17
+ "@oked/sdk": "^0.1.0"
18
+ },
19
+ "keywords": [
20
+ "openclaw",
21
+ "openclaw-cli",
22
+ "oked",
23
+ "installer"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/oked-ai/oked-sdk.git",
28
+ "directory": "packages/openclaw-cli"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/oked-ai/oked-sdk/issues"
32
+ },
33
+ "homepage": "https://github.com/oked-ai/oked-sdk/tree/main/packages/openclaw-cli#readme",
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }