aquaman-proxy 0.11.4 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * - Hash-chained audit logs: Tamper-evident logging
9
9
  * - Custom service registry: YAML-based service config
10
10
  */
11
- import { Command } from 'commander';
11
+ import { Command, Help } from 'commander';
12
12
  import * as path from 'node:path';
13
13
  import * as fs from 'node:fs';
14
14
  import * as http from 'node:http';
@@ -108,9 +108,8 @@ async function promptSecretInput(prompt) {
108
108
  const program = new Command();
109
109
  program
110
110
  .name('aquaman')
111
- .description('Credential isolation layer for OpenClaw - keeps API keys outside the agent process')
111
+ .description('Credential isolation for AI agents \u2014 vault for the proxy core, OpenClaw plugin path, coding-agent adapter path')
112
112
  .version(VERSION)
113
- .addHelpText('before', `\n\u{1F531}\u{1F99E} Aquaman ${aqua(VERSION)} \u2014 Credential isolation for OpenClaw\n`)
114
113
  .configureHelp({
115
114
  subcommandTerm(cmd) {
116
115
  const args = cmd.registeredArguments
@@ -119,11 +118,443 @@ program
119
118
  return arg.required ? `<${n}>` : `[${n}]`;
120
119
  })
121
120
  .join(' ');
122
- return aqua(cmd.name()) + (cmd.options.length ? ' [options]' : '') + (args ? ' ' + args : '');
121
+ // Command names render in default color; only section headings
122
+ // (rendered by our custom formatHelp below) get the aqua treatment.
123
+ return cmd.name() + (cmd.options.length ? ' [options]' : '') + (args ? ' ' + args : '');
124
+ },
125
+ // Custom grouped help layout for the root program. Subcommand helps
126
+ // (e.g. `aquaman openclaw --help`) use Commander's default formatter.
127
+ formatHelp(cmd, helper) {
128
+ // Only customize the ROOT command's help. Nested subcommands keep the
129
+ // default layout \u2014 call the un-overridden formatter directly to avoid
130
+ // recursing into this override.
131
+ if (cmd !== program)
132
+ return Help.prototype.formatHelp.call(helper, cmd, helper);
133
+ const lines = [];
134
+ lines.push('');
135
+ lines.push(` \u{1F531} ${aqua('Aquaman')} ${VERSION} \u2014 Credential isolation for AI agents`);
136
+ lines.push('');
137
+ lines.push(`Usage: ${helper.commandUsage(cmd)}`);
138
+ lines.push('');
139
+ lines.push(cmd.description());
140
+ lines.push('');
141
+ // Options
142
+ const opts = helper.visibleOptions(cmd);
143
+ if (opts.length) {
144
+ lines.push('Options:');
145
+ for (const opt of opts) {
146
+ lines.push(` ${helper.optionTerm(opt).padEnd(20)} ${helper.optionDescription(opt)}`);
147
+ }
148
+ lines.push('');
149
+ }
150
+ const all = helper.visibleCommands(cmd);
151
+ const byName = new Map(all.map((c) => [c.name(), c]));
152
+ const renderRow = (term, desc) => ` ${term.padEnd(30)} ${desc}`;
153
+ const renderCmd = (c) => renderRow(c.name(), c.description() || '');
154
+ // --- Vault & core (agent-agnostic) ---
155
+ lines.push(aqua('Vault & core (agent-agnostic)'));
156
+ const vaultCore = ['setup', 'doctor', 'status', 'daemon', 'stop', 'init'];
157
+ for (const name of vaultCore) {
158
+ const c = byName.get(name);
159
+ if (c)
160
+ lines.push(renderCmd(c));
161
+ }
162
+ lines.push('');
163
+ lines.push(aqua('Vault management'));
164
+ for (const name of ['credentials', 'audit', 'services', 'policy']) {
165
+ const c = byName.get(name);
166
+ if (c)
167
+ lines.push(renderCmd(c));
168
+ }
169
+ lines.push('');
170
+ // --- OpenClaw namespace (nested) ---
171
+ const oc = byName.get('openclaw');
172
+ if (oc) {
173
+ lines.push(aqua('OpenClaw Gateway integration'));
174
+ // Ordered for readability (setup/doctor/status first, then lifecycle).
175
+ const ocOrder = ['setup', 'doctor', 'status', 'start', 'configure', 'migrate'];
176
+ const ocSubs = helper.visibleCommands(oc).filter((s) => s.name() !== 'help');
177
+ const ocSorted = [
178
+ ...ocOrder.map((n) => ocSubs.find((s) => s.name() === n)).filter(Boolean),
179
+ ...ocSubs.filter((s) => !ocOrder.includes(s.name())),
180
+ ];
181
+ for (const sub of ocSorted) {
182
+ lines.push(renderRow(`openclaw ${sub.name()}`, sub.description() || ''));
183
+ }
184
+ lines.push('');
185
+ }
186
+ // --- Coder namespace (shim \u2014 list documented subcommands) ---
187
+ if (byName.has('coder')) {
188
+ lines.push(aqua('AI coding-agent integration (Claude Code today)'));
189
+ const coderSubs = [
190
+ ['coder setup <agent>', 'Install hooks for an agent (claude-code today)'],
191
+ ['coder doctor', 'Deep diagnostic \u2014 projects, broker, per-project vault'],
192
+ ['coder status', 'Configured projects + hook wiring + broker activity'],
193
+ ['coder project list/add/remove', 'Manage ~/.aquaman/projects.yaml'],
194
+ ['coder get <ref>', 'Resolve an aquaman://service/key reference'],
195
+ ['coder exec <cmd>', 'Run command with project env injected + output redacted'],
196
+ ];
197
+ for (const [term, desc] of coderSubs) {
198
+ lines.push(renderRow(term, desc));
199
+ }
200
+ lines.push(` ${'(delegates to the aquaman-coder binary; install: npm install -g aquaman-coder)'}`);
201
+ lines.push('');
202
+ }
203
+ // --- Other ---
204
+ const helpCmd = byName.get('help');
205
+ if (helpCmd) {
206
+ lines.push(aqua('Other'));
207
+ lines.push(renderCmd(helpCmd));
208
+ lines.push('');
209
+ }
210
+ return lines.join('\n');
123
211
  }
124
212
  });
125
- // Start command - launches credential proxy + OpenClaw
213
+ // ============================================================================
214
+ // Top-level commands (vault-only, agent-agnostic)
215
+ // ============================================================================
216
+ //
217
+ // `aquaman setup`, `aquaman doctor`, `aquaman status` cover the proxy + vault
218
+ // surface only. For full bundles, use the namespaced versions:
219
+ // aquaman openclaw setup full OpenClaw bundle
220
+ // aquaman coder setup ... coding-agent adapter
221
+ // ============================================================================
222
+ // aquaman setup \u2014 vault-only minimal setup wizard.
126
223
  program
224
+ .command('setup')
225
+ .description('Vault-only setup wizard \u2014 backend + credentials (use `aquaman openclaw setup` or `aquaman coder setup` for full bundles)')
226
+ .option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden)')
227
+ .option('--no-policy', 'Skip request policy preset configuration')
228
+ .option('--non-interactive', 'Use environment variables instead of prompts (for CI)')
229
+ .action(async (options) => {
230
+ await runVaultSetup({
231
+ backend: options.backend,
232
+ policy: options.policy !== false,
233
+ nonInteractive: !!options.nonInteractive,
234
+ });
235
+ console.log('');
236
+ console.log(' Next steps:');
237
+ console.log(' \u2022 OpenClaw Gateway user? ' + aqua('aquaman openclaw setup'));
238
+ console.log(' \u2022 Claude Code / Codex / etc? ' + aqua('aquaman coder setup claude-code') + ' (requires aquaman-coder)');
239
+ console.log('');
240
+ });
241
+ // aquaman doctor \u2014 overview with persona-aware soft upsells.
242
+ program
243
+ .command('doctor')
244
+ .description('Overview health check (vault + integration summaries with soft upsells)')
245
+ .action(async () => {
246
+ const os = await import('node:os');
247
+ const configDir = getConfigDir();
248
+ const configPath = path.join(configDir, 'config.yaml');
249
+ const openclawStateDir = process.env['OPENCLAW_STATE_DIR'] || path.join(os.homedir(), '.openclaw');
250
+ let issues = 0;
251
+ console.log('');
252
+ console.log(` \u{1F531} Aquaman ${VERSION} \u2014 health check`);
253
+ console.log('');
254
+ console.log(` ${aqua('Vault')}`);
255
+ // Vault \u2014 config file
256
+ if (!fs.existsSync(configPath)) {
257
+ console.log(` \u2717 Config missing (${configPath})`);
258
+ console.log(' \u2192 Run: aquaman setup');
259
+ issues++;
260
+ }
261
+ else {
262
+ // Vault \u2014 backend + creds
263
+ try {
264
+ const config = loadConfig();
265
+ const store = await createCredentialStore({
266
+ backend: config.credentials.backend,
267
+ encryptionPassword: config.credentials.encryptionPassword,
268
+ vaultAddress: config.credentials.vaultAddress,
269
+ vaultToken: config.credentials.vaultToken,
270
+ onePasswordVault: config.credentials.onePasswordVault,
271
+ onePasswordAccount: config.credentials.onePasswordAccount,
272
+ keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
273
+ keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
274
+ bitwardenFolder: config.credentials.bitwardenFolder,
275
+ bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
276
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId
277
+ });
278
+ const creds = await store.list();
279
+ console.log(` \u2713 ${config.credentials.backend} backend (${creds.length} credential${creds.length !== 1 ? 's' : ''})`);
280
+ }
281
+ catch (err) {
282
+ console.log(` \u2717 Backend not accessible: ${err.message}`);
283
+ console.log(' \u2192 Run: aquaman setup');
284
+ issues++;
285
+ }
286
+ // Vault \u2014 proxy running
287
+ const sockPath = path.join(configDir, 'proxy.sock');
288
+ try {
289
+ await new Promise((resolve, reject) => {
290
+ const req = http.request({ socketPath: sockPath, path: '/_health', method: 'GET' }, (res) => {
291
+ res.resume();
292
+ res.on('end', () => res.statusCode === 200 ? resolve() : reject(new Error(`HTTP ${res.statusCode}`)));
293
+ });
294
+ req.on('error', reject);
295
+ req.end();
296
+ });
297
+ console.log(` \u2713 Proxy running on socket`);
298
+ }
299
+ catch {
300
+ console.log(` \u2717 Proxy not running`);
301
+ console.log(' \u2192 Run: aquaman daemon &');
302
+ issues++;
303
+ }
304
+ }
305
+ // OpenClaw integration
306
+ let openclawDetected = false;
307
+ try {
308
+ const { execSync } = await import('node:child_process');
309
+ execSync('which openclaw', { stdio: 'pipe' });
310
+ openclawDetected = true;
311
+ }
312
+ catch { /* */ }
313
+ if (!openclawDetected)
314
+ openclawDetected = fs.existsSync(openclawStateDir);
315
+ console.log('');
316
+ console.log(` ${aqua('OpenClaw integration')}`);
317
+ if (!openclawDetected) {
318
+ console.log(` \u2022 not detected (skipping \u2014 only relevant if you run an OpenClaw Gateway)`);
319
+ }
320
+ else {
321
+ const pluginInstalled = fs.existsSync(path.join(openclawStateDir, 'extensions', 'aquaman-plugin'));
322
+ if (pluginInstalled) {
323
+ console.log(` \u2713 plugin installed (deep: ${aqua('aquaman openclaw doctor')})`);
324
+ }
325
+ else {
326
+ console.log(` \u2717 plugin not installed`);
327
+ console.log(' \u2192 Run: aquaman openclaw setup');
328
+ issues++;
329
+ }
330
+ }
331
+ // Coder integration
332
+ const projectsYaml = path.join(configDir, 'projects.yaml');
333
+ const claudeSettings = path.join(os.homedir(), '.claude', 'settings.json');
334
+ let coderConfigured = fs.existsSync(projectsYaml);
335
+ if (!coderConfigured && fs.existsSync(claudeSettings)) {
336
+ try {
337
+ const raw = fs.readFileSync(claudeSettings, 'utf-8');
338
+ coderConfigured = raw.includes('aquaman-coder hook') || raw.includes('aquaman coder hook');
339
+ }
340
+ catch { /* */ }
341
+ }
342
+ console.log('');
343
+ console.log(` ${aqua('Coder integration')}`);
344
+ if (coderConfigured) {
345
+ console.log(` \u2713 configured (deep: ${aqua('aquaman coder doctor')})`);
346
+ }
347
+ else {
348
+ console.log(` \u2022 not configured`);
349
+ console.log(` Your coding agents (Claude Code, Codex, \u2026) could use the same`);
350
+ console.log(` vault protection. Install: ${aqua('npm install -g aquaman-coder')}`);
351
+ }
352
+ console.log('');
353
+ if (issues === 0) {
354
+ console.log(' All baseline checks passed.');
355
+ }
356
+ else {
357
+ console.log(` ${issues} issue${issues > 1 ? 's' : ''} found in baseline. Fix above and re-run.`);
358
+ }
359
+ console.log('');
360
+ process.exitCode = issues > 0 ? 1 : 0;
361
+ });
362
+ // aquaman status \u2014 proxy overview.
363
+ program
364
+ .command('status')
365
+ .description('Proxy daemon status overview')
366
+ .action(async () => {
367
+ const config = loadConfig();
368
+ console.log('');
369
+ console.log(` \u{1F531} ${aqua('Aquaman')} ${VERSION} — status`);
370
+ console.log('');
371
+ // Proxy state (probe socket first — the headline number)
372
+ const sockPath = path.join(getConfigDir(), 'proxy.sock');
373
+ let proxyLine = ` ${aqua('Proxy:')} not running`;
374
+ try {
375
+ const body = await new Promise((resolve, reject) => {
376
+ const req = http.request({ socketPath: sockPath, path: '/_health', method: 'GET' }, (res) => {
377
+ let buf = '';
378
+ res.on('data', (c) => { buf += c; });
379
+ res.on('end', () => resolve(buf));
380
+ });
381
+ req.on('error', reject);
382
+ req.end();
383
+ });
384
+ const health = JSON.parse(body);
385
+ proxyLine = ` ${aqua('Proxy:')} running (v${health.version}, uptime ${Math.floor(health.uptime ?? 0)}s)`;
386
+ }
387
+ catch { /* not running */ }
388
+ console.log(proxyLine);
389
+ // Configuration
390
+ console.log('');
391
+ console.log(` ${aqua('Configuration')}`);
392
+ console.log(` Config dir: ${getConfigDir()}`);
393
+ console.log(` Backend: ${config.credentials.backend}`);
394
+ console.log(` Socket: ${sockPath}`);
395
+ console.log(` Audit: ${config.audit.enabled ? 'enabled' : 'disabled'}`);
396
+ // Proxied services
397
+ console.log('');
398
+ console.log(` ${aqua('Proxied services')} (${config.credentials.proxiedServices.length})`);
399
+ for (const svc of config.credentials.proxiedServices)
400
+ console.log(` - ${svc}`);
401
+ console.log('');
402
+ console.log(` For deeper views: ${aqua('aquaman openclaw status')} / ${aqua('aquaman coder status')}`);
403
+ console.log('');
404
+ });
405
+ // ---------------- Shared helper: vault-only setup ----------------
406
+ async function runVaultSetup(opts) {
407
+ const os = await import('node:os');
408
+ const configDir = getConfigDir();
409
+ const configPath = path.join(configDir, 'config.yaml');
410
+ if (!opts.quiet)
411
+ console.log('\n \u{1F531} Vault setup\n');
412
+ // Backend detection
413
+ const platform = os.platform();
414
+ let backend = opts.backend;
415
+ if (!backend) {
416
+ if (platform === 'darwin') {
417
+ backend = 'keychain';
418
+ }
419
+ else {
420
+ try {
421
+ const { execSync } = await import('node:child_process');
422
+ execSync('pkg-config --exists libsecret-1', { stdio: 'pipe' });
423
+ backend = 'keychain';
424
+ }
425
+ catch {
426
+ const { isSystemdCredsAvailable } = await import('../core/credentials/backends/systemd-creds.js');
427
+ backend = isSystemdCredsAvailable() ? 'systemd-creds' : 'encrypted-file';
428
+ }
429
+ }
430
+ }
431
+ const validBackends = ['keychain', 'encrypted-file', 'keepassxc', '1password', 'vault', 'systemd-creds', 'bitwarden'];
432
+ if (!validBackends.includes(backend)) {
433
+ console.error(` Invalid backend: ${backend}. Valid: ${validBackends.join(', ')}`);
434
+ process.exit(1);
435
+ }
436
+ if (!opts.quiet) {
437
+ const platformLabel = platform === 'darwin' ? 'macOS' : platform === 'linux' ? 'Linux' : platform;
438
+ console.log(` Platform: ${platformLabel}`);
439
+ console.log(` Backend: ${backend}\n`);
440
+ }
441
+ ensureConfigDir();
442
+ let config = getDefaultConfig();
443
+ if (fs.existsSync(configPath)) {
444
+ config = loadConfig();
445
+ }
446
+ config.credentials.backend = backend;
447
+ fs.writeFileSync(configPath, yamlStringify(config), { encoding: 'utf-8', mode: 0o600 });
448
+ const auditDir = path.join(configDir, 'audit');
449
+ fs.mkdirSync(auditDir, { recursive: true, mode: 0o700 });
450
+ let store;
451
+ try {
452
+ store = await createCredentialStore({
453
+ backend: config.credentials.backend,
454
+ encryptionPassword: config.credentials.encryptionPassword || process.env['AQUAMAN_ENCRYPTION_PASSWORD'] || process.env['AQUAMAN_KEEPASS_PASSWORD'],
455
+ vaultAddress: config.credentials.vaultAddress || process.env['VAULT_ADDR'],
456
+ vaultToken: config.credentials.vaultToken || process.env['VAULT_TOKEN'],
457
+ onePasswordVault: config.credentials.onePasswordVault,
458
+ onePasswordAccount: config.credentials.onePasswordAccount,
459
+ keepassxcDatabasePath: config.credentials.keepassxcDatabasePath,
460
+ keepassxcKeyFilePath: config.credentials.keepassxcKeyFilePath,
461
+ bitwardenFolder: config.credentials.bitwardenFolder,
462
+ bitwardenOrganizationId: config.credentials.bitwardenOrganizationId,
463
+ bitwardenCollectionId: config.credentials.bitwardenCollectionId
464
+ });
465
+ }
466
+ catch (err) {
467
+ console.error(` Failed to initialize ${backend}: ${err instanceof Error ? err.message : err}`);
468
+ process.exit(1);
469
+ }
470
+ const storedServices = [];
471
+ if (opts.nonInteractive) {
472
+ const anth = process.env['ANTHROPIC_API_KEY'];
473
+ if (anth) {
474
+ await store.set('anthropic', 'api_key', anth);
475
+ storedServices.push('anthropic');
476
+ console.log(' \u2713 Stored anthropic/api_key');
477
+ }
478
+ const op = process.env['OPENAI_API_KEY'];
479
+ if (op) {
480
+ await store.set('openai', 'api_key', op);
481
+ storedServices.push('openai');
482
+ console.log(' \u2713 Stored openai/api_key');
483
+ }
484
+ }
485
+ else {
486
+ const readline = await import('node:readline');
487
+ const promptSecret = (prompt) => new Promise((resolve) => {
488
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
489
+ if (process.stdin.isTTY) {
490
+ process.stdout.write(prompt);
491
+ const stdin = process.stdin;
492
+ stdin.setRawMode(true);
493
+ stdin.resume();
494
+ let input = '';
495
+ const onData = (data) => {
496
+ const char = data.toString();
497
+ if (char === '\n' || char === '\r') {
498
+ stdin.setRawMode(false);
499
+ stdin.removeListener('data', onData);
500
+ stdin.pause();
501
+ rl.close();
502
+ process.stdout.write('\n');
503
+ resolve(input.trim());
504
+ }
505
+ else if (char === '\x7f' || char === '\b') {
506
+ input = input.slice(0, -1);
507
+ }
508
+ else if (char === '\x03') {
509
+ stdin.setRawMode(false);
510
+ process.exit(0);
511
+ }
512
+ else {
513
+ input += char;
514
+ }
515
+ };
516
+ stdin.on('data', onData);
517
+ }
518
+ else {
519
+ rl.question(prompt, (a) => { rl.close(); resolve(a.trim()); });
520
+ }
521
+ });
522
+ const anth = await promptSecret(' ? Anthropic API key (or Enter to skip): ');
523
+ if (anth) {
524
+ await store.set('anthropic', 'api_key', anth);
525
+ storedServices.push('anthropic');
526
+ console.log(' \u2713 anthropic/api_key\n');
527
+ }
528
+ const op = await promptSecret(' ? OpenAI API key (or Enter to skip): ');
529
+ if (op) {
530
+ await store.set('openai', 'api_key', op);
531
+ storedServices.push('openai');
532
+ console.log(' \u2713 openai/api_key\n');
533
+ }
534
+ }
535
+ if (opts.policy && storedServices.length > 0) {
536
+ const presets = getDefaultPolicyPresets();
537
+ const policyToApply = {};
538
+ for (const svc of storedServices) {
539
+ if (presets[svc])
540
+ policyToApply[svc] = presets[svc];
541
+ }
542
+ if (Object.keys(policyToApply).length > 0) {
543
+ config.policy = policyToApply;
544
+ saveConfig(config);
545
+ if (!opts.quiet)
546
+ console.log(' \u2713 Default policy presets applied.\n');
547
+ }
548
+ }
549
+ return { store, storedServices, backend: backend };
550
+ }
551
+ // OpenClaw integration namespace \u2014 full bundle setup, deep doctor/status,
552
+ // migration tools, and the (hidden) plugin-mode entry the plugin spawns.
553
+ const openclaw = program
554
+ .command('openclaw')
555
+ .description('OpenClaw Gateway integration (full setup, deep diagnostics, migration)');
556
+ // openclaw start \u2014 launches credential proxy + OpenClaw
557
+ openclaw
127
558
  .command('start')
128
559
  .description('Start credential proxy and launch OpenClaw')
129
560
  .option('-w, --workspace <path>', 'Workspace directory for OpenClaw')
@@ -375,9 +806,9 @@ program
375
806
  process.on('SIGTERM', shutdown);
376
807
  });
377
808
  // Plugin mode command - for use when managed by OpenClaw plugin
378
- program
379
- .command('plugin-mode')
380
- .description('Run in plugin mode (managed by OpenClaw plugin)')
809
+ openclaw
810
+ .command('plugin-mode', { hidden: true })
811
+ .description('Run in plugin mode (invoked by OpenClaw plugin, not by humans)')
381
812
  .action(async () => {
382
813
  const config = loadConfig();
383
814
  const socketPath = path.join(getConfigDir(), 'proxy.sock');
@@ -458,8 +889,8 @@ program
458
889
  process.on('SIGINT', shutdown);
459
890
  process.on('SIGTERM', shutdown);
460
891
  });
461
- // Configure command - write OpenClaw environment configuration
462
- program
892
+ // openclaw configure write OpenClaw environment configuration
893
+ openclaw
463
894
  .command('configure')
464
895
  .description('Generate environment configuration for OpenClaw')
465
896
  .option('--method <method>', 'Output method: env, dotenv, shell-rc', 'env')
@@ -524,11 +955,13 @@ program
524
955
  }
525
956
  });
526
957
  // Setup command - all-in-one guided onboarding
527
- program
958
+ // openclaw setup — full OpenClaw bundle: vault wizard + plugin install +
959
+ // openclaw.json wiring + auth-profiles.json + optional auto-migration.
960
+ openclaw
528
961
  .command('setup')
529
- .description('All-in-one setup wizardcreates config, stores credentials, installs plugin')
962
+ .description('Full setup for OpenClaw vault + plugin + auth profiles + (optional) auto-migrate')
530
963
  .option('--backend <backend>', 'Credential backend (keychain, encrypted-file, keepassxc, 1password, vault, systemd-creds, bitwarden)')
531
- .option('--no-openclaw', 'Skip OpenClaw plugin installation')
964
+ .option('--no-openclaw', 'Skip OpenClaw plugin installation step (run vault setup only)')
532
965
  .option('--no-policy', 'Skip request policy preset configuration')
533
966
  .option('--non-interactive', 'Use environment variables instead of prompts (for CI)')
534
967
  .action(async (options) => {
@@ -983,10 +1416,12 @@ program
983
1416
  console.log(' Troubleshooting: aquaman doctor');
984
1417
  console.log('');
985
1418
  });
986
- // Doctor command - diagnostic tool
987
- program
1419
+ // openclaw doctor deep diagnostic for the OpenClaw integration. Includes
1420
+ // the agent-agnostic vault/audit/policy checks too so a single command gives
1421
+ // the OpenClaw operator the full picture.
1422
+ openclaw
988
1423
  .command('doctor')
989
- .description('Check aquaman configuration and diagnose issues')
1424
+ .description('Deep diagnostic for the OpenClaw integration (vault + plugin + auth profiles)')
990
1425
  .action(async () => {
991
1426
  const os = await import('node:os');
992
1427
  const configDir = getConfigDir();
@@ -994,7 +1429,7 @@ program
994
1429
  const openclawStateDir = process.env['OPENCLAW_STATE_DIR'] || path.join(os.homedir(), '.openclaw');
995
1430
  let issues = 0;
996
1431
  console.log('');
997
- console.log(` \u{1F531}\u{1F99E} Aquaman ${VERSION} \u2014 Welcome to the doctor\u2019s office.`);
1432
+ console.log(` \u{1F531} Aquaman ${VERSION} \u2014 Welcome to the doctor\u2019s office.`);
998
1433
  console.log('');
999
1434
  // 1. Config file
1000
1435
  if (fs.existsSync(configPath)) {
@@ -1323,7 +1758,7 @@ program
1323
1758
  for (const c of unmigrated) {
1324
1759
  console.log(` ${c.service}/${c.key} \u2190 ${c.source}`);
1325
1760
  }
1326
- console.log(' \u2192 Run: aquaman migrate openclaw --auto');
1761
+ console.log(' \u2192 Run: aquaman openclaw migrate --auto');
1327
1762
  issues++;
1328
1763
  }
1329
1764
  if (needsCleanup.length > 0) {
@@ -1454,38 +1889,64 @@ credentials
1454
1889
  console.error('Credential store not available:', error instanceof Error ? error.message : error);
1455
1890
  process.exit(1);
1456
1891
  }
1457
- // Read value from stdin
1458
- console.log(`Enter value for ${service}/${key} (input hidden):`);
1459
- const readline = await import('node:readline');
1460
- const rl = readline.createInterface({
1461
- input: process.stdin,
1462
- output: process.stdout
1463
- });
1464
- // Disable echo
1465
- if (process.stdin.isTTY) {
1466
- process.stdin.setRawMode(true);
1467
- }
1468
- let value = '';
1469
- process.stdin.on('data', (data) => {
1470
- const char = data.toString();
1471
- if (char === '\n' || char === '\r') {
1472
- process.stdin.setRawMode(false);
1473
- rl.close();
1474
- storeCredential();
1475
- }
1476
- else if (char === '\x7f' || char === '\b') {
1477
- value = value.slice(0, -1);
1478
- }
1479
- else {
1480
- value += char;
1481
- }
1482
- });
1483
- async function storeCredential() {
1484
- console.log('');
1485
- await store.set(service, key, value.trim());
1486
- console.log(`Credential stored: ${service}/${key}`);
1892
+ // Two stdin paths:
1893
+ // - TTY (interactive shell): per-character raw-mode read so the value
1894
+ // never echoes. Submit on Enter, backspace on DEL/^H.
1895
+ // - Pipe (scripts / CI / migrations): read all stdin into one buffer.
1896
+ // Strip exactly ONE trailing newline so `printf x | ...` and
1897
+ // `echo x | ...` both round-trip to `x` without mangling embedded
1898
+ // newlines in PEM keys or JSON blobs.
1899
+ const value = process.stdin.isTTY
1900
+ ? await readTtyHiddenInput(`Enter value for ${service}/${key} (input hidden):`)
1901
+ : await readAllStdin();
1902
+ if (value.length === 0) {
1903
+ console.error('Empty credential value rejected. Pipe a value or type one.');
1904
+ process.exit(1);
1487
1905
  }
1906
+ await store.set(service, key, value);
1907
+ console.log(`Credential stored: ${service}/${key}`);
1488
1908
  });
1909
+ async function readAllStdin() {
1910
+ const chunks = [];
1911
+ for await (const chunk of process.stdin) {
1912
+ chunks.push(chunk);
1913
+ }
1914
+ const raw = Buffer.concat(chunks).toString('utf-8');
1915
+ // Strip a single trailing \n (and the optional \r before it for CRLF).
1916
+ return raw.endsWith('\n') ? raw.replace(/\r?\n$/, '') : raw;
1917
+ }
1918
+ async function readTtyHiddenInput(prompt) {
1919
+ console.log(prompt);
1920
+ process.stdin.setRawMode(true);
1921
+ process.stdin.resume();
1922
+ process.stdin.setEncoding('utf-8');
1923
+ return new Promise((resolve) => {
1924
+ let value = '';
1925
+ const onData = (chunk) => {
1926
+ for (const char of chunk) {
1927
+ if (char === '\n' || char === '\r') {
1928
+ process.stdin.setRawMode(false);
1929
+ process.stdin.pause();
1930
+ process.stdin.off('data', onData);
1931
+ console.log('');
1932
+ resolve(value);
1933
+ return;
1934
+ }
1935
+ if (char === '\x7f' || char === '\b') {
1936
+ value = value.slice(0, -1);
1937
+ }
1938
+ else if (char === '\x03') { // Ctrl-C
1939
+ process.stdin.setRawMode(false);
1940
+ process.exit(130);
1941
+ }
1942
+ else {
1943
+ value += char;
1944
+ }
1945
+ }
1946
+ };
1947
+ process.stdin.on('data', onData);
1948
+ });
1949
+ }
1489
1950
  credentials
1490
1951
  .command('list')
1491
1952
  .description('List stored credentials')
@@ -1600,7 +2061,10 @@ credentials
1600
2061
  console.log(` vault kv put secret/aquaman/${name}/${key} credential="YOUR_KEY"`);
1601
2062
  break;
1602
2063
  case '1password':
1603
- console.log(` op item create --vault aquaman --category "API Credential" --title aquaman-${name}-${key} credential="YOUR_KEY"`);
2064
+ // Prefer aquaman's own CLI it pipes the value via a 0o600 temp
2065
+ // template file, never exposing the secret on argv or in op's
2066
+ // /proc/<pid>/cmdline.
2067
+ console.log(` aquaman credentials add ${name} ${key}`);
1604
2068
  break;
1605
2069
  default:
1606
2070
  console.log(` aquaman credentials add ${name} ${key}`);
@@ -1718,9 +2182,8 @@ policy
1718
2182
  }
1719
2183
  });
1720
2184
  // Migration commands
1721
- const migrate = program.command('migrate').description('Migrate credentials from other sources');
1722
- migrate
1723
- .command('openclaw')
2185
+ openclaw
2186
+ .command('migrate')
1724
2187
  .description('Migrate channel credentials from openclaw.json into aquaman')
1725
2188
  .option('-c, --config <path>', 'Path to openclaw.json')
1726
2189
  .option('--dry-run', 'Show what would be migrated without writing')
@@ -1741,7 +2204,7 @@ migrate
1741
2204
  console.error('No aquaman config found. Run `aquaman setup` first.');
1742
2205
  process.exit(1);
1743
2206
  }
1744
- console.log(`\n \u{1F531}\u{1F99E} Aquaman ${aqua(VERSION)} \u2014 Time to put your secrets somewhere safe.\n`);
2207
+ console.log(`\n \u{1F531} Aquaman ${aqua(VERSION)} \u2014 Time to put your secrets somewhere safe.\n`);
1745
2208
  console.log(' Scanning for plaintext credentials...\n');
1746
2209
  const openclawStateDir = process.env['OPENCLAW_STATE_DIR'] || path.join(os.homedir(), '.openclaw');
1747
2210
  const openclawConfigPath = findOpenClawConfig(opts.config || path.join(openclawStateDir, 'openclaw.json'));
@@ -2109,9 +2572,10 @@ migrate
2109
2572
  }
2110
2573
  });
2111
2574
  // Status command
2112
- program
2575
+ // openclaw status — deep status for the OpenClaw integration.
2576
+ openclaw
2113
2577
  .command('status')
2114
- .description('Show aquaman status')
2578
+ .description('OpenClaw-specific status (plugin lifecycle, sentinel env vars)')
2115
2579
  .action(async () => {
2116
2580
  const config = loadConfig();
2117
2581
  console.log('aquaman status\n');
@@ -2187,6 +2651,47 @@ function formatEntry(entry) {
2187
2651
  return JSON.stringify(entry.data).slice(0, 80);
2188
2652
  }
2189
2653
  }
2654
+ // ---------------- coder namespace (shim → aquaman-coder bin) ----------------
2655
+ //
2656
+ // The `aquaman coder *` namespace presents a unified user-facing surface for
2657
+ // the coding-agent adapter. The actual implementation lives in the separate
2658
+ // `aquaman-coder` package (see packages/coder/). This shim execs that binary
2659
+ // with the remaining argv, so the proxy CLI never imports coder code — the
2660
+ // `proxy → coder ✗` boundary in docs/PACKAGES.md stays intact.
2661
+ //
2662
+ // If aquaman-coder isn't installed, we print a clear install hint.
2663
+ // Intercept `aquaman coder ...` BEFORE Commander parses it, so the catch-all
2664
+ // behavior is exact: every token after `coder` flows through to the
2665
+ // aquaman-coder binary verbatim, including --flags Commander would otherwise
2666
+ // interpret as unknown options.
2667
+ //
2668
+ // Only intercept when `coder` is the first command token (argv[2]). This
2669
+ // avoids false positives like `aquaman policy test coder/api /foo` where
2670
+ // `coder` appears as a positional argument elsewhere.
2671
+ if (process.argv[2] === 'coder') {
2672
+ const subArgs = process.argv.slice(3);
2673
+ const { spawnSync } = await import('node:child_process');
2674
+ const result = spawnSync('aquaman-coder', subArgs, { stdio: 'inherit' });
2675
+ if (result.error && result.error.code === 'ENOENT') {
2676
+ console.error('aquaman-coder is not installed.');
2677
+ console.error('Install it with: npm install -g aquaman-coder');
2678
+ process.exit(127);
2679
+ }
2680
+ if (result.error) {
2681
+ console.error(`Failed to run aquaman-coder: ${result.error.message}`);
2682
+ process.exit(1);
2683
+ }
2684
+ process.exit(result.status ?? 0);
2685
+ }
2686
+ // Register the `coder` namespace so it shows up in `aquaman --help`.
2687
+ // Its action is unreachable (the intercept above bypasses Commander entirely),
2688
+ // but documentation matters.
2689
+ program
2690
+ .command('coder')
2691
+ .description('AI coding-agent integration (delegates to `aquaman-coder`)')
2692
+ .allowUnknownOption()
2693
+ .helpOption(false)
2694
+ .action(() => { });
2190
2695
  // Show help when run without arguments (like openclaw does)
2191
2696
  if (process.argv.length <= 2) {
2192
2697
  program.help();