@highway1/cli 0.1.49 → 0.1.52
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 +10802 -112048
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/commands/ask.ts +158 -0
- package/src/commands/card.ts +8 -3
- package/src/commands/discover.ts +52 -42
- package/src/commands/identity.ts +12 -3
- package/src/commands/inbox.ts +222 -0
- package/src/commands/join.ts +36 -228
- package/src/commands/peers.ts +85 -0
- package/src/commands/send.ts +56 -58
- package/src/commands/serve.ts +271 -0
- package/src/commands/status.ts +18 -3
- package/src/commands/stop.ts +49 -0
- package/src/commands/trust.ts +144 -5
- package/src/daemon/client.ts +31 -9
- package/src/daemon/server.ts +301 -88
- package/src/index.ts +10 -0
- package/LICENSE +0 -21
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@highway1/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.52",
|
|
4
4
|
"description": "CLI tool for Clawiverse network",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"clawiverse": "./dist/index.js",
|
|
8
8
|
"hw1": "./dist/index.js"
|
|
9
9
|
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"dev": "tsup --watch",
|
|
13
|
+
"clean": "rm -rf dist"
|
|
14
|
+
},
|
|
10
15
|
"dependencies": {
|
|
11
|
-
"@highway1/core": "^0.1.
|
|
16
|
+
"@highway1/core": "^0.1.51",
|
|
12
17
|
"chalk": "^5.3.0",
|
|
13
18
|
"cli-table3": "^0.6.5",
|
|
14
19
|
"commander": "^12.1.0",
|
|
@@ -23,10 +28,5 @@
|
|
|
23
28
|
},
|
|
24
29
|
"engines": {
|
|
25
30
|
"node": ">=22.0.0"
|
|
26
|
-
},
|
|
27
|
-
"scripts": {
|
|
28
|
-
"build": "tsup",
|
|
29
|
-
"dev": "tsup --watch",
|
|
30
|
-
"clean": "rm -rf dist"
|
|
31
31
|
}
|
|
32
|
-
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask Command - CVP-0010 §2.1
|
|
3
|
+
*
|
|
4
|
+
* One-step network query: discover + send + wait for response.
|
|
5
|
+
*
|
|
6
|
+
* hw1 ask "translate hello to Japanese"
|
|
7
|
+
* hw1 ask "review this code" --file main.py
|
|
8
|
+
* hw1 ask "translate hello" --to kaito-translator
|
|
9
|
+
* hw1 ask "summarize" --min-trust 0.8 --format json
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { DaemonClient } from '../daemon/client.js';
|
|
14
|
+
import { createLogger } from '@highway1/core';
|
|
15
|
+
import { readFile } from 'node:fs/promises';
|
|
16
|
+
import { basename } from 'node:path';
|
|
17
|
+
|
|
18
|
+
const logger = createLogger('cli:ask');
|
|
19
|
+
|
|
20
|
+
export function registerAskCommand(program: Command): void {
|
|
21
|
+
program
|
|
22
|
+
.command('ask <query>')
|
|
23
|
+
.description('Ask the network — discover an agent and send a request in one step')
|
|
24
|
+
.option('--to <name-or-did>', 'Skip discovery, send to specific agent name or DID')
|
|
25
|
+
.option('--min-trust <score>', 'Minimum trust score (0-1)', '0')
|
|
26
|
+
.option('--timeout <seconds>', 'Response timeout in seconds', '30')
|
|
27
|
+
.option('--file <path>', 'Attach a file to the request')
|
|
28
|
+
.option('--protocol <protocol>', 'Protocol to use', '/clawiverse/msg/1.0.0')
|
|
29
|
+
.option('--dry-run', 'Show what would be sent without sending')
|
|
30
|
+
.option('--format <fmt>', 'Output format: text|json', 'text')
|
|
31
|
+
.action(async (query: string, options) => {
|
|
32
|
+
const client = new DaemonClient();
|
|
33
|
+
if (!(await client.isDaemonRunning())) {
|
|
34
|
+
console.error('Daemon not running. Start with: hw1 join');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
let targetDid: string | undefined;
|
|
40
|
+
|
|
41
|
+
// Resolve target
|
|
42
|
+
if (options.to) {
|
|
43
|
+
if (options.to.startsWith('did:')) {
|
|
44
|
+
targetDid = options.to;
|
|
45
|
+
} else {
|
|
46
|
+
// Name resolution via DHT
|
|
47
|
+
if (options.format !== 'json') process.stderr.write(`Searching for "${options.to}"...\n`);
|
|
48
|
+
const results = await client.send('discover', { query: options.to });
|
|
49
|
+
if (!results || results.length === 0) {
|
|
50
|
+
console.error(`No agent found matching: ${options.to}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
targetDid = results[0].did;
|
|
54
|
+
if (options.format !== 'json') process.stderr.write(`Found: ${results[0].name ?? targetDid}\n`);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
// Semantic discovery
|
|
58
|
+
if (options.format !== 'json') process.stderr.write(`Searching for agents matching: "${query}"...\n`);
|
|
59
|
+
const minTrust = parseFloat(options.minTrust);
|
|
60
|
+
const results = await client.send('discover', {
|
|
61
|
+
query,
|
|
62
|
+
filters: minTrust > 0 ? { minTrustScore: minTrust } : undefined,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!results || results.length === 0) {
|
|
66
|
+
console.error(`No agents found for: ${query}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Pick highest-trust match
|
|
71
|
+
const sorted = [...results].sort((a: any, b: any) => {
|
|
72
|
+
const ta = a.trust?.interactionScore ?? 0;
|
|
73
|
+
const tb = b.trust?.interactionScore ?? 0;
|
|
74
|
+
return tb - ta;
|
|
75
|
+
});
|
|
76
|
+
targetDid = sorted[0].did;
|
|
77
|
+
if (options.format !== 'json') {
|
|
78
|
+
process.stderr.write(`Best match: ${sorted[0].name ?? targetDid} (trust: ${((sorted[0].trust?.interactionScore ?? 0) * 100).toFixed(0)}%)\n`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Build payload
|
|
83
|
+
const payload: Record<string, unknown> = { text: query };
|
|
84
|
+
|
|
85
|
+
if (options.file) {
|
|
86
|
+
const fileBytes = await readFile(options.file);
|
|
87
|
+
const filename = basename(options.file);
|
|
88
|
+
payload.attachment = {
|
|
89
|
+
filename,
|
|
90
|
+
mimeType: guessMimeType(filename),
|
|
91
|
+
size: fileBytes.length,
|
|
92
|
+
data: fileBytes.toString('base64'),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (options.dryRun) {
|
|
97
|
+
const out = { to: targetDid, protocol: options.protocol, payload };
|
|
98
|
+
if (options.format === 'json') {
|
|
99
|
+
console.log(JSON.stringify(out, null, 2));
|
|
100
|
+
} else {
|
|
101
|
+
console.log(`[dry-run] Would send to: ${targetDid}`);
|
|
102
|
+
console.log(`[dry-run] Protocol: ${options.protocol}`);
|
|
103
|
+
console.log(`[dry-run] Payload: ${JSON.stringify(payload)}`);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (options.format !== 'json') process.stderr.write(`Sending request...\n`);
|
|
109
|
+
|
|
110
|
+
const result = await client.send('send', {
|
|
111
|
+
to: targetDid,
|
|
112
|
+
protocol: options.protocol,
|
|
113
|
+
payload,
|
|
114
|
+
type: 'request',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (options.format === 'json') {
|
|
118
|
+
console.log(JSON.stringify({
|
|
119
|
+
from: targetDid,
|
|
120
|
+
messageId: result.id,
|
|
121
|
+
response: result.response ?? null,
|
|
122
|
+
verified: true,
|
|
123
|
+
}, null, 2));
|
|
124
|
+
} else {
|
|
125
|
+
if (result.response) {
|
|
126
|
+
const resp = result.response;
|
|
127
|
+
const p = resp.payload as any;
|
|
128
|
+
if (p?.text) {
|
|
129
|
+
console.log(p.text);
|
|
130
|
+
} else if (p?.result) {
|
|
131
|
+
console.log(typeof p.result === 'string' ? p.result : JSON.stringify(p.result, null, 2));
|
|
132
|
+
} else {
|
|
133
|
+
console.log(JSON.stringify(p, null, 2));
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
console.log(`Request sent (${result.id.slice(-8)}). No response received within timeout.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
logger.error('Ask failed', err);
|
|
141
|
+
console.error('Error:', (err as Error).message);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function guessMimeType(filename: string): string {
|
|
148
|
+
const ext = filename.split('.').pop()?.toLowerCase() ?? '';
|
|
149
|
+
const map: Record<string, string> = {
|
|
150
|
+
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
|
151
|
+
gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
|
|
152
|
+
pdf: 'application/pdf', txt: 'text/plain', md: 'text/markdown',
|
|
153
|
+
json: 'application/json', csv: 'text/csv',
|
|
154
|
+
mp3: 'audio/mpeg', mp4: 'video/mp4', wav: 'audio/wav',
|
|
155
|
+
zip: 'application/zip', gz: 'application/gzip',
|
|
156
|
+
};
|
|
157
|
+
return map[ext] ?? 'application/octet-stream';
|
|
158
|
+
}
|
package/src/commands/card.ts
CHANGED
|
@@ -9,10 +9,9 @@ export function registerCardCommand(program: Command): void {
|
|
|
9
9
|
card
|
|
10
10
|
.command('show')
|
|
11
11
|
.description('Show current Agent Card')
|
|
12
|
-
.
|
|
12
|
+
.option('--format <fmt>', 'Output format: text|json', 'text')
|
|
13
|
+
.action(async (options) => {
|
|
13
14
|
try {
|
|
14
|
-
printHeader('Agent Card');
|
|
15
|
-
|
|
16
15
|
const agentCard = getAgentCard();
|
|
17
16
|
|
|
18
17
|
if (!agentCard) {
|
|
@@ -20,6 +19,12 @@ export function registerCardCommand(program: Command): void {
|
|
|
20
19
|
process.exit(1);
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
if (options.format === 'json') {
|
|
23
|
+
console.log(JSON.stringify(agentCard, null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
printHeader('Agent Card');
|
|
23
28
|
console.log(JSON.stringify(agentCard, null, 2));
|
|
24
29
|
} catch (err) {
|
|
25
30
|
error(`Failed to show card: ${(err as Error).message}`);
|
package/src/commands/discover.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
createRelayClient,
|
|
4
|
+
createRelayIndexOperations,
|
|
4
5
|
importKeyPair,
|
|
5
|
-
|
|
6
|
+
createAgentCard,
|
|
7
|
+
signAgentCard,
|
|
8
|
+
sign,
|
|
6
9
|
} from '@highway1/core';
|
|
7
|
-
import { getIdentity,
|
|
10
|
+
import { getIdentity, getAgentCard } from '../config.js';
|
|
8
11
|
import { error, spinner, printHeader, info, success } from '../ui.js';
|
|
9
12
|
import Table from 'cli-table3';
|
|
10
13
|
|
|
14
|
+
const DEFAULT_RELAY_URLS = ['ws://relay.highway1.net:8080'];
|
|
15
|
+
|
|
16
|
+
function getRelayUrls(options: any): string[] {
|
|
17
|
+
if (options.relay) return [options.relay];
|
|
18
|
+
const envRelays = process.env.HW1_RELAY_URLS;
|
|
19
|
+
if (envRelays) return envRelays.split(',').map((u: string) => u.trim());
|
|
20
|
+
return DEFAULT_RELAY_URLS;
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
export function registerDiscoverCommand(program: Command): void {
|
|
12
24
|
program
|
|
13
25
|
.command('discover')
|
|
@@ -18,7 +30,7 @@ export function registerDiscoverCommand(program: Command): void {
|
|
|
18
30
|
.option('--min-trust <score>', 'Minimum trust score (0-1)')
|
|
19
31
|
.option('--language <lang>', 'Filter by language')
|
|
20
32
|
.option('--limit <number>', 'Maximum number of results', '10')
|
|
21
|
-
.option('--
|
|
33
|
+
.option('--relay <url>', 'Relay WebSocket URL')
|
|
22
34
|
.action(async (options) => {
|
|
23
35
|
try {
|
|
24
36
|
printHeader('Discover Agents');
|
|
@@ -30,56 +42,58 @@ export function registerDiscoverCommand(program: Command): void {
|
|
|
30
42
|
process.exit(1);
|
|
31
43
|
}
|
|
32
44
|
|
|
33
|
-
const spin = spinner('
|
|
45
|
+
const spin = spinner('Connecting to relay...');
|
|
34
46
|
|
|
35
47
|
const keyPair = importKeyPair({
|
|
36
48
|
publicKey: identity.publicKey,
|
|
37
49
|
privateKey: identity.privateKey,
|
|
38
50
|
});
|
|
39
51
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const
|
|
52
|
+
const relayUrls = getRelayUrls(options);
|
|
53
|
+
const card = getAgentCard();
|
|
54
|
+
const capabilities = (card?.capabilities ?? []).map((c: string) => ({
|
|
55
|
+
id: c, name: c, description: `Capability: ${c}`,
|
|
56
|
+
}));
|
|
57
|
+
const agentCard = createAgentCard(
|
|
58
|
+
identity.did,
|
|
59
|
+
card?.name ?? 'Clawiverse Agent',
|
|
60
|
+
card?.description ?? '',
|
|
61
|
+
capabilities,
|
|
62
|
+
[],
|
|
63
|
+
);
|
|
64
|
+
const signedCard = await signAgentCard(agentCard, (data) => sign(data, keyPair.privateKey));
|
|
65
|
+
|
|
66
|
+
const relayClient = createRelayClient({
|
|
67
|
+
relayUrls,
|
|
68
|
+
did: identity.did,
|
|
43
69
|
keyPair,
|
|
44
|
-
|
|
45
|
-
enableDHT: true,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
await node.start();
|
|
49
|
-
|
|
50
|
-
spin.text = 'Waiting for DHT peers...';
|
|
51
|
-
// Wait until connected to at least one peer, then give DHT time to sync
|
|
52
|
-
await new Promise<void>((resolve) => {
|
|
53
|
-
const timeout = setTimeout(resolve, 15000);
|
|
54
|
-
node.libp2p.addEventListener('peer:connect', () => {
|
|
55
|
-
clearTimeout(timeout);
|
|
56
|
-
setTimeout(resolve, 3000); // give DHT routing table time to populate
|
|
57
|
-
}, { once: true });
|
|
70
|
+
card: signedCard,
|
|
58
71
|
});
|
|
59
72
|
|
|
60
|
-
|
|
73
|
+
await relayClient.start();
|
|
74
|
+
spin.text = 'Querying relay...';
|
|
61
75
|
|
|
62
|
-
const
|
|
76
|
+
const relayIndex = createRelayIndexOperations(relayClient);
|
|
63
77
|
|
|
64
78
|
if (options.did) {
|
|
65
|
-
const
|
|
79
|
+
const agentCard = await relayIndex.queryAgentCard(options.did);
|
|
66
80
|
|
|
67
|
-
if (
|
|
81
|
+
if (agentCard) {
|
|
68
82
|
spin.succeed('Agent found!');
|
|
69
83
|
console.log();
|
|
70
|
-
info(`DID: ${
|
|
71
|
-
info(`Name: ${
|
|
72
|
-
info(`Description: ${
|
|
73
|
-
info(`Version: ${
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
info(`
|
|
84
|
+
info(`DID: ${agentCard.did}`);
|
|
85
|
+
info(`Name: ${agentCard.name}`);
|
|
86
|
+
info(`Description: ${agentCard.description}`);
|
|
87
|
+
info(`Version: ${agentCard.version}`);
|
|
88
|
+
const caps = agentCard.capabilities.map((c: any) =>
|
|
89
|
+
typeof c === 'string' ? c : c.name
|
|
90
|
+
).join(', ');
|
|
91
|
+
info(`Capabilities: ${caps || '(none)'}`);
|
|
92
|
+
info(`Timestamp: ${new Date(agentCard.timestamp).toISOString()}`);
|
|
78
93
|
} else {
|
|
79
94
|
spin.fail('Agent not found');
|
|
80
95
|
}
|
|
81
96
|
} else if (options.capability || options.query) {
|
|
82
|
-
// Semantic search
|
|
83
97
|
const query: any = {
|
|
84
98
|
limit: parseInt(options.limit, 10),
|
|
85
99
|
};
|
|
@@ -91,9 +105,7 @@ export function registerDiscoverCommand(program: Command): void {
|
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
if (options.minTrust) {
|
|
94
|
-
query.filters = {
|
|
95
|
-
minTrustScore: parseFloat(options.minTrust),
|
|
96
|
-
};
|
|
108
|
+
query.filters = { minTrustScore: parseFloat(options.minTrust) };
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
if (options.language) {
|
|
@@ -101,7 +113,7 @@ export function registerDiscoverCommand(program: Command): void {
|
|
|
101
113
|
query.filters.language = options.language;
|
|
102
114
|
}
|
|
103
115
|
|
|
104
|
-
const cards = await
|
|
116
|
+
const cards = await relayIndex.searchSemantic(query);
|
|
105
117
|
|
|
106
118
|
spin.succeed(`Found ${cards.length} agents`);
|
|
107
119
|
|
|
@@ -133,7 +145,6 @@ export function registerDiscoverCommand(program: Command): void {
|
|
|
133
145
|
console.log();
|
|
134
146
|
console.log(table.toString());
|
|
135
147
|
|
|
136
|
-
// Show detailed info for first result if query was used
|
|
137
148
|
if (options.query && cards.length > 0) {
|
|
138
149
|
const card = cards[0];
|
|
139
150
|
console.log();
|
|
@@ -156,10 +167,9 @@ export function registerDiscoverCommand(program: Command): void {
|
|
|
156
167
|
info('Usage: hw1 discover --did <did>');
|
|
157
168
|
info(' hw1 discover --capability <capability>');
|
|
158
169
|
info(' hw1 discover --query "translate Japanese"');
|
|
159
|
-
info(' hw1 discover --query "code review" --min-trust 0.8');
|
|
160
170
|
}
|
|
161
171
|
|
|
162
|
-
await
|
|
172
|
+
await relayClient.stop();
|
|
163
173
|
} catch (err) {
|
|
164
174
|
error(`Failed to discover: ${(err as Error).message}`);
|
|
165
175
|
process.exit(1);
|
package/src/commands/identity.ts
CHANGED
|
@@ -6,10 +6,9 @@ export function registerIdentityCommand(program: Command): void {
|
|
|
6
6
|
program
|
|
7
7
|
.command('identity')
|
|
8
8
|
.description('Show current identity information')
|
|
9
|
-
.
|
|
9
|
+
.option('--format <fmt>', 'Output format: text|json', 'text')
|
|
10
|
+
.action(async (options) => {
|
|
10
11
|
try {
|
|
11
|
-
printHeader('Clawiverse Identity');
|
|
12
|
-
|
|
13
12
|
const identity = getIdentity();
|
|
14
13
|
const card = getAgentCard();
|
|
15
14
|
|
|
@@ -18,6 +17,16 @@ export function registerIdentityCommand(program: Command): void {
|
|
|
18
17
|
process.exit(1);
|
|
19
18
|
}
|
|
20
19
|
|
|
20
|
+
if (options.format === 'json') {
|
|
21
|
+
console.log(JSON.stringify({
|
|
22
|
+
did: identity.did,
|
|
23
|
+
publicKey: identity.publicKey,
|
|
24
|
+
agentCard: card || null,
|
|
25
|
+
}, null, 2));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
printHeader('Clawiverse Identity');
|
|
21
30
|
printKeyValue('DID', identity.did);
|
|
22
31
|
printKeyValue('Public Key', identity.publicKey.substring(0, 16) + '...');
|
|
23
32
|
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbox Command - CVP-0010 §2.3
|
|
3
|
+
*
|
|
4
|
+
* hw1 inbox - List messages (newest first)
|
|
5
|
+
* hw1 inbox --unread - Only unread
|
|
6
|
+
* hw1 inbox read <id> - Show full message
|
|
7
|
+
* hw1 inbox reply <id> - Reply to a message
|
|
8
|
+
* hw1 inbox delete <id> - Delete a message
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { DaemonClient } from '../daemon/client.js';
|
|
13
|
+
import { createLogger } from '@highway1/core';
|
|
14
|
+
|
|
15
|
+
const logger = createLogger('cli:inbox');
|
|
16
|
+
|
|
17
|
+
function shortDid(did: string): string {
|
|
18
|
+
if (did.startsWith('did:clawiverse:')) return did.slice(15, 29) + '…';
|
|
19
|
+
if (did.length > 30) return did.slice(0, 14) + '…' + did.slice(-8);
|
|
20
|
+
return did;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatAge(ts: number): string {
|
|
24
|
+
const secs = Math.floor((Date.now() - ts) / 1000);
|
|
25
|
+
if (secs < 60) return `${secs}s ago`;
|
|
26
|
+
if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
|
|
27
|
+
if (secs < 86400) return `${Math.floor(secs / 3600)}h ago`;
|
|
28
|
+
return `${Math.floor(secs / 86400)}d ago`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createInboxCommand(): Command {
|
|
32
|
+
const inbox = new Command('inbox')
|
|
33
|
+
.description('Manage your message inbox (CVP-0010 §2.3)')
|
|
34
|
+
.option('--unread', 'Show only unread messages')
|
|
35
|
+
.option('--from <did>', 'Filter by sender DID')
|
|
36
|
+
.option('--protocol <protocol>', 'Filter by protocol')
|
|
37
|
+
.option('--limit <n>', 'Max messages to show', '50')
|
|
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
|
+
try {
|
|
47
|
+
const filter: any = {};
|
|
48
|
+
if (options.unread) filter.unreadOnly = true;
|
|
49
|
+
if (options.from) filter.fromDid = options.from;
|
|
50
|
+
if (options.protocol) filter.protocol = options.protocol;
|
|
51
|
+
|
|
52
|
+
const page = await client.send('inbox', {
|
|
53
|
+
filter,
|
|
54
|
+
pagination: { limit: parseInt(options.limit, 10) },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (options.format === 'json') {
|
|
58
|
+
console.log(JSON.stringify(page, null, 2));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const unreadCount = page.messages.filter((m: any) => !m.readAt).length;
|
|
63
|
+
console.log(`\nInbox (${page.total} total, ${unreadCount} unread)\n`);
|
|
64
|
+
|
|
65
|
+
if (page.messages.length === 0) {
|
|
66
|
+
console.log(' No messages.');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const msg of page.messages) {
|
|
71
|
+
const unread = !msg.readAt ? '●' : ' ';
|
|
72
|
+
const from = shortDid(msg.envelope.from);
|
|
73
|
+
const age = formatAge(msg.receivedAt ?? msg.envelope.timestamp);
|
|
74
|
+
const id = msg.envelope.id.slice(-8);
|
|
75
|
+
const proto = msg.envelope.protocol ?? '';
|
|
76
|
+
const text = typeof msg.envelope.payload === 'object' && msg.envelope.payload !== null
|
|
77
|
+
? (msg.envelope.payload as any).text ?? (msg.envelope.payload as any).message ?? ''
|
|
78
|
+
: String(msg.envelope.payload ?? '');
|
|
79
|
+
|
|
80
|
+
console.log(`${unread} [${id}] ${from} ${age} ${proto}`);
|
|
81
|
+
if (text) {
|
|
82
|
+
const preview = String(text).slice(0, 72);
|
|
83
|
+
console.log(` ${preview}${text.length > 72 ? '…' : ''}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
console.log();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.error('Inbox list failed', error);
|
|
89
|
+
console.error('Error:', (error as Error).message);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// hw1 inbox read <id>
|
|
95
|
+
inbox
|
|
96
|
+
.command('read <id>')
|
|
97
|
+
.description('Show full message')
|
|
98
|
+
.option('--format <fmt>', 'Output format: text|json', 'text')
|
|
99
|
+
.action(async (id: string, options) => {
|
|
100
|
+
const client = new DaemonClient();
|
|
101
|
+
if (!(await client.isDaemonRunning())) {
|
|
102
|
+
console.error('Daemon not running. Start with: hw1 join');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Support short IDs: find by suffix
|
|
108
|
+
let msg = await client.send('get_message', { id }).catch(() => null);
|
|
109
|
+
|
|
110
|
+
if (!msg) {
|
|
111
|
+
// Try to find by short suffix via inbox list
|
|
112
|
+
const page = await client.send('inbox', { filter: {}, pagination: { limit: 200 } });
|
|
113
|
+
const match = page.messages.find((m: any) => m.envelope.id.endsWith(id));
|
|
114
|
+
if (match) {
|
|
115
|
+
msg = await client.send('get_message', { id: match.envelope.id });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!msg) {
|
|
120
|
+
console.error(`Message not found: ${id}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (options.format === 'json') {
|
|
125
|
+
console.log(JSON.stringify(msg, null, 2));
|
|
126
|
+
} else {
|
|
127
|
+
console.log('\n' + '─'.repeat(60));
|
|
128
|
+
console.log(`ID: ${msg.envelope.id}`);
|
|
129
|
+
console.log(`From: ${msg.envelope.from}`);
|
|
130
|
+
console.log(`Protocol: ${msg.envelope.protocol}`);
|
|
131
|
+
console.log(`Type: ${msg.envelope.type}`);
|
|
132
|
+
console.log(`Received: ${new Date(msg.receivedAt ?? msg.envelope.timestamp).toLocaleString()}`);
|
|
133
|
+
if (msg.trustScore != null) {
|
|
134
|
+
console.log(`Trust: ${(msg.trustScore * 100).toFixed(0)}%`);
|
|
135
|
+
}
|
|
136
|
+
console.log('─'.repeat(60));
|
|
137
|
+
console.log(JSON.stringify(msg.envelope.payload, null, 2));
|
|
138
|
+
console.log();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Mark as read
|
|
142
|
+
await client.send('mark_read', { id: msg.envelope.id });
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logger.error('Read message failed', error);
|
|
145
|
+
console.error('Error:', (error as Error).message);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// hw1 inbox reply <id> --message "..."
|
|
151
|
+
inbox
|
|
152
|
+
.command('reply <id>')
|
|
153
|
+
.description('Reply to a message')
|
|
154
|
+
.requiredOption('-m, --message <text>', 'Reply text')
|
|
155
|
+
.option('--protocol <protocol>', 'Protocol to use', 'clawiverse/chat/1.0.0')
|
|
156
|
+
.action(async (id: string, options) => {
|
|
157
|
+
const client = new DaemonClient();
|
|
158
|
+
if (!(await client.isDaemonRunning())) {
|
|
159
|
+
console.error('Daemon not running. Start with: hw1 join');
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// Resolve message
|
|
165
|
+
let msg = await client.send('get_message', { id }).catch(() => null);
|
|
166
|
+
if (!msg) {
|
|
167
|
+
const page = await client.send('inbox', { filter: {}, pagination: { limit: 200 } });
|
|
168
|
+
const match = page.messages.find((m: any) => m.envelope.id.endsWith(id));
|
|
169
|
+
if (match) msg = await client.send('get_message', { id: match.envelope.id });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!msg) {
|
|
173
|
+
console.error(`Message not found: ${id}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const result = await client.send('send', {
|
|
178
|
+
to: msg.envelope.from,
|
|
179
|
+
protocol: options.protocol,
|
|
180
|
+
payload: { text: options.message, replyTo: msg.envelope.id },
|
|
181
|
+
type: 'response',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
console.log(`Reply sent (${result.id.slice(-8)})`);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
logger.error('Reply failed', error);
|
|
187
|
+
console.error('Error:', (error as Error).message);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// hw1 inbox delete <id>
|
|
193
|
+
inbox
|
|
194
|
+
.command('delete <id>')
|
|
195
|
+
.description('Delete a message')
|
|
196
|
+
.action(async (id: string) => {
|
|
197
|
+
const client = new DaemonClient();
|
|
198
|
+
if (!(await client.isDaemonRunning())) {
|
|
199
|
+
console.error('Daemon not running. Start with: hw1 join');
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// Resolve short ID
|
|
205
|
+
let fullId = id;
|
|
206
|
+
if (id.length < 36) {
|
|
207
|
+
const page = await client.send('inbox', { filter: {}, pagination: { limit: 200 } });
|
|
208
|
+
const match = page.messages.find((m: any) => m.envelope.id.endsWith(id));
|
|
209
|
+
if (match) fullId = match.envelope.id;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await client.send('delete_message', { id: fullId });
|
|
213
|
+
console.log(`Deleted message ${id}`);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
logger.error('Delete failed', error);
|
|
216
|
+
console.error('Error:', (error as Error).message);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return inbox;
|
|
222
|
+
}
|