@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/package.json CHANGED
@@ -1,14 +1,19 @@
1
1
  {
2
2
  "name": "@highway1/cli",
3
- "version": "0.1.49",
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.46",
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
+ }
@@ -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
- .action(async () => {
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}`);
@@ -1,13 +1,25 @@
1
1
  import { Command } from 'commander';
2
2
  import {
3
- createNode,
3
+ createRelayClient,
4
+ createRelayIndexOperations,
4
5
  importKeyPair,
5
- createDHTOperations,
6
+ createAgentCard,
7
+ signAgentCard,
8
+ sign,
6
9
  } from '@highway1/core';
7
- import { getIdentity, getBootstrapPeers } from '../config.js';
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('--bootstrap <peers...>', 'Bootstrap peer addresses')
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('Starting node...');
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 bootstrapPeers = options.bootstrap || getBootstrapPeers();
41
-
42
- const node = await createNode({
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
- bootstrapPeers,
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
- spin.text = 'Querying DHT...';
73
+ await relayClient.start();
74
+ spin.text = 'Querying relay...';
61
75
 
62
- const dht = createDHTOperations(node.libp2p);
76
+ const relayIndex = createRelayIndexOperations(relayClient);
63
77
 
64
78
  if (options.did) {
65
- const card = await dht.queryAgentCard(options.did);
79
+ const agentCard = await relayIndex.queryAgentCard(options.did);
66
80
 
67
- if (card) {
81
+ if (agentCard) {
68
82
  spin.succeed('Agent found!');
69
83
  console.log();
70
- info(`DID: ${card.did}`);
71
- info(`Name: ${card.name}`);
72
- info(`Description: ${card.description}`);
73
- info(`Version: ${card.version}`);
74
- info(`Capabilities: ${card.capabilities.join(', ') || '(none)'}`);
75
- info(`Peer ID: ${card.peerId || '(unknown)'}`);
76
- info(`Endpoints: ${card.endpoints.length > 0 ? card.endpoints.join('\n ') : '(none)'}`);
77
- info(`Timestamp: ${new Date(card.timestamp).toISOString()}`);
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 dht.searchSemantic(query);
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 node.stop();
172
+ await relayClient.stop();
163
173
  } catch (err) {
164
174
  error(`Failed to discover: ${(err as Error).message}`);
165
175
  process.exit(1);
@@ -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
- .action(async () => {
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
+ }