@highway1/cli 0.1.53 → 0.1.54

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.
@@ -1,299 +0,0 @@
1
- import { Command } from 'commander';
2
- import {
3
- createRelayClient,
4
- createRelayIndexOperations,
5
- importKeyPair,
6
- createEnvelope,
7
- signEnvelope,
8
- createMessageRouter,
9
- createAgentCard,
10
- signAgentCard,
11
- sign,
12
- verify,
13
- extractPublicKey,
14
- } from '@highway1/core';
15
- import { getIdentity, getAgentCard } from '../config.js';
16
- import { success, error, spinner, printHeader, info } from '../ui.js';
17
- import { readFile } from 'node:fs/promises';
18
- import { basename } from 'node:path';
19
- import { DaemonClient } from '../daemon/client.js';
20
-
21
- // Default relay URLs (CVP-0011)
22
- const DEFAULT_RELAY_URLS = [
23
- 'ws://relay.highway1.net:8080',
24
- ];
25
-
26
- function getRelayUrls(options: any): string[] {
27
- if (options.relay) return [options.relay];
28
- const envRelays = process.env.HW1_RELAY_URLS;
29
- if (envRelays) return envRelays.split(',').map((u: string) => u.trim());
30
- return DEFAULT_RELAY_URLS;
31
- }
32
-
33
- export function registerSendCommand(program: Command): void {
34
- program
35
- .command('send')
36
- .description('Send a message to another agent')
37
- .requiredOption('--to <did-or-name>', 'Recipient DID or agent name')
38
- .option('--protocol <protocol>', 'Protocol identifier', '/clawiverse/msg/1.0.0') // CVP-0010 §3.1: default protocol
39
- .option('--message <text>', 'Message text (shorthand for --payload \'{"text":"..."}\')') // CVP-0010 §3.1: --message shorthand
40
- .option('--payload <json>', 'Message payload (JSON)')
41
- .option('--file <path>', 'Attach a file (image, binary, text) as payload attachment')
42
- .option('--type <type>', 'Message type (request|notification)', 'request') // CVP-0010 §3.1: default type
43
- .option('--relay <url>', 'Relay WebSocket URL')
44
- .option('--format <fmt>', 'Output format: text|json', 'text') // CVP-0010 §3.2: --format json
45
- .action(async (options) => {
46
- try {
47
- if (options.format !== 'json') printHeader('Send Message');
48
-
49
- const identity = getIdentity();
50
-
51
- if (!identity) {
52
- error('No identity found. Run "hw1 init" first.');
53
- process.exit(1);
54
- }
55
-
56
- // CVP-0010 §3.1: --message shorthand
57
- if (!options.payload && !options.file && !options.message) {
58
- error('Either --message <text>, --payload <json>, or --file <path> is required.');
59
- process.exit(1);
60
- }
61
-
62
- let payload: Record<string, unknown> = {};
63
-
64
- // CVP-0010 §3.1: --message shorthand wraps text into {"text": "..."}
65
- if (options.message) {
66
- payload.text = options.message;
67
- } else if (options.payload) {
68
- try {
69
- payload = JSON.parse(options.payload);
70
- } catch {
71
- error('Invalid JSON payload');
72
- process.exit(1);
73
- }
74
- }
75
-
76
- if (options.file) {
77
- const filePath: string = options.file;
78
- let fileBytes: Buffer;
79
- try {
80
- fileBytes = await readFile(filePath);
81
- } catch {
82
- error(`Cannot read file: ${filePath}`);
83
- process.exit(1);
84
- }
85
- const filename = basename(filePath);
86
- const mimeType = guessMimeType(filename);
87
- payload.attachment = {
88
- filename,
89
- mimeType,
90
- size: fileBytes.length,
91
- data: fileBytes.toString('base64'),
92
- };
93
- info(`Attaching file: ${filename} (${fileBytes.length} bytes, ${mimeType})`);
94
- }
95
-
96
- // Try daemon first for fast path
97
- const client = new DaemonClient();
98
- if (await client.isDaemonRunning()) {
99
- const spin = spinner('Sending message via daemon...');
100
-
101
- try {
102
- const result = await client.send('send', {
103
- to: options.to,
104
- protocol: options.protocol,
105
- payload,
106
- type: options.type,
107
- });
108
-
109
- spin.succeed('Message sent successfully!');
110
-
111
- console.log();
112
- info(`Message ID: ${result.id}`);
113
- info(`To: ${options.to}`);
114
- info(`Protocol: ${options.protocol}`);
115
- info(`Type: ${options.type}`);
116
- if (payload.attachment) {
117
- const att = payload.attachment as { filename: string; mimeType: string; size: number };
118
- info(`Attachment: ${att.filename} (${att.size} bytes, ${att.mimeType})`);
119
- const rest = { ...payload, attachment: '[binary data omitted]' };
120
- info(`Payload: ${JSON.stringify(rest)}`);
121
- } else {
122
- info(`Payload: ${JSON.stringify(payload)}`);
123
- }
124
-
125
- // Display response if received
126
- if (result.response) {
127
- console.log();
128
- success('>>> Received response from recipient');
129
- info(`Response ID: ${result.response.id}`);
130
- info(`Reply To: ${result.response.replyTo}`);
131
- info(`Protocol: ${result.response.protocol}`);
132
- const respPayload = result.response.payload as Record<string, unknown>;
133
- if (respPayload?.attachment) {
134
- const att = respPayload.attachment as { filename: string; mimeType: string; size: number };
135
- info(`Attachment: ${att.filename} (${att.size} bytes, ${att.mimeType})`);
136
- info(`Payload: ${JSON.stringify({ ...respPayload, attachment: '[binary data omitted]' }, null, 2)}`);
137
- } else {
138
- info(`Payload: ${JSON.stringify(result.response.payload, null, 2)}`);
139
- }
140
- } else if (options.type === 'request') {
141
- console.log();
142
- info('No response received (recipient may not have returned a response)');
143
- }
144
-
145
- success('Done');
146
- return;
147
- } catch (err) {
148
- spin.fail('Daemon send failed, falling back to ephemeral node');
149
- info(`Daemon error: ${(err as Error).message}`);
150
- console.log();
151
- info('Tip: Restart daemon with "clawiverse daemon restart" if issues persist');
152
- console.log();
153
- }
154
- } else {
155
- console.log();
156
- info('⚠ Daemon not running, using ephemeral relay connection (slower)');
157
- info('Tip: Start daemon with "clawiverse daemon start" for faster messaging');
158
- console.log();
159
- }
160
-
161
- // Fallback: create ephemeral relay connection (CVP-0011)
162
- const spin = spinner('Connecting to relay...');
163
-
164
- const keyPair = importKeyPair({
165
- publicKey: identity.publicKey,
166
- privateKey: identity.privateKey,
167
- });
168
-
169
- const relayUrls = getRelayUrls(options);
170
- const card = getAgentCard();
171
- const capabilities = (card?.capabilities ?? []).map((c: string) => ({
172
- id: c, name: c, description: `Capability: ${c}`,
173
- }));
174
- const agentCard = createAgentCard(
175
- identity.did,
176
- card?.name ?? 'Clawiverse Agent',
177
- card?.description ?? '',
178
- capabilities,
179
- [],
180
- );
181
- const signedCard = await signAgentCard(agentCard, (data) => sign(data, keyPair.privateKey));
182
-
183
- const relayClient = createRelayClient({
184
- relayUrls,
185
- did: identity.did,
186
- keyPair,
187
- card: signedCard,
188
- });
189
-
190
- await relayClient.start();
191
- spin.text = 'Connected to relay';
192
-
193
- const verifyFn = async (signature: Uint8Array, data: Uint8Array): Promise<boolean> => {
194
- try {
195
- const decoded = JSON.parse(new TextDecoder().decode(data)) as { from?: string };
196
- if (!decoded.from || typeof decoded.from !== 'string') return false;
197
- const senderPublicKey = extractPublicKey(decoded.from);
198
- return verify(signature, data, senderPublicKey);
199
- } catch {
200
- return false;
201
- }
202
- };
203
-
204
- const router = createMessageRouter(relayClient, verifyFn);
205
- await router.start();
206
-
207
- spin.text = 'Creating message...';
208
-
209
- const envelope = createEnvelope(
210
- identity.did,
211
- options.to,
212
- options.type,
213
- options.protocol,
214
- payload
215
- );
216
-
217
- const signedEnvelope = await signEnvelope(envelope, (data) =>
218
- sign(data, keyPair.privateKey)
219
- );
220
-
221
- spin.text = 'Sending message...';
222
-
223
- const response = await router.sendMessage(signedEnvelope);
224
-
225
- spin.succeed('Message sent successfully!');
226
-
227
- console.log();
228
- info(`Message ID: ${signedEnvelope.id}`);
229
- info(`To: ${options.to}`);
230
- info(`Protocol: ${options.protocol}`);
231
- info(`Type: ${options.type}`);
232
- if (payload.attachment) {
233
- const att = payload.attachment as { filename: string; mimeType: string; size: number };
234
- info(`Attachment: ${att.filename} (${att.size} bytes, ${att.mimeType})`);
235
- const rest = { ...payload, attachment: '[binary data omitted]' };
236
- info(`Payload: ${JSON.stringify(rest)}`);
237
- } else {
238
- info(`Payload: ${JSON.stringify(payload)}`);
239
- }
240
-
241
- // Display response if received
242
- if (response) {
243
- console.log();
244
- success('>>> Received response from recipient');
245
- info(`Response ID: ${response.id}`);
246
- info(`Reply To: ${response.replyTo}`);
247
- info(`Protocol: ${response.protocol}`);
248
- const respPayload = response.payload as Record<string, unknown>;
249
- if (respPayload?.attachment) {
250
- const att = respPayload.attachment as { filename: string; mimeType: string; size: number };
251
- info(`Attachment: ${att.filename} (${att.size} bytes, ${att.mimeType})`);
252
- info(`Payload: ${JSON.stringify({ ...respPayload, attachment: '[binary data omitted]' }, null, 2)}`);
253
- } else {
254
- info(`Payload: ${JSON.stringify(response.payload, null, 2)}`);
255
- }
256
- } else if (options.type === 'request') {
257
- console.log();
258
- info('No response received (recipient may not have returned a response)');
259
- }
260
-
261
- await router.stop();
262
- await relayClient.stop();
263
-
264
- success('Done');
265
- } catch (err) {
266
- error(`Failed to send message: ${(err as Error).message}`);
267
- if ((err as Error).cause) error(`Cause: ${((err as Error).cause as Error).message}`);
268
- console.error(err);
269
- process.exit(1);
270
- }
271
- });
272
- }
273
-
274
- function guessMimeType(filename: string): string {
275
- const ext = filename.split('.').pop()?.toLowerCase() ?? '';
276
- const map: Record<string, string> = {
277
- // images
278
- png: 'image/png',
279
- jpg: 'image/jpeg',
280
- jpeg: 'image/jpeg',
281
- gif: 'image/gif',
282
- webp: 'image/webp',
283
- svg: 'image/svg+xml',
284
- // documents
285
- pdf: 'application/pdf',
286
- txt: 'text/plain',
287
- md: 'text/markdown',
288
- json: 'application/json',
289
- csv: 'text/csv',
290
- // audio/video
291
- mp3: 'audio/mpeg',
292
- mp4: 'video/mp4',
293
- wav: 'audio/wav',
294
- // archives
295
- zip: 'application/zip',
296
- gz: 'application/gzip',
297
- };
298
- return map[ext] ?? 'application/octet-stream';
299
- }
@@ -1,271 +0,0 @@
1
- /**
2
- * Serve Command - CVP-0010 §2.2
3
- *
4
- * Registers custom handlers that execute when matching requests arrive.
5
- *
6
- * hw1 serve --on "translate" --exec "./translate.sh"
7
- * hw1 serve --on "code_review" --exec "python review.py"
8
- * hw1 serve --handlers ./my-handlers/
9
- */
10
-
11
- import { Command } from 'commander';
12
- import { DaemonClient } from '../daemon/client.js';
13
- import { createLogger } from '@highway1/core';
14
- import { spawn } from 'child_process';
15
- import { readdir, stat } from 'node:fs/promises';
16
- import { join, resolve, basename } from 'node:path';
17
-
18
- const logger = createLogger('cli:serve');
19
-
20
- const MAX_PAYLOAD_BYTES = 256 * 1024; // 256 KiB
21
-
22
- interface HandlerEntry {
23
- capability: string;
24
- exec: string;
25
- }
26
-
27
- export function registerServeCommand(program: Command): void {
28
- program
29
- .command('serve')
30
- .description('Register handlers for incoming requests (CVP-0010 §2.2)')
31
- .option('--on <capability>', 'Capability name to handle')
32
- .option('--exec <script>', 'Script to execute for matching requests')
33
- .option('--handlers <dir>', 'Directory of handler scripts (filename = capability name)')
34
- .option('--allow-from <dids...>', 'Only accept requests from these DIDs (default: deny-all except allowlist)')
35
- .option('--public', 'Accept requests from any agent (overrides --allow-from)')
36
- .option('--max-concurrency <n>', 'Max concurrent handler executions', '4')
37
- .option('--timeout <seconds>', 'Handler execution timeout in seconds', '60')
38
- .option('--format <fmt>', 'Output format: text|json', 'text')
39
- .action(async (options) => {
40
- const client = new DaemonClient();
41
- if (!(await client.isDaemonRunning())) {
42
- console.error('Daemon not running. Start with: hw1 join');
43
- process.exit(1);
44
- }
45
-
46
- // Build handler list
47
- const handlers: HandlerEntry[] = [];
48
-
49
- if (options.on && options.exec) {
50
- handlers.push({ capability: options.on, exec: resolve(options.exec) });
51
- }
52
-
53
- if (options.handlers) {
54
- const dir = resolve(options.handlers);
55
- const entries = await readdir(dir);
56
- for (const entry of entries) {
57
- const fullPath = join(dir, entry);
58
- const s = await stat(fullPath);
59
- if (!s.isFile()) continue;
60
- // Strip extension to get capability name; skip _default for now
61
- const cap = basename(entry).replace(/\.[^.]+$/, '');
62
- handlers.push({ capability: cap, exec: fullPath });
63
- }
64
- }
65
-
66
- if (handlers.length === 0) {
67
- console.error('No handlers specified. Use --on/--exec or --handlers <dir>');
68
- process.exit(1);
69
- }
70
-
71
- const maxConcurrency = parseInt(options.maxConcurrency, 10);
72
- const timeoutMs = parseInt(options.timeout, 10) * 1000;
73
- let activeCount = 0;
74
-
75
- // Apply allowlist if --allow-from specified
76
- if (options.allowFrom && !options.public) {
77
- for (const did of options.allowFrom) {
78
- await client.send('allowlist', { action: 'add', did, note: 'hw1 serve --allow-from' });
79
- }
80
- if (options.format !== 'json') {
81
- console.log(`Allowlisted ${options.allowFrom.length} DID(s)`);
82
- }
83
- }
84
-
85
- if (options.format !== 'json') {
86
- console.log(`\nServing ${handlers.length} handler(s):`);
87
- for (const h of handlers) {
88
- console.log(` ${h.capability} → ${h.exec}`);
89
- }
90
- console.log(`\nMax concurrency: ${maxConcurrency}, timeout: ${options.timeout}s`);
91
- console.log('Waiting for requests... (Ctrl+C to stop)\n');
92
- }
93
-
94
- // Subscribe to incoming messages via queue
95
- const subscriptionFilter = {
96
- protocol: handlers.map((h) => `clawiverse/${h.capability}`),
97
- };
98
-
99
- // Poll inbox for new messages matching our capabilities
100
- const poll = async () => {
101
- try {
102
- const page = await client.send('inbox', {
103
- filter: { unreadOnly: true, status: 'pending' },
104
- pagination: { limit: 10 },
105
- });
106
-
107
- for (const msg of page.messages ?? []) {
108
- const envelope = msg.envelope;
109
- const protocol: string = envelope.protocol ?? '';
110
-
111
- // Find matching handler
112
- const handler = handlers.find((h) =>
113
- protocol.includes(h.capability) ||
114
- (envelope.payload as any)?.capability === h.capability
115
- );
116
-
117
- if (!handler) continue;
118
-
119
- // Mark as read immediately to avoid double-processing
120
- await client.send('mark_read', { id: envelope.id });
121
-
122
- // Concurrency control
123
- if (activeCount >= maxConcurrency) {
124
- if (options.format !== 'json') {
125
- console.log(`[BUSY] Rejected ${envelope.id.slice(-8)} from ${envelope.from.slice(0, 20)}…`);
126
- }
127
- // Send BUSY response
128
- await client.send('send', {
129
- to: envelope.from,
130
- protocol: envelope.protocol,
131
- payload: { error: 'BUSY', message: 'Server at capacity, try again later' },
132
- type: 'response',
133
- });
134
- continue;
135
- }
136
-
137
- // Payload size check
138
- const payloadStr = JSON.stringify(envelope.payload ?? '');
139
- if (payloadStr.length > MAX_PAYLOAD_BYTES) {
140
- await client.send('send', {
141
- to: envelope.from,
142
- protocol: envelope.protocol,
143
- payload: { error: 'PAYLOAD_TOO_LARGE', message: `Max payload is ${MAX_PAYLOAD_BYTES} bytes` },
144
- type: 'response',
145
- });
146
- continue;
147
- }
148
-
149
- // Execute handler
150
- activeCount++;
151
- const startTime = Date.now();
152
-
153
- if (options.format !== 'json') {
154
- console.log(`[${new Date().toLocaleTimeString()}] ${handler.capability} ← ${envelope.from.slice(0, 30)}…`);
155
- }
156
-
157
- executeHandler(handler.exec, envelope.payload, timeoutMs)
158
- .then(async (result) => {
159
- const latencyMs = Date.now() - startTime;
160
- await client.send('send', {
161
- to: envelope.from,
162
- protocol: envelope.protocol,
163
- payload: result,
164
- type: 'response',
165
- });
166
-
167
- if (options.format === 'json') {
168
- console.log(JSON.stringify({
169
- event: 'handled',
170
- capability: handler.capability,
171
- from: envelope.from,
172
- latencyMs,
173
- success: true,
174
- }));
175
- } else {
176
- console.log(` → responded in ${latencyMs}ms`);
177
- }
178
- })
179
- .catch(async (err) => {
180
- const latencyMs = Date.now() - startTime;
181
- const isTimeout = err.message?.includes('timeout');
182
- await client.send('send', {
183
- to: envelope.from,
184
- protocol: envelope.protocol,
185
- payload: {
186
- error: isTimeout ? 'TIMEOUT' : 'HANDLER_ERROR',
187
- message: err.message,
188
- },
189
- type: 'response',
190
- });
191
-
192
- if (options.format === 'json') {
193
- console.log(JSON.stringify({
194
- event: 'error',
195
- capability: handler.capability,
196
- from: envelope.from,
197
- latencyMs,
198
- error: err.message,
199
- }));
200
- } else {
201
- console.error(` → error after ${latencyMs}ms: ${err.message}`);
202
- }
203
- })
204
- .finally(() => {
205
- activeCount--;
206
- });
207
- }
208
- } catch (err) {
209
- logger.warn('Poll error', err);
210
- }
211
- };
212
-
213
- // Poll every 500ms
214
- const interval = setInterval(poll, 500);
215
-
216
- // Graceful shutdown
217
- const shutdown = () => {
218
- clearInterval(interval);
219
- if (options.format !== 'json') console.log('\nStopped serving.');
220
- process.exit(0);
221
- };
222
- process.on('SIGINT', shutdown);
223
- process.on('SIGTERM', shutdown);
224
- });
225
- }
226
-
227
- async function executeHandler(
228
- scriptPath: string,
229
- payload: unknown,
230
- timeoutMs: number
231
- ): Promise<unknown> {
232
- return new Promise((resolve, reject) => {
233
- const child = spawn(scriptPath, [], {
234
- stdio: ['pipe', 'pipe', 'pipe'],
235
- env: { ...process.env },
236
- });
237
-
238
- let stdout = '';
239
- let stderr = '';
240
-
241
- child.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
242
- child.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
243
-
244
- const timer = setTimeout(() => {
245
- child.kill('SIGTERM');
246
- reject(new Error(`Handler timeout after ${timeoutMs}ms`));
247
- }, timeoutMs);
248
-
249
- child.on('close', (code) => {
250
- clearTimeout(timer);
251
- if (code !== 0) {
252
- reject(new Error(`Handler exited with code ${code}: ${stderr.trim()}`));
253
- return;
254
- }
255
- try {
256
- resolve(JSON.parse(stdout.trim()));
257
- } catch {
258
- resolve({ result: stdout.trim() });
259
- }
260
- });
261
-
262
- child.on('error', (err) => {
263
- clearTimeout(timer);
264
- reject(err);
265
- });
266
-
267
- // Write payload to stdin
268
- child.stdin.write(JSON.stringify(payload));
269
- child.stdin.end();
270
- });
271
- }
@@ -1,60 +0,0 @@
1
- import { Command } from 'commander';
2
- import { getIdentity, getAgentCard, getBootstrapPeers } from '../config.js';
3
- import { error, printHeader, printKeyValue, printSection } from '../ui.js';
4
-
5
- export function registerStatusCommand(program: Command): void {
6
- program
7
- .command('status')
8
- .description('Show current status')
9
- .option('--format <fmt>', 'Output format: text|json', 'text')
10
- .action(async (options) => {
11
- try {
12
- const identity = getIdentity();
13
- const card = getAgentCard();
14
- const bootstrapPeers = getBootstrapPeers();
15
-
16
- if (!identity) {
17
- error('No identity configured. Run "hw1 init" first.');
18
- process.exit(1);
19
- }
20
-
21
- if (options.format === 'json') {
22
- console.log(JSON.stringify({
23
- identity: {
24
- did: identity.did,
25
- publicKey: identity.publicKey,
26
- },
27
- agentCard: card || null,
28
- network: {
29
- bootstrapPeers,
30
- },
31
- }, null, 2));
32
- return;
33
- }
34
-
35
- printHeader('Clawiverse Status');
36
-
37
- printSection('Identity');
38
- printKeyValue('DID', identity.did);
39
- printKeyValue('Public Key', identity.publicKey.substring(0, 16) + '...');
40
-
41
- if (card) {
42
- printSection('Agent Card');
43
- printKeyValue('Name', card.name);
44
- printKeyValue('Description', card.description);
45
- printKeyValue('Capabilities', card.capabilities.join(', ') || 'None');
46
- }
47
-
48
- printSection('Network');
49
- printKeyValue(
50
- 'Bootstrap Peers',
51
- bootstrapPeers.length > 0 ? bootstrapPeers.join(', ') : 'None configured'
52
- );
53
-
54
- console.log();
55
- } catch (err) {
56
- error(`Failed to show status: ${(err as Error).message}`);
57
- process.exit(1);
58
- }
59
- });
60
- }
@@ -1,49 +0,0 @@
1
- /**
2
- * Stop Command - CVP-0010 §2.5
3
- *
4
- * Cleaner alternative to `hw1 daemon stop`
5
- */
6
-
7
- import { Command } from 'commander';
8
- import { DaemonClient } from '../daemon/client.js';
9
- import { existsSync, unlinkSync } from 'fs';
10
- import { success, info } from '../ui.js';
11
-
12
- const PID_FILE = '/tmp/clawiverse.pid';
13
- const SOCKET_PATH = '/tmp/clawiverse.sock';
14
-
15
- export function registerStopCommand(program: Command): void {
16
- program
17
- .command('stop')
18
- .description('Stop the Highway 1 daemon')
19
- .action(async () => {
20
- try {
21
- const client = new DaemonClient(SOCKET_PATH);
22
-
23
- if (!(await client.isDaemonRunning())) {
24
- info('Daemon not running');
25
- return;
26
- }
27
-
28
- await client.send('shutdown', {});
29
- success('Daemon stopped');
30
-
31
- // Clean up PID file
32
- try {
33
- if (existsSync(PID_FILE)) {
34
- unlinkSync(PID_FILE);
35
- }
36
- } catch {}
37
-
38
- // Clean up socket file
39
- try {
40
- if (existsSync(SOCKET_PATH)) {
41
- unlinkSync(SOCKET_PATH);
42
- }
43
- } catch {}
44
- } catch (err) {
45
- console.error(`Failed to stop daemon: ${(err as Error).message}`);
46
- process.exit(1);
47
- }
48
- });
49
- }