@pingagent/sdk 0.1.11 → 0.1.13

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/bin/pingagent.js CHANGED
@@ -5,6 +5,8 @@ import * as path from 'node:path';
5
5
  import * as os from 'node:os';
6
6
  import * as readline from 'node:readline';
7
7
  import { spawnSync } from 'node:child_process';
8
+ import { createRequire } from 'node:module';
9
+ import { fileURLToPath } from 'node:url';
8
10
  import {
9
11
  PingAgentClient,
10
12
  generateIdentity,
@@ -40,6 +42,8 @@ import {
40
42
  } from '../dist/index.js';
41
43
  import { ERROR_HINTS, SCHEMA_TEXT } from '@pingagent/schemas';
42
44
 
45
+ const require = createRequire(import.meta.url);
46
+ const THIS_FILE = fileURLToPath(import.meta.url);
43
47
  const DEFAULT_SERVER = 'https://pingagent.chat';
44
48
  const UPGRADE_URL = 'https://pingagent.chat';
45
49
  const DEFAULT_IDENTITY_PATH = path.join(os.homedir(), '.pingagent', 'identity.json');
@@ -140,29 +144,69 @@ function writeTrustPolicyDoc(identityPath, doc) {
140
144
 
141
145
  function findOpenClawInstallScript() {
142
146
  const explicit = process.env.PINGAGENT_OPENCLAW_INSTALL_BIN;
143
- if (explicit) return { cmd: process.execPath, args: [resolvePath(explicit)] };
144
- const repoScript = path.resolve(process.cwd(), 'packages', 'openclaw-install', 'install.mjs');
145
- if (fs.existsSync(repoScript)) return { cmd: process.execPath, args: [repoScript] };
146
- return null;
147
+ if (explicit) return { cmd: process.execPath, args: [resolvePath(explicit)], source: 'env' };
148
+
149
+ const localCandidates = [
150
+ path.resolve(process.cwd(), 'packages', 'openclaw-install', 'install.mjs'),
151
+ path.resolve(path.dirname(THIS_FILE), '..', '..', 'openclaw-install', 'install.mjs'),
152
+ ];
153
+ for (const candidate of localCandidates) {
154
+ if (fs.existsSync(candidate)) return { cmd: process.execPath, args: [candidate], source: 'repo' };
155
+ }
156
+
157
+ try {
158
+ const pkgJsonPath = require.resolve('@pingagent/openclaw-install/package.json');
159
+ const installedScript = path.join(path.dirname(pkgJsonPath), 'install.mjs');
160
+ if (fs.existsSync(installedScript)) {
161
+ return { cmd: process.execPath, args: [installedScript], source: 'installed-package' };
162
+ }
163
+ } catch {
164
+ // fall through to npx fallback
165
+ }
166
+
167
+ return { cmd: 'npx', args: ['-y', '@pingagent/openclaw-install'], source: 'npx' };
147
168
  }
148
169
 
149
170
  function runOpenClawInstall(args) {
150
171
  const resolved = findOpenClawInstallScript();
151
- if (!resolved) {
152
- return { ok: false, stdout: '', stderr: 'OpenClaw installer script not found locally. Set PINGAGENT_OPENCLAW_INSTALL_BIN.' };
153
- }
154
172
  const result = spawnSync(resolved.cmd, [...resolved.args, ...args], {
155
173
  encoding: 'utf-8',
156
174
  env: process.env,
157
175
  });
176
+ const errorMessage = result.error
177
+ ? (result.error.code === 'ENOENT'
178
+ ? `Failed to launch ${resolved.cmd}. Install npx/Node tooling or set PINGAGENT_OPENCLAW_INSTALL_BIN.`
179
+ : String(result.error.message || result.error))
180
+ : '';
158
181
  return {
159
- ok: result.status === 0,
182
+ ok: result.status === 0 && !result.error,
160
183
  stdout: String(result.stdout ?? ''),
161
- stderr: String(result.stderr ?? ''),
184
+ stderr: errorMessage || String(result.stderr ?? ''),
162
185
  status: result.status ?? 1,
186
+ source: resolved.source,
163
187
  };
164
188
  }
165
189
 
190
+ function getHostPanelSurfaceUrl() {
191
+ const portRaw = String(process.env.PINGAGENT_WEB_PORT || '3846').trim();
192
+ const port = Number.parseInt(portRaw, 10);
193
+ return `http://127.0.0.1:${Number.isFinite(port) ? port : 3846}/host-panel`;
194
+ }
195
+
196
+ function getSurfaceRecommendationLines(primaryCommandPrefix = 'npx @pingagent/sdk', secondaryCommandPrefix = 'pingagent') {
197
+ return [
198
+ 'With GUI: Host Panel',
199
+ ` Start locally: ${primaryCommandPrefix} web`,
200
+ ...(secondaryCommandPrefix && secondaryCommandPrefix !== primaryCommandPrefix ? [` Or via local bin: ${secondaryCommandPrefix} web`] : []),
201
+ ` URL when running: ${getHostPanelSurfaceUrl()}`,
202
+ 'Headless / low-token: TUI',
203
+ ` ${primaryCommandPrefix} host tui`,
204
+ ...(secondaryCommandPrefix && secondaryCommandPrefix !== primaryCommandPrefix ? [` ${secondaryCommandPrefix} host tui`] : []),
205
+ ` ${primaryCommandPrefix} host tui --once`,
206
+ 'MCP: agent/runtime control surface, not the default human operator UI',
207
+ ];
208
+ }
209
+
166
210
  function clearScreen() {
167
211
  process.stdout.write('\x1Bc');
168
212
  }
@@ -194,11 +238,11 @@ function truncateLine(value, max = 100) {
194
238
 
195
239
  function formatSessionRow(session, selected) {
196
240
  const marker = selected ? '>' : ' ';
197
- const rebind = session.binding_alert ? ' !rebind' : '';
241
+ const reconnect = session.binding_alert ? ' !reconnect' : '';
198
242
  const trust = session.trust_state || 'unknown';
199
243
  const unread = session.unread_count ?? 0;
200
244
  const who = truncateLine(session.remote_did || session.conversation_id || 'unknown', 40);
201
- return `${marker} ${who} [${trust}] unread=${unread}${rebind}`;
245
+ return `${marker} ${who} [${trust}] unread=${unread}${reconnect}`;
202
246
  }
203
247
 
204
248
  function formatMessageRow(message) {
@@ -410,13 +454,13 @@ function renderHostTuiScreen(hostState, uiState) {
410
454
  'PingAgent Host TUI',
411
455
  `DID: ${hostState.identity.did}`,
412
456
  `status=${formatStatusLine(uiState?.statusLevel || 'info', uiState?.statusMessage || '(ready)')}${statusTs}${statusCountdown}`,
413
- `runtime_mode=${hostState.runtimeMode} receive_mode=${hostState.ingressRuntime?.receive_mode || 'webhook'} active_chat_session=${hostState.activeChatSession || '(none)'}`,
457
+ `runtime_mode=${hostState.runtimeMode} receive_mode=${hostState.ingressRuntime?.receive_mode || 'webhook'} current_openclaw_chat=${hostState.activeChatSession || '(none)'}`,
414
458
  `ingress=${ingressLabel}${degraded ? ' action=[f] fix-now' : ''}`,
415
459
  uiState?.publicLinkUrl ? `public_link=${uiState.publicLinkUrl}` : null,
416
460
  `sessions=${sessions.length} unread_total=${hostState.unreadTotal ?? 0} alert_sessions=${hostState.alertSessions ?? 0} view=${view}`,
417
461
  `policy=${hostState.policyPath}`,
418
- `session_map=${hostState.sessionMapPath}`,
419
- `binding_alerts=${hostState.sessionBindingAlertsPath}`,
462
+ `chat_link_map=${hostState.sessionMapPath}`,
463
+ `chat_link_alerts=${hostState.sessionBindingAlertsPath}`,
420
464
  hostState.ingressRuntime?.hooks_last_error ? `hooks_error=${truncateLine(hostState.ingressRuntime.hooks_last_error, 120)}` : null,
421
465
  '',
422
466
  ].filter(Boolean);
@@ -443,8 +487,8 @@ function renderHostTuiScreen(hostState, uiState) {
443
487
  lines.push('- A: apply first open trust recommendation for selected session');
444
488
  lines.push('- D: dismiss current open recommendation');
445
489
  lines.push('- R: reopen dismissed/superseded recommendation');
446
- lines.push('- b: bind selected conversation to current chat session');
447
- lines.push('- c: clear selected binding');
490
+ lines.push('- b: attach selected session to the current OpenClaw chat');
491
+ lines.push('- c: detach the selected chat link');
448
492
  lines.push('- q: quit');
449
493
  } else if (view === 'history') {
450
494
  lines.push('Conversation History');
@@ -543,13 +587,13 @@ function renderHostTuiScreen(hostState, uiState) {
543
587
  lines.push(`remote=${selected.remote_did || '(unknown)'}`);
544
588
  lines.push(`trust=${selected.trust_state} unread=${selected.unread_count}`);
545
589
  lines.push(`last_preview=${selected.last_message_preview || '(none)'}`);
546
- lines.push(`binding=${selected.binding?.session_key || '(unbound)'}`);
547
- lines.push(`current_chat=${hostState.activeChatSession || '(none)'}`);
590
+ lines.push(`chat_link=${selected.binding?.session_key || '(none)'}`);
591
+ lines.push(`current_openclaw_chat=${hostState.activeChatSession || '(none)'}`);
548
592
  if (selected.binding_alert) {
549
- lines.push(`needs_rebind=true`);
593
+ lines.push('needs_reconnect=true');
550
594
  lines.push(`warning=${selected.binding_alert.message}`);
551
595
  } else {
552
- lines.push('needs_rebind=false');
596
+ lines.push('needs_reconnect=false');
553
597
  }
554
598
  if (openRecommendation) {
555
599
  lines.push(`trust_action=${getTrustRecommendationActionLabel(openRecommendation)}`);
@@ -573,8 +617,8 @@ function renderHostTuiScreen(hostState, uiState) {
573
617
  '[S] summary',
574
618
  '[o] history',
575
619
  '[t] tasks',
576
- '[b] bind-current',
577
- '[c] clear-binding',
620
+ '[b] attach-chat',
621
+ '[c] detach-chat',
578
622
  ].filter(Boolean).join(' ');
579
623
  lines.push(`actions=${actionBar}`);
580
624
  lines.push('');
@@ -611,7 +655,7 @@ function renderHostTuiScreen(hostState, uiState) {
611
655
  }
612
656
 
613
657
  lines.push('');
614
- lines.push('Keys: ↑/↓ or j/k select Enter/l open Esc/h back g/G jump r refresh a approve A apply-rec D dismiss-rec R reopen-rec d demo m read p reply o history s search t tasks x cancel-task y dump f fix-hooks b bind c clear ? help q quit');
658
+ lines.push('Keys: ↑/↓ or j/k select Enter/l open Esc/h back g/G jump r refresh a approve A apply-rec D dismiss-rec R reopen-rec d demo m read p reply o history s search t tasks x cancel-task y dump f fix-hooks b attach-chat c detach-chat ? help q quit');
615
659
  return lines.join('\n');
616
660
  }
617
661
 
@@ -1156,18 +1200,18 @@ async function runHostTui(identityPath, opts) {
1156
1200
  const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
1157
1201
  if (!selected?.conversation_id) return;
1158
1202
  const current = latestState.activeChatSession || '(none)';
1159
- const previous = selected.binding?.session_key || '(unbound)';
1160
- const confirmed = await confirmAction(`Rebind conversation ${selected.conversation_id}\nRemote DID: ${selected.remote_did || '(unknown)'}\nCurrent chat: ${current}\nPrevious binding: ${previous}\nProceed?`);
1203
+ const previous = selected.binding?.session_key || '(none)';
1204
+ const confirmed = await confirmAction(`Attach chat link for conversation ${selected.conversation_id}\nRemote DID: ${selected.remote_did || '(unknown)'}\nCurrent OpenClaw chat: ${current}\nPrevious chat link: ${previous}\nProceed?`);
1161
1205
  if (confirmed) {
1162
1206
  if (!latestState.activeChatSession) {
1163
- setStatus('Rebind failed: no active chat session.', 'err');
1207
+ setStatus('Attach failed: no active OpenClaw chat.', 'err');
1164
1208
  latestState = redraw();
1165
1209
  return;
1166
1210
  }
1167
1211
  setSessionBinding(selected.conversation_id, latestState.activeChatSession);
1168
- setStatus(`Rebound ${selected.conversation_id} -> ${latestState.activeChatSession}`, 'ok');
1212
+ setStatus(`Attached chat link ${selected.conversation_id} -> ${latestState.activeChatSession}`, 'ok');
1169
1213
  } else {
1170
- setStatus('Rebind cancelled.', 'warn');
1214
+ setStatus('Attach chat link cancelled.', 'warn');
1171
1215
  }
1172
1216
  latestState = redraw();
1173
1217
  return;
@@ -1176,7 +1220,7 @@ async function runHostTui(identityPath, opts) {
1176
1220
  const selected = (latestState.sessions || []).find((session) => session.session_key === uiState.selectedSessionKey);
1177
1221
  if (!selected?.conversation_id) return;
1178
1222
  removeSessionBinding(selected.conversation_id);
1179
- setStatus(`Cleared binding for ${selected.conversation_id}`, 'ok');
1223
+ setStatus(`Detached chat link for ${selected.conversation_id}`, 'ok');
1180
1224
  latestState = redraw();
1181
1225
  return;
1182
1226
  }
@@ -3121,13 +3165,116 @@ program
3121
3165
  else console.log(`Connected. conversation=${convo.data.conversation_id} did=${targetDid}`);
3122
3166
  });
3123
3167
 
3168
+ async function runHostBootstrap(opts) {
3169
+ const validTemplates = new Set(['launchd', 'systemd', 'docker', 'pm2', 'supervisord']);
3170
+ const template = opts.template ? String(opts.template).trim() : '';
3171
+ if (template && !validTemplates.has(template)) {
3172
+ console.error(`Unsupported template: ${template}. Valid: ${Array.from(validTemplates).join(', ')}`);
3173
+ process.exit(1);
3174
+ }
3175
+
3176
+ const steps = [];
3177
+ const runStep = (label, args) => {
3178
+ const result = runOpenClawInstall(args);
3179
+ steps.push({ label, args, result });
3180
+ return { label, args, result };
3181
+ };
3182
+
3183
+ const installStep = runStep('install', []);
3184
+ const hooksStep = installStep.result.ok ? runStep('hooks repair', ['fix-hooks']) : null;
3185
+ const verifyStep = installStep.result.ok && hooksStep?.result.ok
3186
+ ? runStep('runtime verify', ['verify-runtime', '--fix-hooks'])
3187
+ : null;
3188
+
3189
+ let runnerStep = null;
3190
+ if (verifyStep?.result.ok && opts.write) {
3191
+ const runnerArgs = ['init-runner', '--ingress', '--panel'];
3192
+ if (template) runnerArgs.push('--template', template);
3193
+ const supportsWrite = !template || template === 'launchd' || template === 'systemd';
3194
+ if (supportsWrite) runnerArgs.push('--write');
3195
+ runnerStep = runStep(supportsWrite ? 'runner setup' : 'runner template', runnerArgs);
3196
+ }
3197
+
3198
+ if (runnerStep?.result.stdout && opts.write) {
3199
+ console.log(runnerStep.result.stdout.trim());
3200
+ if (runnerStep.result.stderr.trim()) console.error(runnerStep.result.stderr.trim());
3201
+ console.log('');
3202
+ }
3203
+
3204
+ const failed = steps.find((step) => !step.result.ok) ?? null;
3205
+ const formatStepStatus = (step, fallback = 'skipped') => (step ? (step.result.ok ? 'ok' : 'failed') : fallback);
3206
+ const runnerStatus = !opts.write
3207
+ ? 'not_written'
3208
+ : !runnerStep
3209
+ ? 'skipped'
3210
+ : !runnerStep.result.ok
3211
+ ? 'failed'
3212
+ : (template && template !== 'launchd' && template !== 'systemd')
3213
+ ? 'template_printed_not_started'
3214
+ : 'written_not_started';
3215
+ const installerSource = installStep.result.source || 'unknown';
3216
+
3217
+ console.log('PingAgent Host Bootstrap');
3218
+ console.log('========================');
3219
+ console.log(`install=${formatStepStatus(installStep)}`);
3220
+ console.log(`hooks_repair=${formatStepStatus(hooksStep)}`);
3221
+ console.log(`runtime_verify=${formatStepStatus(verifyStep)}`);
3222
+ console.log(`installer_source=${installerSource}`);
3223
+ console.log(`runner=${runnerStatus}`);
3224
+ console.log(`host_panel_url=${getHostPanelSurfaceUrl()}`);
3225
+ console.log('host_panel_started_by_bootstrap=false');
3226
+ console.log('host_panel_start=npx @pingagent/sdk web');
3227
+ console.log('host_panel_start_local=pingagent web');
3228
+ console.log('tui=npx @pingagent/sdk host tui');
3229
+ console.log('tui_local=pingagent host tui');
3230
+ console.log('');
3231
+ console.log('Control surfaces:');
3232
+ for (const line of getSurfaceRecommendationLines('npx @pingagent/sdk', 'pingagent')) console.log(line);
3233
+ console.log('');
3234
+ if (!opts.write) {
3235
+ console.log('Next steps:');
3236
+ console.log(' Bootstrap validates and repairs config, but it does not start long-lived daemons.');
3237
+ console.log(' Start the Host Panel now with: npx @pingagent/sdk web (or pingagent web)');
3238
+ console.log(' Use the headless surface now with: npx @pingagent/sdk host tui (or pingagent host tui)');
3239
+ console.log(` Re-run with: npx @pingagent/sdk host bootstrap --write${template ? ` --template ${template}` : ''}`);
3240
+ console.log(' Manual path: npx @pingagent/openclaw-install init-runner --ingress --panel');
3241
+ } else if (runnerStep?.result.ok) {
3242
+ console.log('Next steps:');
3243
+ console.log(' Runner files/templates were generated, but bootstrap did not start those services.');
3244
+ if (!template || template === 'launchd' || template === 'systemd') {
3245
+ console.log(' Follow the printed launchctl/systemctl instructions to start them.');
3246
+ } else {
3247
+ console.log(' Start the generated runner with your chosen process manager.');
3248
+ }
3249
+ }
3250
+
3251
+ if (failed) {
3252
+ const stdout = failed.result.stdout.trim();
3253
+ const stderr = failed.result.stderr.trim();
3254
+ console.error('');
3255
+ console.error(`Bootstrap failed during ${failed.label}.`);
3256
+ if (stdout) console.error(stdout);
3257
+ if (stderr) console.error(stderr);
3258
+ process.exit(1);
3259
+ }
3260
+ }
3261
+
3124
3262
  const host = program
3125
3263
  .command('host')
3126
- .description('Headless runtime inspection and control for PingAgent host state');
3264
+ .description('OpenClaw host activation and operator control surfaces for GUI, headless, and low-token workflows');
3265
+
3266
+ host
3267
+ .command('bootstrap')
3268
+ .description('Run the idempotent OpenClaw activation flow and print the recommended Host Panel / TUI surfaces')
3269
+ .option('--write', 'Write launchd/systemd runner files when supported')
3270
+ .option('--template <name>', 'Runner template: launchd, systemd, docker, pm2, or supervisord')
3271
+ .action(async (opts) => {
3272
+ await runHostBootstrap(opts);
3273
+ });
3127
3274
 
3128
3275
  host
3129
3276
  .command('tui')
3130
- .description('Start a terminal UI for runtime, sessions, bindings, and rebind actions')
3277
+ .description('Start the headless / low-token terminal UI for runtime, sessions, chat links, and repair actions')
3131
3278
  .option('--once', 'Print one snapshot and exit')
3132
3279
  .option('--refresh-ms <ms>', 'Refresh interval in interactive mode', '2000')
3133
3280
  .option('--profile <name>', 'Use profile from ~/.pingagent/<name>')
@@ -3142,7 +3289,7 @@ host
3142
3289
 
3143
3290
  program
3144
3291
  .command('web')
3145
- .description('Start local web UI and host panel for debugging, runtime inspection, trust policy, and audit. By default scans ~/.pingagent for profiles; use --identity-dir to lock to one profile.')
3292
+ .description('Start the Host Panel, the primary GUI surface for runtime inspection, trust policy, and repair. Use pingagent host tui for headless or low-token operation.')
3146
3293
  .option('--port <port>', 'Port for the web server', '3846')
3147
3294
  .action(async (opts) => {
3148
3295
  const serverUrl = process.env.PINGAGENT_SERVER_URL || DEFAULT_SERVER;