@dp-pcs/ogp 0.2.30 → 0.3.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.
@@ -0,0 +1,472 @@
1
+ # Platform-Agnostic OGP Architecture
2
+
3
+ > Design principles and implementation plan for making OGP work with any AI assistant platform
4
+
5
+ ## Executive Summary
6
+
7
+ OGP is designed to enable federation between AI assistants regardless of their underlying platform. The core protocol (Ed25519 signatures, peer management, scope enforcement) is platform-agnostic. Only the **notification mechanism** (how OGP communicates with the local agent) needs to be adapted per platform.
8
+
9
+ ## Core Design Principle
10
+
11
+ **One OGP daemon instance per AI assistant instance.**
12
+
13
+ Each OGP daemon is:
14
+ - **Independent**: Own port, state directory, keypair, configuration
15
+ - **Platform-agnostic**: Core protocol implementation never changes
16
+ - **Notification-pluggable**: Backend adapter for communicating with local agent
17
+
18
+ This design allows:
19
+ - Multiple AI platforms to federate seamlessly
20
+ - Multiple OGP instances on the same machine (for testing or multi-agent setups)
21
+ - Easy addition of new platforms without modifying core protocol
22
+
23
+ ## Architecture Layers
24
+
25
+ ```
26
+ ┌─────────────────────────────────────────────────────────────┐
27
+ │ Layer 1: Core OGP Protocol (Platform-Agnostic) │
28
+ │ - Ed25519 signing/verification │
29
+ │ - Peer management (peers.json) │
30
+ │ - Scope enforcement (Doorman) │
31
+ │ - Message routing (intent handlers) │
32
+ │ - HTTP endpoints (/federation/*, /.well-known/ogp) │
33
+ │ - Rendezvous integration │
34
+ └────────────────────────┬────────────────────────────────────┘
35
+
36
+
37
+ ┌─────────────────────────────────────────────────────────────┐
38
+ │ Layer 2: Notification Backend (Platform-Specific) │
39
+ │ │
40
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
41
+ │ │ OpenClaw │ │ Hermes │ │ Future │ │
42
+ │ │ Backend │ │ Backend │ │ Platform │ │
43
+ │ │ │ │ │ │ Backend │ │
44
+ │ │ POST /hooks/ │ │ POST webhook │ │ POST /api/v1/│ │
45
+ │ │ agent │ │ /ogp_fed │ │ messages │ │
46
+ │ └──────────────┘ └──────────────┘ └──────────────┘ │
47
+ └─────────────────────────────────────────────────────────────┘
48
+ ```
49
+
50
+ ## Notification Backend System
51
+
52
+ ### Current Implementation (OpenClaw-Only)
53
+
54
+ ```typescript
55
+ // src/daemon/notify.ts
56
+ async function notifyOpenClaw(
57
+ peerId: string,
58
+ intent: string,
59
+ payload: any
60
+ ): Promise<void> {
61
+ const openclawUrl = config.openclawUrl || "http://localhost:18789";
62
+ const token = config.openclawToken;
63
+
64
+ if (config.hooks?.enabled && config.openclawHooksToken) {
65
+ // Method 1: Webhook (preferred)
66
+ await fetch(`${openclawUrl}/hooks/agent`, {
67
+ method: "POST",
68
+ headers: {
69
+ "Content-Type": "application/json",
70
+ "Authorization": `Bearer ${config.openclawHooksToken}`
71
+ },
72
+ body: JSON.stringify({
73
+ agentId: config.agentId,
74
+ peerId,
75
+ intent,
76
+ topic: payload.topic,
77
+ message: payload.message,
78
+ notifyTarget: resolveNotifyTarget(config.agentId),
79
+ timestamp: new Date().toISOString()
80
+ })
81
+ });
82
+ } else {
83
+ // Method 2: CLI fallback
84
+ await execAsync(`openclaw system event --mode now`);
85
+ }
86
+ }
87
+ ```
88
+
89
+ ### Proposed Refactor (Platform-Agnostic)
90
+
91
+ ```typescript
92
+ // src/daemon/notify.ts
93
+
94
+ interface NotificationBackend {
95
+ name: string;
96
+ notify(context: NotificationContext): Promise<void>;
97
+ }
98
+
99
+ interface NotificationContext {
100
+ peerId: string;
101
+ peerDisplayName: string;
102
+ intent: string;
103
+ payload: any;
104
+ timestamp: string;
105
+ }
106
+
107
+ class OpenClawBackend implements NotificationBackend {
108
+ name = "openclaw";
109
+
110
+ async notify(ctx: NotificationContext): Promise<void> {
111
+ const url = config.openclawUrl || "http://localhost:18789";
112
+
113
+ if (config.hooks?.enabled && config.openclawHooksToken) {
114
+ await fetch(`${url}/hooks/agent`, {
115
+ method: "POST",
116
+ headers: {
117
+ "Content-Type": "application/json",
118
+ "Authorization": `Bearer ${config.openclawHooksToken}`
119
+ },
120
+ body: JSON.stringify({
121
+ agentId: config.agentId,
122
+ peerId: ctx.peerId,
123
+ intent: ctx.intent,
124
+ topic: ctx.payload.topic,
125
+ message: ctx.payload.message,
126
+ notifyTarget: resolveNotifyTarget(config.agentId),
127
+ timestamp: ctx.timestamp
128
+ })
129
+ });
130
+ } else {
131
+ await execAsync(`openclaw system event --mode now`);
132
+ }
133
+ }
134
+ }
135
+
136
+ class HermesBackend implements NotificationBackend {
137
+ name = "hermes";
138
+
139
+ async notify(ctx: NotificationContext): Promise<void> {
140
+ const webhookUrl = config.hermesWebhookUrl || "http://localhost:8644/webhooks/ogp_federation";
141
+ const secret = config.hermesWebhookSecret;
142
+
143
+ const body = {
144
+ peer_id: ctx.peerId,
145
+ peer_display_name: ctx.peerDisplayName,
146
+ intent: ctx.intent,
147
+ topic: ctx.payload.topic || "",
148
+ message: ctx.payload.message || "",
149
+ priority: ctx.payload.priority || "normal",
150
+ conversation_id: ctx.payload.conversationId,
151
+ timestamp: ctx.timestamp,
152
+ ...ctx.payload
153
+ };
154
+
155
+ const signature = computeHMAC(JSON.stringify(body), secret);
156
+
157
+ await fetch(webhookUrl, {
158
+ method: "POST",
159
+ headers: {
160
+ "Content-Type": "application/json",
161
+ "X-Hub-Signature-256": `sha256=${signature}`
162
+ },
163
+ body: JSON.stringify(body)
164
+ });
165
+ }
166
+ }
167
+
168
+ // Factory for backend selection
169
+ function getNotificationBackend(): NotificationBackend {
170
+ const platform = config.platform || "openclaw";
171
+
172
+ switch (platform) {
173
+ case "openclaw":
174
+ return new OpenClawBackend();
175
+ case "hermes":
176
+ return new HermesBackend();
177
+ default:
178
+ throw new Error(`Unknown platform: ${platform}`);
179
+ }
180
+ }
181
+
182
+ // Main notification entry point
183
+ export async function notifyLocalAgent(
184
+ peerId: string,
185
+ peerDisplayName: string,
186
+ intent: string,
187
+ payload: any
188
+ ): Promise<void> {
189
+ const backend = getNotificationBackend();
190
+
191
+ await backend.notify({
192
+ peerId,
193
+ peerDisplayName,
194
+ intent,
195
+ payload,
196
+ timestamp: new Date().toISOString()
197
+ });
198
+ }
199
+ ```
200
+
201
+ ### Configuration Schema
202
+
203
+ ```json
204
+ // ~/.ogp/config.json (OpenClaw instance)
205
+ {
206
+ "daemonPort": 18790,
207
+ "platform": "openclaw",
208
+ "gatewayUrl": "https://alice-openclaw.example.com",
209
+ "displayName": "Alice (OpenClaw)",
210
+ "email": "alice@example.com",
211
+ "stateDir": "~/.ogp",
212
+
213
+ "openclawUrl": "http://localhost:18789",
214
+ "openclawToken": "...",
215
+ "openclawHooksToken": "...",
216
+ "agentId": "main",
217
+ "notifyTarget": "telegram:123456789",
218
+
219
+ "rendezvous": {
220
+ "enabled": true,
221
+ "url": "https://rendezvous.elelem.expert"
222
+ }
223
+ }
224
+ ```
225
+
226
+ ```json
227
+ // ~/.ogp-hermes/config.json (Hermes instance)
228
+ {
229
+ "daemonPort": 18791,
230
+ "platform": "hermes",
231
+ "gatewayUrl": "https://alice-hermes.example.com",
232
+ "displayName": "Alice (Hermes)",
233
+ "email": "alice@example.com",
234
+ "stateDir": "~/.ogp-hermes",
235
+
236
+ "hermesWebhookUrl": "http://localhost:8644/webhooks/ogp_federation",
237
+ "hermesWebhookSecret": "shared-secret-here",
238
+
239
+ "rendezvous": {
240
+ "enabled": true,
241
+ "url": "https://rendezvous.elelem.expert"
242
+ }
243
+ }
244
+ ```
245
+
246
+ ## Multi-Instance Setup (Local Testing)
247
+
248
+ ### Running Both OpenClaw and Hermes Locally
249
+
250
+ **Step 1: Configure Hermes Webhook**
251
+
252
+ ```yaml
253
+ # ~/.hermes/config.yaml
254
+ platforms:
255
+ webhook:
256
+ enabled: true
257
+ port: 8644
258
+ host: "127.0.0.1"
259
+ routes:
260
+ ogp_federation:
261
+ secret: "shared-secret-here"
262
+ events: ["*"] # Accept all OGP intents
263
+ prompt: |
264
+ 📡 **OGP Federation Message**
265
+
266
+ **From:** {{peer_display_name}} ({{peer_id}})
267
+ **Intent:** {{intent}}
268
+ {{#if topic}}**Topic:** {{topic}}{{/if}}
269
+ {{#if priority}}**Priority:** {{priority}}{{/if}}
270
+
271
+ {{message}}
272
+ deliver: "telegram" # Or wherever you want responses
273
+ deliver_extra:
274
+ chat_id: "your-telegram-chat-id"
275
+ ```
276
+
277
+ **Step 2: Start OGP for OpenClaw (existing)**
278
+
279
+ ```bash
280
+ # Uses ~/.ogp and port 18790
281
+ ogp start
282
+ ```
283
+
284
+ **Step 3: Start OGP for Hermes (new instance)**
285
+
286
+ ```bash
287
+ # Create separate state directory
288
+ mkdir -p ~/.ogp-hermes
289
+
290
+ # Create config (see above)
291
+ cat > ~/.ogp-hermes/config.json <<EOF
292
+ {
293
+ "daemonPort": 18791,
294
+ "platform": "hermes",
295
+ "gatewayUrl": "http://localhost:18791",
296
+ "displayName": "Alice (Hermes)",
297
+ "email": "alice@example.com",
298
+ "stateDir": "~/.ogp-hermes",
299
+ "hermesWebhookUrl": "http://localhost:8644/webhooks/ogp_federation",
300
+ "hermesWebhookSecret": "shared-secret-here"
301
+ }
302
+ EOF
303
+
304
+ # Start second OGP daemon
305
+ OGP_STATE_DIR=~/.ogp-hermes ogp start --port 18791
306
+ ```
307
+
308
+ **Step 4: Federate the Two Local Instances**
309
+
310
+ ```bash
311
+ # From OpenClaw's OGP, request federation with Hermes's OGP
312
+ ogp federation request http://localhost:18791 --alias hermes-local
313
+
314
+ # From Hermes's OGP, approve the request
315
+ OGP_STATE_DIR=~/.ogp-hermes ogp federation approve <openclaw-peer-id>
316
+
317
+ # Now send a message from OpenClaw to Hermes
318
+ ogp federation send hermes-local message '{"text":"Hello from OpenClaw!"}'
319
+ ```
320
+
321
+ The message will:
322
+ 1. Leave OpenClaw's OGP (port 18790)
323
+ 2. Arrive at Hermes's OGP (port 18791)
324
+ 3. Be verified by Doorman
325
+ 4. POST to Hermes webhook (port 8644)
326
+ 5. Hermes processes and responds
327
+
328
+ ### Why This Works
329
+
330
+ 1. **Independent Keypairs**: Each OGP instance generates its own Ed25519 keypair
331
+ 2. **Independent Peer Lists**: `~/.ogp/peers.json` vs `~/.ogp-hermes/peers.json`
332
+ 3. **No Port Conflicts**: 18790 vs 18791
333
+ 4. **Standard P2P Flow**: Same cryptographic verification as if they were remote
334
+
335
+ ## Implementation Phases
336
+
337
+ ### Phase 1: Sidecar Integration (Week 1)
338
+
339
+ **Goal:** Get Hermes working with OGP via webhook without modifying core protocol
340
+
341
+ **Tasks:**
342
+ 1. ✅ Document architecture (this file)
343
+ 2. Add Hermes notification backend to `src/daemon/notify.ts`
344
+ 3. Add `platform` config field to config schema
345
+ 4. Test with local multi-instance setup
346
+ 5. Update CLI to support `OGP_STATE_DIR` environment variable
347
+
348
+ **Deliverables:**
349
+ - OGP works with both OpenClaw and Hermes
350
+ - Can run multiple instances simultaneously
351
+ - Documentation for setup
352
+
353
+ ### Phase 2: Native Hermes Platform (Month 1-2)
354
+
355
+ **Goal:** Create Hermes platform adapter that speaks OGP natively
356
+
357
+ **Tasks:**
358
+ 1. Create `~/.hermes/hermes-agent/gateway/platforms/ogp.py`
359
+ 2. Implement Ed25519 crypto using Python `cryptography` library
360
+ 3. Implement Doorman access control
361
+ 4. Implement peer management (JSON storage)
362
+ 5. Implement HTTP endpoints (`/.well-known/ogp`, `/federation/*`)
363
+ 6. Test interoperability with Node.js OGP instances
364
+
365
+ **Deliverables:**
366
+ - Hermes can speak OGP without separate daemon
367
+ - Full protocol compatibility
368
+ - Performance benchmarks
369
+
370
+ ### Phase 3: Protocol Standardization (Month 3+)
371
+
372
+ **Goal:** Formalize OGP as a true multi-platform standard
373
+
374
+ **Tasks:**
375
+ 1. Extract protocol specification into standalone repo
376
+ 2. Create reference test suite
377
+ 3. Add protocol versioning and negotiation
378
+ 4. Support for other platforms (claude-code, OpenHands, etc.)
379
+ 5. Performance optimizations
380
+ 6. Security audit
381
+
382
+ **Deliverables:**
383
+ - OGP protocol specification v1.0
384
+ - Reference implementations: Node.js, Python
385
+ - Compliance test suite
386
+
387
+ ## Remote Federation (Typical Use Case)
388
+
389
+ When users are on different machines (the normal scenario):
390
+
391
+ ```
392
+ ┌─────────────────────────┐ ┌─────────────────────────┐
393
+ │ Alice's Machine │ │ Bob's Machine │
394
+ │ │ │ │
395
+ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
396
+ │ │ OGP Daemon │◄├───────────┤►│ OGP Daemon │ │
397
+ │ │ :18790 │ │ Internet │ │ :18790 │ │
398
+ │ │ │ │ (HTTPS) │ │ │ │
399
+ │ │ Platform: OpenClaw │ │ │ │ Platform: Hermes │ │
400
+ │ └──────────┬──────────┘ │ │ └──────────┬──────────┘ │
401
+ │ │ │ │ │ │
402
+ │ ▼ │ │ ▼ │
403
+ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
404
+ │ │ OpenClaw Instance │ │ │ │ Hermes Instance │ │
405
+ │ │ :18789 │ │ │ │ Webhook :8644 │ │
406
+ │ └─────────────────────┘ │ │ └─────────────────────┘ │
407
+ └─────────────────────────┘ └─────────────────────────┘
408
+ ```
409
+
410
+ **Key Point:** Alice doesn't need to know Bob uses Hermes. Bob doesn't need to know Alice uses OpenClaw. The OGP protocol is the same; only the local notification mechanism differs.
411
+
412
+ ## Benefits of This Architecture
413
+
414
+ 1. **True Platform Independence**: Core protocol never changes
415
+ 2. **Easy Testing**: Can run multiple instances locally
416
+ 3. **No Breaking Changes**: OpenClaw integration continues working unchanged
417
+ 4. **Extensible**: Add new platforms by implementing notification backend
418
+ 5. **Federation is P2P**: Each instance is independent, regardless of platform
419
+ 6. **Router Analogy Holds**: Like routers running BGP regardless of vendor
420
+
421
+ ## Security Considerations
422
+
423
+ ### Running Multiple Local Instances
424
+
425
+ When running multiple OGP instances on the same machine:
426
+ - Each instance has its own keypair (different identities)
427
+ - Localhost federation is cryptographically identical to remote federation
428
+ - Useful for testing but not a security boundary
429
+ - Each instance can have different scope policies
430
+
431
+ ### Production Deployment
432
+
433
+ For remote federation:
434
+ - Always use HTTPS tunnels (cloudflared, ngrok)
435
+ - Verify peer identity out-of-band before approval
436
+ - Use scope negotiation to limit what peers can access
437
+ - Monitor peer activity via agent-comms logs
438
+
439
+ ## Next Steps
440
+
441
+ 1. **Immediate:** Implement Hermes notification backend in `notify.ts`
442
+ 2. **Week 1:** Test local multi-instance federation
443
+ 3. **Week 2:** Document Hermes setup for OGP users
444
+ 4. **Month 1:** Begin native Hermes platform adapter
445
+ 5. **Month 2:** Interoperability testing
446
+ 6. **Month 3:** Protocol specification v1.0
447
+
448
+ ## Questions & Answers
449
+
450
+ **Q: Will adding Hermes support break OpenClaw integration?**
451
+ A: No. The changes are additive (new notification backend). OpenClaw continues using the existing backend.
452
+
453
+ **Q: Can I federate OpenClaw and Hermes on the same machine?**
454
+ A: Yes! Run two OGP instances with different ports and state directories.
455
+
456
+ **Q: Do I need to run separate OGP daemons for different platforms?**
457
+ A: Yes, in the multi-instance model. Each agent instance gets its own OGP daemon. This keeps federation truly P2P.
458
+
459
+ **Q: What if I want one OGP daemon to notify multiple platforms?**
460
+ A: Not recommended. The clean architecture is one daemon per agent. Multi-platform notification adds complexity and breaks the P2P model.
461
+
462
+ **Q: How does this scale to future platforms (Anthropic Claude, OpenHands, etc.)?**
463
+ A: Add a new notification backend class. The core protocol stays unchanged.
464
+
465
+ **Q: Can the native Hermes adapter replace the sidecar?**
466
+ A: Eventually, yes. The native adapter implements the full OGP protocol in Python, eliminating the need for the Node.js daemon.
467
+
468
+ ---
469
+
470
+ **Last Updated:** 2026-04-04
471
+ **OGP Version:** 0.2.31
472
+ **Status:** Design Document
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dp-pcs/ogp",
3
- "version": "0.2.30",
3
+ "version": "0.3.0",
4
4
  "description": "Open Gateway Protocol (OGP) - Peer-to-peer federation daemon for OpenClaw AI gateways with cryptographic signatures",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,7 +12,8 @@
12
12
  "scripts": {
13
13
  "build": "tsc",
14
14
  "dev": "tsc --watch",
15
- "prepublishOnly": "npm run build"
15
+ "prepublishOnly": "npm run build",
16
+ "postinstall": "node scripts/install-skills.js || echo 'Note: Run ogp-install-skills to install OGP skills for your AI agent'"
16
17
  },
17
18
  "keywords": [
18
19
  "ogp",
@@ -5,10 +5,50 @@ import { homedir } from 'node:os';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { dirname } from 'node:path';
7
7
  import { createInterface } from 'node:readline';
8
+ import { execSync } from 'node:child_process';
8
9
 
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
11
  const skillsSrc = join(__dirname, '..', 'skills');
11
- const skillsDest = join(homedir(), '.openclaw', 'skills');
12
+
13
+ // Detect which AI platform(s) are installed
14
+ function detectPlatforms() {
15
+ const platforms = [];
16
+
17
+ // Check for OpenClaw
18
+ if (existsSync(join(homedir(), '.openclaw')) || existsSync(join(homedir(), '.claw'))) {
19
+ platforms.push({
20
+ name: 'OpenClaw',
21
+ skillsDir: join(homedir(), '.openclaw', 'skills'),
22
+ check: () => existsSync(join(homedir(), '.openclaw')) || existsSync(join(homedir(), '.claw'))
23
+ });
24
+ }
25
+
26
+ // Check for Hermes
27
+ if (existsSync(join(homedir(), '.hermes'))) {
28
+ platforms.push({
29
+ name: 'Hermes',
30
+ skillsDir: join(homedir(), '.claude', 'skills'),
31
+ check: () => existsSync(join(homedir(), '.hermes'))
32
+ });
33
+ }
34
+
35
+ // Check for Claude Code (generic)
36
+ if (existsSync(join(homedir(), '.claude', 'skills'))) {
37
+ platforms.push({
38
+ name: 'Claude Code',
39
+ skillsDir: join(homedir(), '.claude', 'skills'),
40
+ check: () => existsSync(join(homedir(), '.claude'))
41
+ });
42
+ }
43
+
44
+ // Remove duplicates by skillsDir path
45
+ const seen = new Set();
46
+ return platforms.filter(p => {
47
+ if (seen.has(p.skillsDir)) return false;
48
+ seen.add(p.skillsDir);
49
+ return true;
50
+ });
51
+ }
12
52
 
13
53
  // Auto-discover all skill folders
14
54
  const availableSkills = readdirSync(skillsSrc, { withFileTypes: true })
@@ -67,33 +107,115 @@ async function selectSkills() {
67
107
  return indices.map(i => availableSkills[i]);
68
108
  }
69
109
 
70
- async function main() {
71
- const selected = await selectSkills();
110
+ async function selectPlatforms(platforms) {
111
+ if (!isInteractive || platforms.length === 1) {
112
+ // Non-interactive or only one platform: install to all
113
+ return platforms;
114
+ }
115
+
116
+ console.log('\n🤖 Detected AI Platforms:\n');
117
+ platforms.forEach((p, i) => {
118
+ console.log(` ${i + 1}. ${p.name} (${p.skillsDir})`);
119
+ });
120
+ console.log(` ${platforms.length + 1}. Install to all`);
121
+ console.log(` 0. Cancel\n`);
122
+
123
+ const answer = await prompt(`Select platforms to install to (e.g. "1 2" or "${platforms.length + 1}" for all): `);
124
+
125
+ if (!answer || answer === '0') {
126
+ console.log('Cancelled.');
127
+ process.exit(0);
128
+ }
72
129
 
73
- if (!existsSync(skillsDest)) {
74
- mkdirSync(skillsDest, { recursive: true });
130
+ if (answer === String(platforms.length + 1) || answer.toLowerCase() === 'all') {
131
+ return platforms;
132
+ }
133
+
134
+ const indices = answer.split(/[\s,]+/).map(n => parseInt(n) - 1).filter(i => i >= 0 && i < platforms.length);
135
+ if (indices.length === 0) {
136
+ console.log('No valid selection. Installing to all platforms.');
137
+ return platforms;
138
+ }
139
+
140
+ return indices.map(i => platforms[i]);
141
+ }
142
+
143
+ async function main() {
144
+ const detectedPlatforms = detectPlatforms();
145
+
146
+ if (detectedPlatforms.length === 0) {
147
+ console.log('\n⚠️ No supported AI platforms detected.');
148
+ console.log('');
149
+ console.log('OGP skills can be installed to:');
150
+ console.log(' - OpenClaw: ~/.openclaw/skills/');
151
+ console.log(' - Hermes: ~/.claude/skills/');
152
+ console.log(' - Claude Code: ~/.claude/skills/');
153
+ console.log('');
154
+ console.log('To install manually:');
155
+ console.log(' cp -r $(npm root -g)/@dp-pcs/ogp/skills/* ~/.openclaw/skills/');
156
+ console.log(' # or');
157
+ console.log(' cp -r $(npm root -g)/@dp-pcs/ogp/skills/* ~/.claude/skills/');
158
+ process.exit(1);
75
159
  }
76
160
 
77
- console.log('');
78
- let installed = 0;
79
- for (const skill of selected) {
80
- const src = join(skillsSrc, skill);
81
- const dest = join(skillsDest, skill);
82
- try {
83
- cpSync(src, dest, { recursive: true });
84
- console.log(`✓ Installed: ${skill} → ${dest}`);
85
- installed++;
86
- } catch (err) {
87
- console.warn(`✗ Failed to install ${skill}: ${err.message}`);
161
+ const selectedSkills = await selectSkills();
162
+ const selectedPlatforms = await selectPlatforms(detectedPlatforms);
163
+
164
+ let totalInstalled = 0;
165
+
166
+ for (const platform of selectedPlatforms) {
167
+ console.log(`\n📥 Installing to ${platform.name} (${platform.skillsDir})...`);
168
+
169
+ if (!existsSync(platform.skillsDir)) {
170
+ mkdirSync(platform.skillsDir, { recursive: true });
171
+ console.log(` Created directory: ${platform.skillsDir}`);
172
+ }
173
+
174
+ let installed = 0;
175
+ for (const skill of selectedSkills) {
176
+ const src = join(skillsSrc, skill);
177
+ const dest = join(platform.skillsDir, skill);
178
+ try {
179
+ cpSync(src, dest, { recursive: true });
180
+ console.log(` ✓ ${skill}`);
181
+ installed++;
182
+ } catch (err) {
183
+ console.warn(` ✗ ${skill}: ${err.message}`);
184
+ }
88
185
  }
186
+
187
+ console.log(` ${installed}/${selectedSkills.length} skills installed`);
188
+ totalInstalled += installed;
89
189
  }
90
190
 
91
- if (installed > 0) {
92
- console.log(`\n${installed} skill(s) installed. Restart your OpenClaw gateway to load them.`);
191
+ if (totalInstalled > 0) {
192
+ console.log(`\n✅ Successfully installed ${totalInstalled} skill(s) to ${selectedPlatforms.length} platform(s)`);
193
+ console.log('');
194
+
195
+ // Platform-specific restart instructions
196
+ const platformNames = selectedPlatforms.map(p => p.name);
197
+ if (platformNames.includes('OpenClaw')) {
198
+ console.log('OpenClaw: Restart your gateway to load the skills');
199
+ console.log(' openclaw restart');
200
+ }
201
+ if (platformNames.includes('Hermes')) {
202
+ console.log('Hermes: Skills are loaded automatically');
203
+ console.log(' hermes skills list (to verify)');
204
+ }
205
+ if (platformNames.includes('Claude Code')) {
206
+ console.log('Claude Code: Restart Claude Code to load the skills');
207
+ }
208
+ } else {
209
+ console.log('\n⚠️ No skills were installed');
93
210
  }
94
211
  }
95
212
 
96
213
  main().catch(err => {
97
- console.warn('Install failed:', err.message);
98
- console.warn('Manual install: cp -r $(npm root -g)/@dp-pcs/ogp/skills/ogp ~/.openclaw/skills/');
214
+ console.warn('\n❌ Install failed:', err.message);
215
+ console.warn('');
216
+ console.warn('Manual install:');
217
+ console.warn(' cp -r $(npm root -g)/@dp-pcs/ogp/skills/* ~/.openclaw/skills/');
218
+ console.warn(' # or');
219
+ console.warn(' cp -r $(npm root -g)/@dp-pcs/ogp/skills/* ~/.claude/skills/');
220
+ process.exit(1);
99
221
  });