@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.
- package/package.json +11 -22
- package/bin/clawiverse.js +0 -2
- package/src/commands/ask.ts +0 -158
- package/src/commands/card.ts +0 -104
- package/src/commands/daemon.ts +0 -207
- package/src/commands/discover.ts +0 -178
- package/src/commands/identity.ts +0 -46
- package/src/commands/inbox.ts +0 -222
- package/src/commands/init.ts +0 -54
- package/src/commands/join.ts +0 -198
- package/src/commands/peers.ts +0 -85
- package/src/commands/send.ts +0 -299
- package/src/commands/serve.ts +0 -271
- package/src/commands/status.ts +0 -60
- package/src/commands/stop.ts +0 -49
- package/src/commands/trust.ts +0 -354
- package/src/config.ts +0 -74
- package/src/daemon/client.ts +0 -90
- package/src/daemon/server.ts +0 -481
- package/src/index.ts +0 -61
- package/src/ui.ts +0 -38
package/src/commands/send.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/serve.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/status.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/stop.ts
DELETED
|
@@ -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
|
-
}
|