@highway1/cli 0.1.49 → 0.1.50

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,12 +1,17 @@
1
1
  {
2
2
  "name": "@highway1/cli",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
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
16
  "@highway1/core": "^0.1.46",
12
17
  "chalk": "^5.3.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}`);
@@ -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
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Peers Command - CVP-0010 §2.4
3
+ *
4
+ * hw1 peers - List online agents
5
+ * hw1 peers --capability translate - Filter by capability
6
+ * hw1 peers --format json
7
+ */
8
+
9
+ import { Command } from 'commander';
10
+ import { DaemonClient } from '../daemon/client.js';
11
+ import { createLogger } from '@highway1/core';
12
+
13
+ const logger = createLogger('cli:peers');
14
+
15
+ export function registerPeersCommand(program: Command): void {
16
+ program
17
+ .command('peers')
18
+ .description('List agents on the network')
19
+ .option('--capability <cap>', 'Filter by capability')
20
+ .option('--query <text>', 'Natural language search')
21
+ .option('--min-trust <score>', 'Minimum trust score (0-1)')
22
+ .option('--limit <n>', 'Max results', '20')
23
+ .option('--format <fmt>', 'Output format: text|json', 'text')
24
+ .action(async (options) => {
25
+ const client = new DaemonClient();
26
+ if (!(await client.isDaemonRunning())) {
27
+ console.error('Daemon not running. Start with: hw1 join');
28
+ process.exit(1);
29
+ }
30
+
31
+ try {
32
+ const searchQuery = options.query ?? options.capability ?? '';
33
+ const params: any = { query: searchQuery };
34
+ if (options.minTrust) {
35
+ params.filters = { minTrustScore: parseFloat(options.minTrust) };
36
+ }
37
+
38
+ const results = await client.send('discover', params);
39
+ const agents: any[] = results ?? [];
40
+
41
+ // Filter by capability if specified
42
+ const filtered = options.capability
43
+ ? agents.filter((a: any) => {
44
+ const caps: any[] = a.capabilities ?? [];
45
+ return caps.some((c: any) => {
46
+ const name = typeof c === 'string' ? c : c.name ?? c.id ?? '';
47
+ return name.toLowerCase().includes(options.capability.toLowerCase());
48
+ });
49
+ })
50
+ : agents;
51
+
52
+ const limited = filtered.slice(0, parseInt(options.limit, 10));
53
+
54
+ if (options.format === 'json') {
55
+ console.log(JSON.stringify(limited, null, 2));
56
+ return;
57
+ }
58
+
59
+ if (limited.length === 0) {
60
+ console.log('No agents found.');
61
+ return;
62
+ }
63
+
64
+ console.log(`\nPeers (${limited.length} found)\n`);
65
+ for (const agent of limited) {
66
+ const trust = agent.trust?.interactionScore ?? 0;
67
+ const trustStr = `${(trust * 100).toFixed(0)}%`;
68
+ const caps = (agent.capabilities ?? [])
69
+ .map((c: any) => typeof c === 'string' ? c : c.name ?? c.id ?? '')
70
+ .filter(Boolean)
71
+ .slice(0, 3)
72
+ .join(', ');
73
+ const shortDid = agent.did?.slice(0, 30) + '…';
74
+ console.log(` ${agent.name ?? shortDid} trust:${trustStr}`);
75
+ if (caps) console.log(` capabilities: ${caps}`);
76
+ console.log(` did: ${agent.did}`);
77
+ }
78
+ console.log();
79
+ } catch (err) {
80
+ logger.error('Peers failed', err);
81
+ console.error('Error:', (err as Error).message);
82
+ process.exit(1);
83
+ }
84
+ });
85
+ }
@@ -20,16 +20,18 @@ export function registerSendCommand(program: Command): void {
20
20
  program
21
21
  .command('send')
22
22
  .description('Send a message to another agent')
23
- .requiredOption('--to <did>', 'Recipient DID')
24
- .requiredOption('--protocol <protocol>', 'Protocol identifier')
23
+ .requiredOption('--to <did-or-name>', 'Recipient DID or agent name')
24
+ .option('--protocol <protocol>', 'Protocol identifier', '/clawiverse/msg/1.0.0') // CVP-0010 §3.1: default protocol
25
+ .option('--message <text>', 'Message text (shorthand for --payload \'{"text":"..."}\')') // CVP-0010 §3.1: --message shorthand
25
26
  .option('--payload <json>', 'Message payload (JSON)')
26
27
  .option('--file <path>', 'Attach a file (image, binary, text) as payload attachment')
27
- .option('--type <type>', 'Message type (request|notification)', 'request')
28
+ .option('--type <type>', 'Message type (request|notification)', 'request') // CVP-0010 §3.1: default type
28
29
  .option('--bootstrap <peers...>', 'Bootstrap peer addresses')
29
30
  .option('--peer <multiaddr>', 'Direct peer multiaddr (bypasses DHT lookup)')
31
+ .option('--format <fmt>', 'Output format: text|json', 'text') // CVP-0010 §3.2: --format json
30
32
  .action(async (options) => {
31
33
  try {
32
- printHeader('Send Message');
34
+ if (options.format !== 'json') printHeader('Send Message');
33
35
 
34
36
  const identity = getIdentity();
35
37
 
@@ -38,14 +40,18 @@ export function registerSendCommand(program: Command): void {
38
40
  process.exit(1);
39
41
  }
40
42
 
41
- if (!options.payload && !options.file) {
42
- error('Either --payload <json> or --file <path> is required.');
43
+ // CVP-0010 §3.1: --message shorthand
44
+ if (!options.payload && !options.file && !options.message) {
45
+ error('Either --message <text>, --payload <json>, or --file <path> is required.');
43
46
  process.exit(1);
44
47
  }
45
48
 
46
49
  let payload: Record<string, unknown> = {};
47
50
 
48
- if (options.payload) {
51
+ // CVP-0010 §3.1: --message shorthand wraps text into {"text": "..."}
52
+ if (options.message) {
53
+ payload.text = options.message;
54
+ } else if (options.payload) {
49
55
  try {
50
56
  payload = JSON.parse(options.payload);
51
57
  } catch {