@ilucky21c/ajp-cli 0.1.0

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.
Files changed (2) hide show
  1. package/index.js +231 -0
  2. package/package.json +17 -0
package/index.js ADDED
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ajp-cli — Agent Job Protocol CLI
4
+ *
5
+ * Requires Provenance identity (PROVENANCE_ID + PROVENANCE_PRIVATE_KEY).
6
+ * For identity setup: npx provenance keygen / npx provenance register
7
+ *
8
+ * Usage:
9
+ * ajp hire <provenance_id> --instruction <text> [--budget <usd>] [--timeout <s>]
10
+ * ajp jobs <job_id> --endpoint <url>
11
+ */
12
+
13
+ import { createPrivateKey, sign as nodeSign, randomBytes } from 'crypto';
14
+
15
+ const API = process.env.PROVENANCE_API_URL || 'https://provenance-web-mu.vercel.app';
16
+ const VERSION = '0.1.0';
17
+
18
+ // ── Colours ───────────────────────────────────────────────────────────────────
19
+
20
+ const c = {
21
+ reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m',
22
+ green: '\x1b[32m', amber: '\x1b[33m', red: '\x1b[31m', blue: '\x1b[34m', white: '\x1b[97m',
23
+ };
24
+ const ok = s => `${c.green}✓${c.reset} ${s}`;
25
+ const err = s => `${c.red}✗${c.reset} ${s}`;
26
+ const dim = s => `${c.dim}${s}${c.reset}`;
27
+ const hi = s => `${c.white}${c.bold}${s}${c.reset}`;
28
+ const amb = s => `${c.amber}${s}${c.reset}`;
29
+
30
+ // ── Arg parsing ───────────────────────────────────────────────────────────────
31
+
32
+ function parseArgs(argv) {
33
+ const args = { _: [] };
34
+ let i = 0;
35
+ while (i < argv.length) {
36
+ const a = argv[i];
37
+ if (a.startsWith('--')) {
38
+ const key = a.slice(2);
39
+ const next = argv[i + 1];
40
+ if (next && !next.startsWith('--')) { args[key] = next; i += 2; }
41
+ else { args[key] = true; i++; }
42
+ } else { args._.push(a); i++; }
43
+ }
44
+ return args;
45
+ }
46
+
47
+ // ── Signing ───────────────────────────────────────────────────────────────────
48
+
49
+ function generateJobId() {
50
+ return `job_${Date.now().toString(36)}${randomBytes(6).toString('hex')}`;
51
+ }
52
+
53
+ function signOffer(offer, privateKeyBase64) {
54
+ const { signature: _, ...rest } = offer;
55
+ const canonical = JSON.stringify(rest, Object.keys(rest).sort());
56
+ const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' });
57
+ return `ed25519:${nodeSign(null, Buffer.from(canonical, 'utf8'), key).toString('base64')}`;
58
+ }
59
+
60
+ // ── Commands ──────────────────────────────────────────────────────────────────
61
+
62
+ async function cmdHire(args) {
63
+ const targetId = args._[1];
64
+ const instruction = args.instruction || args.i;
65
+ const budget = parseFloat(args.budget || args.b || '1.0');
66
+ const timeout = parseInt(args.timeout || args.t || '120');
67
+ const privateKey = args['private-key'] || process.env.PROVENANCE_PRIVATE_KEY;
68
+ const provenanceId = args['from-id'] || process.env.PROVENANCE_ID;
69
+
70
+ if (!targetId) { console.error(err('Usage: ajp hire <provenance_id> --instruction <text>')); process.exit(1); }
71
+ if (!instruction) { console.error(err('--instruction required')); process.exit(1); }
72
+ if (!privateKey) { console.error(err('PROVENANCE_PRIVATE_KEY not set. Run: npx provenance keygen')); process.exit(1); }
73
+ if (!provenanceId) { console.error(err('PROVENANCE_ID not set. Run: npx provenance register')); process.exit(1); }
74
+
75
+ console.log(`\n${amb('Hiring')} ${hi(targetId)}...\n`);
76
+
77
+ // Resolve endpoint
78
+ process.stdout.write(dim(' Resolving endpoint...'));
79
+ const agentRes = await fetch(`${API}/api/agent/${targetId.replace('provenance:', '').replace(':', '/')}`);
80
+ const agentData = await agentRes.json();
81
+ if (!agentData?.ajp?.endpoint) {
82
+ console.log('\n' + err('Agent has no AJP endpoint. Ask them to add ajp.endpoint to PROVENANCE.yml.'));
83
+ process.exit(1);
84
+ }
85
+ const endpoint = agentData.ajp.endpoint.replace(/\/$/, '');
86
+ console.log(` ${c.green}${endpoint}${c.reset}`);
87
+
88
+ // Build and sign offer
89
+ const now = new Date();
90
+ const expiresAt = new Date(now.getTime() + timeout * 1000);
91
+ const jobId = generateJobId();
92
+
93
+ const offer = {
94
+ ajp: '0.1', job_id: jobId, parent_job_id: null,
95
+ from: { type: 'orchestrator', id: null, provenance_id: provenanceId },
96
+ to: { provenance_id: targetId },
97
+ task: { type: 'task', instruction, input: {}, output_format: 'json' },
98
+ context: { credentials: {}, memory: [], constraints: [] },
99
+ budget: { max_usd: budget, max_seconds: timeout, max_llm_tokens: 10000 },
100
+ callback: null,
101
+ issued_at: now.toISOString(),
102
+ expires_at: expiresAt.toISOString(),
103
+ signature: '',
104
+ };
105
+ offer.signature = signOffer(offer, privateKey);
106
+
107
+ // Submit
108
+ process.stdout.write(dim(' Submitting job...'));
109
+ const submitRes = await fetch(`${endpoint}/jobs`, {
110
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(offer),
111
+ });
112
+ const submitData = await submitRes.json().catch(() => ({}));
113
+
114
+ if (!submitRes.ok) {
115
+ console.log('\n' + err(submitData.error || submitData.reason || `HTTP ${submitRes.status}`));
116
+ process.exit(1);
117
+ }
118
+ console.log(` ${c.green}${jobId}${c.reset}`);
119
+
120
+ // Poll
121
+ const deadline = Date.now() + timeout * 1000;
122
+ const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
123
+ let fi = 0;
124
+
125
+ while (Date.now() < deadline) {
126
+ await new Promise(r => setTimeout(r, 2000));
127
+ const pollRes = await fetch(`${endpoint}/jobs/${jobId}`);
128
+ const pollData = await pollRes.json().catch(() => ({}));
129
+
130
+ process.stdout.write(`\r ${c.blue}${frames[fi++ % frames.length]}${c.reset} ${dim(pollData.status || 'polling...')} `);
131
+
132
+ if (pollData.status === 'completed') {
133
+ await fetch(`${endpoint}/jobs/${jobId}/ack`, {
134
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
135
+ body: JSON.stringify({ received: true }),
136
+ }).catch(() => {});
137
+
138
+ const dur = pollData.usage?.duration_seconds?.toFixed(1);
139
+ process.stdout.write(`\r${ok(`Completed${dur ? ` in ${dur}s` : ''}`)} \n\n`);
140
+
141
+ const out = pollData.output;
142
+ console.log(typeof out === 'string' ? out : JSON.stringify(out, null, 2));
143
+
144
+ if (pollData.usage) {
145
+ const u = pollData.usage;
146
+ const parts = [];
147
+ if (u.duration_seconds != null) parts.push(`${u.duration_seconds.toFixed(1)}s`);
148
+ if (u.cost_usd > 0) parts.push(`$${u.cost_usd.toFixed(4)}`);
149
+ if (u.llm_tokens > 0) parts.push(`${u.llm_tokens} tokens`);
150
+ if (parts.length) console.log('\n' + dim(parts.join(' · ')));
151
+ }
152
+ console.log();
153
+ return;
154
+ }
155
+
156
+ if (['failed','expired','rejected'].includes(pollData.status)) {
157
+ process.stdout.write(`\r${err(pollData.status + (pollData.message ? ': ' + pollData.message : ''))} \n\n`);
158
+ process.exit(1);
159
+ }
160
+ }
161
+
162
+ console.log('\n' + err(`Timed out after ${timeout}s`));
163
+ process.exit(1);
164
+ }
165
+
166
+ async function cmdJobs(args) {
167
+ const jobId = args._[1];
168
+ const endpoint = args.endpoint;
169
+ if (!jobId) { console.error(err('Usage: ajp jobs <job_id> --endpoint <url>')); process.exit(1); }
170
+ if (!endpoint) { console.error(err('--endpoint required')); process.exit(1); }
171
+
172
+ const res = await fetch(`${endpoint.replace(/\/$/, '')}/jobs/${jobId}`);
173
+ const data = await res.json();
174
+
175
+ const statusColor = { completed: c.green, failed: c.red, expired: c.red, running: c.blue, accepted: c.amber }[data.status] || c.dim;
176
+ console.log(`\n${dim('job_id:')} ${data.job_id}`);
177
+ console.log(`${dim('status:')} ${statusColor}${data.status}${c.reset}`);
178
+ if (data.output) console.log(`\n${JSON.stringify(data.output, null, 2)}`);
179
+ if (data.message) console.log(`${c.red}${data.message}${c.reset}`);
180
+ console.log();
181
+ }
182
+
183
+ function cmdHelp() {
184
+ console.log(`
185
+ ${hi('ajp')} ${dim(`v${VERSION}`)} — Agent Job Protocol CLI
186
+
187
+ ${amb('Commands:')}
188
+ ${hi('hire')} <provenance_id> Send a job to an agent via AJP
189
+ --instruction <text> What you want the agent to do
190
+ [--budget <usd>] Max cost ceiling (default: 1.00)
191
+ [--timeout <seconds>] Max wait time (default: 120)
192
+ [--from-id <id>] Your Provenance ID (default: $PROVENANCE_ID)
193
+ [--private-key <key>] Your private key (default: $PROVENANCE_PRIVATE_KEY)
194
+
195
+ ${hi('jobs')} <job_id> Check status of a job
196
+ --endpoint <url> The agent's AJP endpoint URL
197
+
198
+ ${amb('Environment variables:')}
199
+ PROVENANCE_ID Your Provenance ID (set up with: npx provenance register)
200
+ PROVENANCE_PRIVATE_KEY Your Ed25519 private key (set up with: npx provenance keygen)
201
+ PROVENANCE_API_URL Override Provenance API base URL
202
+
203
+ ${amb('Examples:')}
204
+ ajp hire provenance:github:alice/summarizer \\
205
+ --instruction "Summarize https://arxiv.org/abs/2501.00001" \\
206
+ --budget 0.50 --timeout 60
207
+
208
+ ajp jobs job_m0abc123 --endpoint https://alice-agent.example.com/api/agent
209
+
210
+ ${amb('Identity setup (first time):')}
211
+ npx provenance keygen
212
+ npx provenance register --id provenance:github:your-org/your-agent --url <url>
213
+ ${dim('Then set PROVENANCE_ID and PROVENANCE_PRIVATE_KEY in your environment.')}
214
+ `);
215
+ }
216
+
217
+ // ── Main ──────────────────────────────────────────────────────────────────────
218
+
219
+ const argv = process.argv.slice(2);
220
+ const args = parseArgs(argv);
221
+ const cmd = args._[0];
222
+
223
+ try {
224
+ if (!cmd || cmd === 'help' || args.help) cmdHelp();
225
+ else if (cmd === 'hire') await cmdHire(args);
226
+ else if (cmd === 'jobs') await cmdJobs(args);
227
+ else { console.error(err(`Unknown command: ${cmd}\nRun \`ajp help\` for usage.`)); process.exit(1); }
228
+ } catch (e) {
229
+ console.error(err(e.message));
230
+ process.exit(1);
231
+ }
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@ilucky21c/ajp-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for the Agent Job Protocol — hire agents and check job status from the terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "ajp": "./index.js"
8
+ },
9
+ "engines": { "node": ">=18" },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/ilucky21c/ajp-protocol.git",
13
+ "directory": "cli"
14
+ },
15
+ "keywords": ["ajp", "agent-job-protocol", "provenance", "ai", "agents", "cli"],
16
+ "license": "MIT"
17
+ }