agentbnb 7.0.0 → 8.0.1

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.
@@ -67,9 +67,10 @@ import {
67
67
  } from "../../chunk-3LWBH7P3.js";
68
68
 
69
69
  // skills/agentbnb/bootstrap.ts
70
- import { join as join5 } from "path";
70
+ import { join as join5, basename, dirname as dirname4 } from "path";
71
71
  import { homedir as homedir2 } from "os";
72
- import { spawnSync } from "child_process";
72
+ import { spawnSync, exec } from "child_process";
73
+ import { promisify as promisify2 } from "util";
73
74
  import { randomUUID as randomUUID10 } from "crypto";
74
75
 
75
76
  // src/runtime/process-guard.ts
@@ -6735,6 +6736,7 @@ function isNetworkError(err) {
6735
6736
  }
6736
6737
 
6737
6738
  // skills/agentbnb/bootstrap.ts
6739
+ var execAsync = promisify2(exec);
6738
6740
  function resolveWorkspaceDir() {
6739
6741
  if (process.env["AGENTBNB_DIR"]) {
6740
6742
  return process.env["AGENTBNB_DIR"];
@@ -6807,7 +6809,58 @@ function registerDecomposerCard(configDir, owner) {
6807
6809
  );
6808
6810
  }
6809
6811
  }
6810
- async function activate(config = {}) {
6812
+ function findCli() {
6813
+ const result = spawnSync("which", ["agentbnb"], { encoding: "utf-8", stdio: "pipe" });
6814
+ if (result.status === 0 && result.stdout.trim()) {
6815
+ return result.stdout.trim();
6816
+ }
6817
+ return null;
6818
+ }
6819
+ async function runCommand(cmd, env) {
6820
+ return execAsync(cmd, { env });
6821
+ }
6822
+ function deriveAgentName(configDir) {
6823
+ const parent = basename(dirname4(configDir));
6824
+ if (parent && parent !== "." && parent !== ".agentbnb" && parent !== homedir2().split("/").pop()) {
6825
+ return parent;
6826
+ }
6827
+ return `agent-${randomUUID10().slice(0, 8)}`;
6828
+ }
6829
+ var defaultDeps = { findCli, runCommand };
6830
+ async function autoOnboard(configDir, deps = defaultDeps) {
6831
+ process.stderr.write("[agentbnb] First-time setup: initializing agent identity...\n");
6832
+ const cliPath = deps.findCli();
6833
+ if (!cliPath) {
6834
+ process.stderr.write("[agentbnb] CLI not found. Run: npm install -g agentbnb\n");
6835
+ throw new AgentBnBError(
6836
+ "agentbnb CLI not found in PATH. Install with: npm install -g agentbnb",
6837
+ "INIT_FAILED"
6838
+ );
6839
+ }
6840
+ const env = { ...process.env, AGENTBNB_DIR: configDir };
6841
+ const agentName = deriveAgentName(configDir);
6842
+ try {
6843
+ await deps.runCommand(`agentbnb init --owner "${agentName}" --yes --no-detect`, env);
6844
+ process.stderr.write(`[agentbnb] Agent "${agentName}" initialized.
6845
+ `);
6846
+ } catch (err) {
6847
+ const msg = err instanceof Error ? err.message : String(err);
6848
+ throw new AgentBnBError(`Auto-init failed: ${msg}`, "INIT_FAILED");
6849
+ }
6850
+ try {
6851
+ await deps.runCommand("agentbnb openclaw sync", env);
6852
+ process.stderr.write("[agentbnb] Capabilities published from SOUL.md.\n");
6853
+ } catch {
6854
+ process.stderr.write("[agentbnb] Note: openclaw sync skipped (SOUL.md may not exist yet).\n");
6855
+ }
6856
+ const config = loadConfig();
6857
+ if (!config) {
6858
+ throw new AgentBnBError("AgentBnB config still not found after auto-init", "CONFIG_NOT_FOUND");
6859
+ }
6860
+ process.stderr.write("[agentbnb] Agent initialized and published to AgentBnB network.\n");
6861
+ return config;
6862
+ }
6863
+ async function activate(config = {}, _onboardDeps) {
6811
6864
  if (config.agentDir) {
6812
6865
  process.env["AGENTBNB_DIR"] = config.agentDir;
6813
6866
  process.stderr.write(
@@ -6838,19 +6891,7 @@ async function activate(config = {}) {
6838
6891
  }
6839
6892
  let agentConfig = loadConfig();
6840
6893
  if (!agentConfig) {
6841
- const result = spawnSync("agentbnb", ["init", "--yes", "--no-detect"], {
6842
- stdio: "pipe",
6843
- env: { ...process.env },
6844
- encoding: "utf-8"
6845
- });
6846
- if (result.error || result.status !== 0) {
6847
- const msg = result.error?.message ?? result.stderr?.trim() ?? "agentbnb init failed";
6848
- throw new AgentBnBError(`Auto-init failed: ${msg}`, "INIT_FAILED");
6849
- }
6850
- agentConfig = loadConfig();
6851
- if (!agentConfig) {
6852
- throw new AgentBnBError("AgentBnB config still not found after auto-init", "CONFIG_NOT_FOUND");
6853
- }
6894
+ agentConfig = await autoOnboard(configDir, _onboardDeps);
6854
6895
  }
6855
6896
  process.stderr.write(
6856
6897
  `[agentbnb] activate: owner=${agentConfig.owner} config=${configDir}/config.json
@@ -6896,5 +6937,7 @@ async function deactivate(ctx) {
6896
6937
  }
6897
6938
  export {
6898
6939
  activate,
6899
- deactivate
6940
+ deactivate,
6941
+ findCli,
6942
+ runCommand
6900
6943
  };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "7.0.0",
6
+ "version": "8.0.1",
7
7
  "description": "P2P Agent Capability Sharing Protocol — Airbnb for AI agent pipelines",
8
8
  "type": "module",
9
9
  "main": "dist/index.js",
@@ -55,7 +55,7 @@ vi.mock('../../src/registry/store.js', () => ({
55
55
 
56
56
  import { loadConfig } from '../../src/cli/config.js';
57
57
  import { activate, deactivate } from './bootstrap.js';
58
- import type { BootstrapContext } from './bootstrap.js';
58
+ import type { BootstrapContext, OnboardDeps } from './bootstrap.js';
59
59
 
60
60
  const mockLoadConfig = vi.mocked(loadConfig);
61
61
 
@@ -104,16 +104,71 @@ describe('bootstrap activate/deactivate lifecycle', () => {
104
104
  });
105
105
 
106
106
  // ---------------------------------------------------------------------------
107
- // Test 1: CONFIG_NOT_FOUND when no config exists
107
+ // Test 1: INIT_FAILED when CLI not found and config missing
108
108
  // ---------------------------------------------------------------------------
109
- it('activate() throws CONFIG_NOT_FOUND when config is missing', async () => {
109
+ it('activate() throws INIT_FAILED when CLI not found and config missing', async () => {
110
110
  mockLoadConfig.mockReturnValue(null);
111
+ const deps: OnboardDeps = {
112
+ findCli: () => null,
113
+ runCommand: vi.fn(),
114
+ };
111
115
 
112
- await expect(activate()).rejects.toMatchObject({
113
- code: 'CONFIG_NOT_FOUND',
116
+ await expect(activate({}, deps)).rejects.toMatchObject({
117
+ code: 'INIT_FAILED',
114
118
  });
115
119
  });
116
120
 
121
+ // ---------------------------------------------------------------------------
122
+ // Test 1b: Auto-onboard runs init + openclaw sync when config missing
123
+ // ---------------------------------------------------------------------------
124
+ it('activate() auto-onboards when config missing and CLI available', async () => {
125
+ mockLoadConfig.mockReturnValueOnce(null).mockReturnValue(MINIMAL_CONFIG as ReturnType<typeof loadConfig>);
126
+ const mockRun = vi.fn().mockResolvedValue({ stdout: '', stderr: '' });
127
+ const deps: OnboardDeps = {
128
+ findCli: () => '/usr/local/bin/agentbnb',
129
+ runCommand: mockRun,
130
+ };
131
+
132
+ ctx = await activate({}, deps);
133
+
134
+ expect(mockRun).toHaveBeenCalledTimes(2);
135
+ expect(mockRun.mock.calls[0][0]).toMatch(/agentbnb init --owner .* --yes --no-detect/);
136
+ expect(mockRun.mock.calls[1][0]).toBe('agentbnb openclaw sync');
137
+ });
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Test 1c: Auto-onboard continues if openclaw sync fails
141
+ // ---------------------------------------------------------------------------
142
+ it('activate() continues if openclaw sync fails during auto-onboard', async () => {
143
+ mockLoadConfig.mockReturnValueOnce(null).mockReturnValue(MINIMAL_CONFIG as ReturnType<typeof loadConfig>);
144
+ const mockRun = vi.fn()
145
+ .mockResolvedValueOnce({ stdout: '', stderr: '' })
146
+ .mockRejectedValueOnce(new Error('SOUL.md not found'));
147
+ const deps: OnboardDeps = {
148
+ findCli: () => '/usr/local/bin/agentbnb',
149
+ runCommand: mockRun,
150
+ };
151
+
152
+ ctx = await activate({}, deps);
153
+
154
+ expect(ctx.startDisposition).toBe('started');
155
+ });
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Test 1d: Skips auto-onboard when config already exists
159
+ // ---------------------------------------------------------------------------
160
+ it('activate() skips auto-onboard when config already exists', async () => {
161
+ const mockRun = vi.fn();
162
+ const deps: OnboardDeps = {
163
+ findCli: () => '/usr/local/bin/agentbnb',
164
+ runCommand: mockRun,
165
+ };
166
+
167
+ ctx = await activate({}, deps);
168
+
169
+ expect(mockRun).not.toHaveBeenCalled();
170
+ });
171
+
117
172
  // ---------------------------------------------------------------------------
118
173
  // Test 2: BootstrapContext shape
119
174
  // ---------------------------------------------------------------------------
@@ -8,12 +8,15 @@
8
8
  * Teardown: `await deactivate(ctx);`
9
9
  */
10
10
 
11
- import { join } from 'node:path';
11
+ import { join, basename, dirname } from 'node:path';
12
12
  import { existsSync } from 'node:fs';
13
13
  import { homedir } from 'node:os';
14
- import { spawnSync } from 'node:child_process';
14
+ import { spawnSync, exec } from 'node:child_process';
15
+ import { promisify } from 'node:util';
15
16
  import { randomUUID } from 'node:crypto';
16
17
 
18
+ const execAsync = promisify(exec);
19
+
17
20
  /**
18
21
  * Derives a workspace-specific AGENTBNB_DIR using a priority chain:
19
22
  *
@@ -170,6 +173,109 @@ function registerDecomposerCard(configDir: string, owner: string): void {
170
173
  }
171
174
  }
172
175
 
176
+ /**
177
+ * Checks if the `agentbnb` CLI is available in PATH.
178
+ * @returns Absolute path to the CLI, or null if not found.
179
+ */
180
+ export function findCli(): string | null {
181
+ const result = spawnSync('which', ['agentbnb'], { encoding: 'utf-8', stdio: 'pipe' });
182
+ if (result.status === 0 && result.stdout.trim()) {
183
+ return result.stdout.trim();
184
+ }
185
+ return null;
186
+ }
187
+
188
+ /**
189
+ * Runs a shell command asynchronously. Exported for test injection.
190
+ */
191
+ export async function runCommand(cmd: string, env: Record<string, string | undefined>): Promise<{ stdout: string; stderr: string }> {
192
+ return execAsync(cmd, { env });
193
+ }
194
+
195
+ /**
196
+ * Derives a human-readable agent name from the config directory path.
197
+ * If configDir is `~/.openclaw/agents/genesis-bot/.agentbnb`, returns "genesis-bot".
198
+ * Falls back to a random identifier.
199
+ */
200
+ function deriveAgentName(configDir: string): string {
201
+ // configDir is typically <workspace>/.agentbnb — parent dir is the agent workspace
202
+ const parent = basename(dirname(configDir));
203
+ if (parent && parent !== '.' && parent !== '.agentbnb' && parent !== homedir().split('/').pop()) {
204
+ return parent;
205
+ }
206
+ return `agent-${randomUUID().slice(0, 8)}`;
207
+ }
208
+
209
+ /**
210
+ * First-time auto-onboarding: initializes identity, publishes capabilities, and
211
+ * grants the Demand Voucher (50 credits). Called when activate() detects no config.json.
212
+ *
213
+ * Steps:
214
+ * 1. Check agentbnb CLI is available
215
+ * 2. Run `agentbnb init --owner <name> --yes --no-detect` (keypair + config)
216
+ * 3. Run `agentbnb openclaw sync` (publish SOUL.md capabilities)
217
+ * 4. Demand Voucher is auto-issued by the registry on first registration
218
+ *
219
+ * @param configDir - The AGENTBNB_DIR for this agent.
220
+ * @returns The loaded AgentBnBConfig after init.
221
+ * @throws {AgentBnBError} INIT_FAILED if CLI not found or init fails.
222
+ */
223
+ /** Injectable dependencies for autoOnboard (test seam). */
224
+ export interface OnboardDeps {
225
+ findCli: () => string | null;
226
+ runCommand: (cmd: string, env: Record<string, string | undefined>) => Promise<{ stdout: string; stderr: string }>;
227
+ }
228
+
229
+ /** Default production dependencies. */
230
+ const defaultDeps: OnboardDeps = { findCli, runCommand };
231
+
232
+ async function autoOnboard(configDir: string, deps: OnboardDeps = defaultDeps): Promise<import('./../../src/cli/config.js').AgentBnBConfig> {
233
+ process.stderr.write('[agentbnb] First-time setup: initializing agent identity...\n');
234
+
235
+ // Step 0: Check CLI exists
236
+ const cliPath = deps.findCli();
237
+ if (!cliPath) {
238
+ process.stderr.write('[agentbnb] CLI not found. Run: npm install -g agentbnb\n');
239
+ throw new AgentBnBError(
240
+ 'agentbnb CLI not found in PATH. Install with: npm install -g agentbnb',
241
+ 'INIT_FAILED',
242
+ );
243
+ }
244
+
245
+ const env = { ...process.env, AGENTBNB_DIR: configDir };
246
+ const agentName = deriveAgentName(configDir);
247
+
248
+ // Step 1: Initialize identity (keypair + config.json + credit bootstrap)
249
+ try {
250
+ await deps.runCommand(`agentbnb init --owner "${agentName}" --yes --no-detect`, env);
251
+ process.stderr.write(`[agentbnb] Agent "${agentName}" initialized.\n`);
252
+ } catch (err) {
253
+ const msg = err instanceof Error ? err.message : String(err);
254
+ throw new AgentBnBError(`Auto-init failed: ${msg}`, 'INIT_FAILED');
255
+ }
256
+
257
+ // Step 2: Publish capabilities from SOUL.md (if it exists)
258
+ try {
259
+ await deps.runCommand('agentbnb openclaw sync', env);
260
+ process.stderr.write('[agentbnb] Capabilities published from SOUL.md.\n');
261
+ } catch {
262
+ // Non-fatal: SOUL.md may not exist yet, or sync may fail for other reasons.
263
+ // Agent is still initialized and can publish capabilities later.
264
+ process.stderr.write('[agentbnb] Note: openclaw sync skipped (SOUL.md may not exist yet).\n');
265
+ }
266
+
267
+ // Step 3: Demand Voucher (50 credits) is auto-issued by bootstrapAgent() during init.
268
+ // No action needed here.
269
+
270
+ const config = loadConfig();
271
+ if (!config) {
272
+ throw new AgentBnBError('AgentBnB config still not found after auto-init', 'CONFIG_NOT_FOUND');
273
+ }
274
+
275
+ process.stderr.write('[agentbnb] Agent initialized and published to AgentBnB network.\n');
276
+ return config;
277
+ }
278
+
173
279
  /**
174
280
  * Brings an AgentBnB node online (idempotent — safe to call when already running).
175
281
  * Registers SIGTERM/SIGINT handlers that conditionally stop the node on process exit.
@@ -179,7 +285,7 @@ function registerDecomposerCard(configDir: string, owner: string): void {
179
285
  * TODO: Once ServiceCoordinator gains its own signal handling, remove the handlers
180
286
  * registered here to avoid double-handler conflicts. Track in Layer A implementation.
181
287
  */
182
- export async function activate(config: BootstrapConfig = {}): Promise<BootstrapContext> {
288
+ export async function activate(config: BootstrapConfig = {}, _onboardDeps?: OnboardDeps): Promise<BootstrapContext> {
183
289
  // Per-workspace isolation: determine the correct config directory.
184
290
  // Priority: config.agentDir > config.workspaceDir/.agentbnb > AGENTBNB_DIR env > resolveWorkspaceDir()
185
291
  if (config.agentDir) {
@@ -211,20 +317,8 @@ export async function activate(config: BootstrapConfig = {}): Promise<BootstrapC
211
317
 
212
318
  let agentConfig = loadConfig();
213
319
  if (!agentConfig) {
214
- // Auto-init for first-time OpenClaw plugin activation
215
- const result = spawnSync('agentbnb', ['init', '--yes', '--no-detect'], {
216
- stdio: 'pipe',
217
- env: { ...process.env },
218
- encoding: 'utf-8',
219
- });
220
- if (result.error || result.status !== 0) {
221
- const msg = result.error?.message ?? (result.stderr as string | null)?.trim() ?? 'agentbnb init failed';
222
- throw new AgentBnBError(`Auto-init failed: ${msg}`, 'INIT_FAILED');
223
- }
224
- agentConfig = loadConfig();
225
- if (!agentConfig) {
226
- throw new AgentBnBError('AgentBnB config still not found after auto-init', 'CONFIG_NOT_FOUND');
227
- }
320
+ // First-time setup: auto-onboard this agent onto the AgentBnB network.
321
+ agentConfig = await autoOnboard(configDir, _onboardDeps);
228
322
  }
229
323
 
230
324
  // Print startup diagnostic so it's always visible in agent logs.