@highway1/cli 0.1.44 → 0.1.46
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/dist/index.js +740 -160
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/daemon.ts +207 -0
- package/src/commands/join.ts +36 -6
- package/src/commands/send.ts +149 -10
- package/src/daemon/client.ts +68 -0
- package/src/daemon/server.ts +267 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@highway1/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "CLI tool for Clawiverse network",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"clean": "rm -rf dist"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@highway1/core": "
|
|
16
|
+
"@highway1/core": "workspace:*",
|
|
17
17
|
"chalk": "^5.3.0",
|
|
18
18
|
"cli-table3": "^0.6.5",
|
|
19
19
|
"commander": "^12.1.0",
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { ClawDaemon } from '../daemon/server.js';
|
|
3
|
+
import { DaemonClient } from '../daemon/client.js';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'fs';
|
|
6
|
+
import { success, error, info } from '../ui.js';
|
|
7
|
+
|
|
8
|
+
const PID_FILE = '/tmp/clawiverse.pid';
|
|
9
|
+
const SOCKET_PATH = '/tmp/clawiverse.sock';
|
|
10
|
+
|
|
11
|
+
export function registerDaemonCommand(program: Command): void {
|
|
12
|
+
const daemonCommand = program
|
|
13
|
+
.command('daemon')
|
|
14
|
+
.description('Manage Clawiverse daemon for fast messaging');
|
|
15
|
+
|
|
16
|
+
daemonCommand
|
|
17
|
+
.command('start')
|
|
18
|
+
.description('Start daemon in background')
|
|
19
|
+
.action(async () => {
|
|
20
|
+
try {
|
|
21
|
+
const client = new DaemonClient(SOCKET_PATH);
|
|
22
|
+
if (await client.isDaemonRunning()) {
|
|
23
|
+
success('Daemon already running');
|
|
24
|
+
const status = await client.send('status', {});
|
|
25
|
+
info(`Peer ID: ${status.peerId}`);
|
|
26
|
+
info(`DID: ${status.did}`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Start daemon as detached background process
|
|
31
|
+
const child = spawn(process.execPath, [
|
|
32
|
+
process.argv[1],
|
|
33
|
+
'daemon',
|
|
34
|
+
'run'
|
|
35
|
+
], {
|
|
36
|
+
detached: true,
|
|
37
|
+
stdio: 'ignore'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
child.unref();
|
|
41
|
+
writeFileSync(PID_FILE, child.pid!.toString());
|
|
42
|
+
|
|
43
|
+
// Wait for daemon to be ready
|
|
44
|
+
for (let i = 0; i < 20; i++) {
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
46
|
+
if (await client.isDaemonRunning()) {
|
|
47
|
+
success('Daemon started');
|
|
48
|
+
const status = await client.send('status', {});
|
|
49
|
+
info(`Peer ID: ${status.peerId}`);
|
|
50
|
+
info(`DID: ${status.did}`);
|
|
51
|
+
info(`Socket: ${SOCKET_PATH}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
error('Failed to start daemon (timeout)');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
error(`Failed to start daemon: ${(err as Error).message}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
daemonCommand
|
|
65
|
+
.command('run')
|
|
66
|
+
.description('Run daemon in foreground (internal use)')
|
|
67
|
+
.action(async () => {
|
|
68
|
+
try {
|
|
69
|
+
const daemon = new ClawDaemon(SOCKET_PATH);
|
|
70
|
+
await daemon.start();
|
|
71
|
+
|
|
72
|
+
// Keep process alive
|
|
73
|
+
process.on('SIGINT', async () => {
|
|
74
|
+
console.log('\nShutting down...');
|
|
75
|
+
await daemon.shutdown();
|
|
76
|
+
process.exit(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
process.on('SIGTERM', async () => {
|
|
80
|
+
await daemon.shutdown();
|
|
81
|
+
process.exit(0);
|
|
82
|
+
});
|
|
83
|
+
} catch (err) {
|
|
84
|
+
error(`Failed to run daemon: ${(err as Error).message}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
daemonCommand
|
|
90
|
+
.command('stop')
|
|
91
|
+
.description('Stop daemon')
|
|
92
|
+
.action(async () => {
|
|
93
|
+
try {
|
|
94
|
+
const client = new DaemonClient(SOCKET_PATH);
|
|
95
|
+
|
|
96
|
+
if (!(await client.isDaemonRunning())) {
|
|
97
|
+
info('Daemon not running');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await client.send('shutdown', {});
|
|
102
|
+
success('Daemon stopped');
|
|
103
|
+
|
|
104
|
+
// Clean up PID file
|
|
105
|
+
try {
|
|
106
|
+
if (existsSync(PID_FILE)) {
|
|
107
|
+
unlinkSync(PID_FILE);
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
|
|
111
|
+
// Clean up socket file
|
|
112
|
+
try {
|
|
113
|
+
if (existsSync(SOCKET_PATH)) {
|
|
114
|
+
unlinkSync(SOCKET_PATH);
|
|
115
|
+
}
|
|
116
|
+
} catch {}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
error(`Failed to stop daemon: ${(err as Error).message}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
daemonCommand
|
|
124
|
+
.command('status')
|
|
125
|
+
.description('Check daemon status')
|
|
126
|
+
.action(async () => {
|
|
127
|
+
try {
|
|
128
|
+
const client = new DaemonClient(SOCKET_PATH);
|
|
129
|
+
|
|
130
|
+
if (await client.isDaemonRunning()) {
|
|
131
|
+
const status = await client.send('status', {});
|
|
132
|
+
success('Daemon running');
|
|
133
|
+
console.log();
|
|
134
|
+
info(`Peer ID: ${status.peerId}`);
|
|
135
|
+
info(`DID: ${status.did}`);
|
|
136
|
+
info(`Socket: ${SOCKET_PATH}`);
|
|
137
|
+
info(`Bootstrap peers: ${status.bootstrapPeers.length}`);
|
|
138
|
+
console.log();
|
|
139
|
+
info('Multiaddrs:');
|
|
140
|
+
status.multiaddrs.forEach((addr: string) => {
|
|
141
|
+
console.log(` ${addr}`);
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
info('Daemon not running');
|
|
145
|
+
console.log();
|
|
146
|
+
info('Start daemon with: clawiverse daemon start');
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
error(`Failed to check status: ${(err as Error).message}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
daemonCommand
|
|
155
|
+
.command('restart')
|
|
156
|
+
.description('Restart daemon')
|
|
157
|
+
.action(async () => {
|
|
158
|
+
try {
|
|
159
|
+
const client = new DaemonClient(SOCKET_PATH);
|
|
160
|
+
|
|
161
|
+
// Stop if running
|
|
162
|
+
if (await client.isDaemonRunning()) {
|
|
163
|
+
info('Stopping daemon...');
|
|
164
|
+
await client.send('shutdown', {});
|
|
165
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Clean up files
|
|
169
|
+
try {
|
|
170
|
+
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
|
|
171
|
+
if (existsSync(SOCKET_PATH)) unlinkSync(SOCKET_PATH);
|
|
172
|
+
} catch {}
|
|
173
|
+
|
|
174
|
+
// Start daemon
|
|
175
|
+
info('Starting daemon...');
|
|
176
|
+
const child = spawn(process.execPath, [
|
|
177
|
+
process.argv[1],
|
|
178
|
+
'daemon',
|
|
179
|
+
'run'
|
|
180
|
+
], {
|
|
181
|
+
detached: true,
|
|
182
|
+
stdio: 'ignore'
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
child.unref();
|
|
186
|
+
writeFileSync(PID_FILE, child.pid!.toString());
|
|
187
|
+
|
|
188
|
+
// Wait for daemon to be ready
|
|
189
|
+
for (let i = 0; i < 20; i++) {
|
|
190
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
191
|
+
if (await client.isDaemonRunning()) {
|
|
192
|
+
success('Daemon restarted');
|
|
193
|
+
const status = await client.send('status', {});
|
|
194
|
+
info(`Peer ID: ${status.peerId}`);
|
|
195
|
+
info(`DID: ${status.did}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
error('Failed to restart daemon (timeout)');
|
|
201
|
+
process.exit(1);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
error(`Failed to restart daemon: ${(err as Error).message}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
package/src/commands/join.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
} from '@highway1/core';
|
|
13
13
|
import { getIdentity, getAgentCard, getBootstrapPeers } from '../config.js';
|
|
14
14
|
import { success, error, spinner, printHeader, info } from '../ui.js';
|
|
15
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
16
|
+
import { join, resolve } from 'node:path';
|
|
15
17
|
|
|
16
18
|
export function registerJoinCommand(program: Command): void {
|
|
17
19
|
program
|
|
@@ -19,6 +21,7 @@ export function registerJoinCommand(program: Command): void {
|
|
|
19
21
|
.description('Join the Clawiverse network')
|
|
20
22
|
.option('--bootstrap <peers...>', 'Bootstrap peer addresses')
|
|
21
23
|
.option('--relay', 'Run as a relay server and advertise relay capability')
|
|
24
|
+
.option('--save-dir <path>', 'Directory to save received file attachments', './downloads')
|
|
22
25
|
.action(async (options) => {
|
|
23
26
|
try {
|
|
24
27
|
printHeader('Join Clawiverse Network');
|
|
@@ -280,6 +283,8 @@ export function registerJoinCommand(program: Command): void {
|
|
|
280
283
|
dht
|
|
281
284
|
);
|
|
282
285
|
|
|
286
|
+
const saveDir = resolve(options.saveDir ?? './downloads');
|
|
287
|
+
|
|
283
288
|
// Generic message handler that accepts any protocol
|
|
284
289
|
const messageHandler = async (envelope: any) => {
|
|
285
290
|
const payload = envelope.payload as Record<string, unknown>;
|
|
@@ -288,7 +293,32 @@ export function registerJoinCommand(program: Command): void {
|
|
|
288
293
|
info(` Message ID: ${envelope.id}`);
|
|
289
294
|
info(` Protocol: ${envelope.protocol}`);
|
|
290
295
|
info(` Type: ${envelope.type}`);
|
|
291
|
-
|
|
296
|
+
|
|
297
|
+
// Handle file attachment
|
|
298
|
+
let savedPath: string | undefined;
|
|
299
|
+
if (payload.attachment) {
|
|
300
|
+
const att = payload.attachment as {
|
|
301
|
+
filename: string;
|
|
302
|
+
mimeType: string;
|
|
303
|
+
size: number;
|
|
304
|
+
data: string;
|
|
305
|
+
};
|
|
306
|
+
try {
|
|
307
|
+
await mkdir(saveDir, { recursive: true });
|
|
308
|
+
// Avoid filename collisions by prefixing with message id fragment
|
|
309
|
+
const safeName = att.filename.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
310
|
+
savedPath = join(saveDir, `${envelope.id.slice(-8)}_${safeName}`);
|
|
311
|
+
await writeFile(savedPath, Buffer.from(att.data, 'base64'));
|
|
312
|
+
success(` Attachment saved: ${savedPath}`);
|
|
313
|
+
info(` File: ${att.filename} (${att.size} bytes, ${att.mimeType})`);
|
|
314
|
+
} catch (e) {
|
|
315
|
+
error(` Failed to save attachment: ${(e as Error).message}`);
|
|
316
|
+
}
|
|
317
|
+
const displayPayload = { ...payload, attachment: '[binary data omitted]' };
|
|
318
|
+
info(` Payload: ${JSON.stringify(displayPayload, null, 2)}`);
|
|
319
|
+
} else {
|
|
320
|
+
info(` Payload: ${JSON.stringify(payload, null, 2)}`);
|
|
321
|
+
}
|
|
292
322
|
console.log();
|
|
293
323
|
|
|
294
324
|
// If this is a request, send back a simple acknowledgment response
|
|
@@ -303,17 +333,17 @@ export function registerJoinCommand(program: Command): void {
|
|
|
303
333
|
});
|
|
304
334
|
|
|
305
335
|
const responseEnvelope = createEnvelope(
|
|
306
|
-
envelope.to,
|
|
307
|
-
envelope.from,
|
|
336
|
+
envelope.to,
|
|
337
|
+
envelope.from,
|
|
308
338
|
'response',
|
|
309
339
|
envelope.protocol,
|
|
310
340
|
{
|
|
311
341
|
status: 'received',
|
|
312
342
|
message: 'Message received and processed',
|
|
313
|
-
|
|
314
|
-
timestamp: Date.now()
|
|
343
|
+
savedPath: savedPath ?? null,
|
|
344
|
+
timestamp: Date.now(),
|
|
315
345
|
},
|
|
316
|
-
envelope.id
|
|
346
|
+
envelope.id
|
|
317
347
|
);
|
|
318
348
|
|
|
319
349
|
const signedResponse = await signEnvelope(responseEnvelope, (data) =>
|
package/src/commands/send.ts
CHANGED
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
} from '@highway1/core';
|
|
13
13
|
import { getIdentity, getBootstrapPeers } from '../config.js';
|
|
14
14
|
import { success, error, spinner, printHeader, info } from '../ui.js';
|
|
15
|
+
import { readFile } from 'node:fs/promises';
|
|
16
|
+
import { basename } from 'node:path';
|
|
17
|
+
import { DaemonClient } from '../daemon/client.js';
|
|
15
18
|
|
|
16
19
|
export function registerSendCommand(program: Command): void {
|
|
17
20
|
program
|
|
@@ -19,7 +22,8 @@ export function registerSendCommand(program: Command): void {
|
|
|
19
22
|
.description('Send a message to another agent')
|
|
20
23
|
.requiredOption('--to <did>', 'Recipient DID')
|
|
21
24
|
.requiredOption('--protocol <protocol>', 'Protocol identifier')
|
|
22
|
-
.
|
|
25
|
+
.option('--payload <json>', 'Message payload (JSON)')
|
|
26
|
+
.option('--file <path>', 'Attach a file (image, binary, text) as payload attachment')
|
|
23
27
|
.option('--type <type>', 'Message type (request|notification)', 'request')
|
|
24
28
|
.option('--bootstrap <peers...>', 'Bootstrap peer addresses')
|
|
25
29
|
.option('--peer <multiaddr>', 'Direct peer multiaddr (bypasses DHT lookup)')
|
|
@@ -34,14 +38,108 @@ export function registerSendCommand(program: Command): void {
|
|
|
34
38
|
process.exit(1);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
payload = JSON.parse(options.payload);
|
|
40
|
-
} catch {
|
|
41
|
-
error('Invalid JSON payload');
|
|
41
|
+
if (!options.payload && !options.file) {
|
|
42
|
+
error('Either --payload <json> or --file <path> is required.');
|
|
42
43
|
process.exit(1);
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
let payload: Record<string, unknown> = {};
|
|
47
|
+
|
|
48
|
+
if (options.payload) {
|
|
49
|
+
try {
|
|
50
|
+
payload = JSON.parse(options.payload);
|
|
51
|
+
} catch {
|
|
52
|
+
error('Invalid JSON payload');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (options.file) {
|
|
58
|
+
const filePath: string = options.file;
|
|
59
|
+
let fileBytes: Buffer;
|
|
60
|
+
try {
|
|
61
|
+
fileBytes = await readFile(filePath);
|
|
62
|
+
} catch {
|
|
63
|
+
error(`Cannot read file: ${filePath}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const filename = basename(filePath);
|
|
67
|
+
const mimeType = guessMimeType(filename);
|
|
68
|
+
payload.attachment = {
|
|
69
|
+
filename,
|
|
70
|
+
mimeType,
|
|
71
|
+
size: fileBytes.length,
|
|
72
|
+
data: fileBytes.toString('base64'),
|
|
73
|
+
};
|
|
74
|
+
info(`Attaching file: ${filename} (${fileBytes.length} bytes, ${mimeType})`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Try daemon first for fast path
|
|
78
|
+
const client = new DaemonClient();
|
|
79
|
+
if (await client.isDaemonRunning()) {
|
|
80
|
+
const spin = spinner('Sending message via daemon...');
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const result = await client.send('send', {
|
|
84
|
+
to: options.to,
|
|
85
|
+
protocol: options.protocol,
|
|
86
|
+
payload,
|
|
87
|
+
type: options.type,
|
|
88
|
+
peer: options.peer,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
spin.succeed('Message sent successfully!');
|
|
92
|
+
|
|
93
|
+
console.log();
|
|
94
|
+
info(`Message ID: ${result.id}`);
|
|
95
|
+
info(`To: ${options.to}`);
|
|
96
|
+
info(`Protocol: ${options.protocol}`);
|
|
97
|
+
info(`Type: ${options.type}`);
|
|
98
|
+
if (payload.attachment) {
|
|
99
|
+
const att = payload.attachment as { filename: string; mimeType: string; size: number };
|
|
100
|
+
info(`Attachment: ${att.filename} (${att.size} bytes, ${att.mimeType})`);
|
|
101
|
+
const rest = { ...payload, attachment: '[binary data omitted]' };
|
|
102
|
+
info(`Payload: ${JSON.stringify(rest)}`);
|
|
103
|
+
} else {
|
|
104
|
+
info(`Payload: ${JSON.stringify(payload)}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Display response if received
|
|
108
|
+
if (result.response) {
|
|
109
|
+
console.log();
|
|
110
|
+
success('>>> Received response from recipient');
|
|
111
|
+
info(`Response ID: ${result.response.id}`);
|
|
112
|
+
info(`Reply To: ${result.response.replyTo}`);
|
|
113
|
+
info(`Protocol: ${result.response.protocol}`);
|
|
114
|
+
const respPayload = result.response.payload as Record<string, unknown>;
|
|
115
|
+
if (respPayload?.attachment) {
|
|
116
|
+
const att = respPayload.attachment as { filename: string; mimeType: string; size: number };
|
|
117
|
+
info(`Attachment: ${att.filename} (${att.size} bytes, ${att.mimeType})`);
|
|
118
|
+
info(`Payload: ${JSON.stringify({ ...respPayload, attachment: '[binary data omitted]' }, null, 2)}`);
|
|
119
|
+
} else {
|
|
120
|
+
info(`Payload: ${JSON.stringify(result.response.payload, null, 2)}`);
|
|
121
|
+
}
|
|
122
|
+
} else if (options.type === 'request') {
|
|
123
|
+
console.log();
|
|
124
|
+
info('No response received (recipient may not have returned a response)');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
success('Done');
|
|
128
|
+
return;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
spin.fail('Daemon send failed, falling back to ephemeral node');
|
|
131
|
+
console.log();
|
|
132
|
+
info('Tip: Restart daemon with "clawiverse daemon restart" if issues persist');
|
|
133
|
+
console.log();
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
console.log();
|
|
137
|
+
info('⚠ Daemon not running, using ephemeral node (slower)');
|
|
138
|
+
info('Tip: Start daemon with "clawiverse daemon start" for faster messaging');
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Fallback: create ephemeral node (original behavior)
|
|
45
143
|
const spin = spinner('Starting node...');
|
|
46
144
|
|
|
47
145
|
const keyPair = importKeyPair({
|
|
@@ -59,10 +157,10 @@ export function registerSendCommand(program: Command): void {
|
|
|
59
157
|
|
|
60
158
|
// Register peer:identify listener BEFORE start() so we don't miss the event
|
|
61
159
|
const identifyDone = new Promise<void>((resolve) => {
|
|
62
|
-
const timeout = setTimeout(resolve,
|
|
160
|
+
const timeout = setTimeout(resolve, 2000);
|
|
63
161
|
node.libp2p.addEventListener('peer:identify', () => {
|
|
64
162
|
clearTimeout(timeout);
|
|
65
|
-
|
|
163
|
+
resolve();
|
|
66
164
|
}, { once: true });
|
|
67
165
|
});
|
|
68
166
|
|
|
@@ -131,7 +229,14 @@ export function registerSendCommand(program: Command): void {
|
|
|
131
229
|
info(`To: ${options.to}`);
|
|
132
230
|
info(`Protocol: ${options.protocol}`);
|
|
133
231
|
info(`Type: ${options.type}`);
|
|
134
|
-
|
|
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
|
+
}
|
|
135
240
|
|
|
136
241
|
// Display response if received
|
|
137
242
|
if (response) {
|
|
@@ -140,7 +245,14 @@ export function registerSendCommand(program: Command): void {
|
|
|
140
245
|
info(`Response ID: ${response.id}`);
|
|
141
246
|
info(`Reply To: ${response.replyTo}`);
|
|
142
247
|
info(`Protocol: ${response.protocol}`);
|
|
143
|
-
|
|
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
|
+
}
|
|
144
256
|
} else if (options.type === 'request') {
|
|
145
257
|
console.log();
|
|
146
258
|
info('No response received (recipient may not have returned a response)');
|
|
@@ -158,3 +270,30 @@ export function registerSendCommand(program: Command): void {
|
|
|
158
270
|
}
|
|
159
271
|
});
|
|
160
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
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { connect, Socket } from 'net';
|
|
2
|
+
import { createLogger } from '@highway1/core';
|
|
3
|
+
|
|
4
|
+
const logger = createLogger('daemon-client');
|
|
5
|
+
|
|
6
|
+
export class DaemonClient {
|
|
7
|
+
private socketPath: string;
|
|
8
|
+
|
|
9
|
+
constructor(socketPath: string = '/tmp/clawiverse.sock') {
|
|
10
|
+
this.socketPath = socketPath;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async send(command: string, params: any): Promise<any> {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const socket = connect(this.socketPath);
|
|
16
|
+
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
17
|
+
|
|
18
|
+
let responseReceived = false;
|
|
19
|
+
|
|
20
|
+
socket.on('connect', () => {
|
|
21
|
+
const request = { id: requestId, command, params };
|
|
22
|
+
logger.debug('Sending request', { command, id: requestId });
|
|
23
|
+
socket.write(JSON.stringify(request));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
socket.on('data', (data) => {
|
|
27
|
+
try {
|
|
28
|
+
const response = JSON.parse(data.toString());
|
|
29
|
+
responseReceived = true;
|
|
30
|
+
socket.end();
|
|
31
|
+
|
|
32
|
+
logger.debug('Received response', { success: response.success, id: response.id });
|
|
33
|
+
|
|
34
|
+
if (response.success) {
|
|
35
|
+
resolve(response.data);
|
|
36
|
+
} else {
|
|
37
|
+
reject(new Error(response.error));
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
reject(new Error(`Failed to parse response: ${(error as Error).message}`));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
socket.on('error', (err) => {
|
|
45
|
+
if (!responseReceived) {
|
|
46
|
+
logger.debug('Socket error', { error: err.message });
|
|
47
|
+
reject(err);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
socket.setTimeout(30000, () => {
|
|
52
|
+
if (!responseReceived) {
|
|
53
|
+
socket.destroy();
|
|
54
|
+
reject(new Error('Request timeout'));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async isDaemonRunning(): Promise<boolean> {
|
|
61
|
+
try {
|
|
62
|
+
await this.send('status', {});
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|