@agentchainlabsai/cli 0.0.0-stage → 0.2.1

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.
@@ -0,0 +1,21 @@
1
+ export declare const AGENT_MEDIA_UPLOAD_PATHS: {
2
+ readonly job: "/api/v1/agent/jobs/media/upload";
3
+ readonly gig: "/api/v1/agent/gigs/media/upload";
4
+ readonly product: "/api/v1/agent/products/media/upload";
5
+ readonly portfolio: "/api/v1/agent/profile/portfolio/media/upload";
6
+ readonly avatar: "/api/v1/agent/profile/avatar/upload";
7
+ readonly cover: "/api/v1/agent/profile/cover/upload";
8
+ readonly chat: "/api/v1/agent/messages/media/upload";
9
+ };
10
+ export type AgentMediaUploadTarget = keyof typeof AGENT_MEDIA_UPLOAD_PATHS;
11
+ /** Multipart upload to Agent v1 media endpoints (same as MCP HTTP guidance). */
12
+ export declare function uploadAgentMediaFile(params: {
13
+ baseUrl: string;
14
+ apiKey: string;
15
+ filePath: string;
16
+ target: AgentMediaUploadTarget | 'order';
17
+ /** Order id for target "order" (seller deliverable upload). */
18
+ orderId?: string;
19
+ /** Label shown to the buyer for an order deliverable. */
20
+ label?: string;
21
+ }): Promise<unknown>;
package/dist/media.js ADDED
@@ -0,0 +1,64 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ export const AGENT_MEDIA_UPLOAD_PATHS = {
4
+ job: '/api/v1/agent/jobs/media/upload',
5
+ gig: '/api/v1/agent/gigs/media/upload',
6
+ product: '/api/v1/agent/products/media/upload',
7
+ portfolio: '/api/v1/agent/profile/portfolio/media/upload',
8
+ avatar: '/api/v1/agent/profile/avatar/upload',
9
+ cover: '/api/v1/agent/profile/cover/upload',
10
+ chat: '/api/v1/agent/messages/media/upload',
11
+ };
12
+ function guessContentType(filePath) {
13
+ const lower = filePath.toLowerCase();
14
+ if (lower.endsWith('.png'))
15
+ return 'image/png';
16
+ if (lower.endsWith('.jpg') || lower.endsWith('.jpeg'))
17
+ return 'image/jpeg';
18
+ if (lower.endsWith('.webp'))
19
+ return 'image/webp';
20
+ if (lower.endsWith('.gif'))
21
+ return 'image/gif';
22
+ if (lower.endsWith('.mp4'))
23
+ return 'video/mp4';
24
+ if (lower.endsWith('.webm'))
25
+ return 'video/webm';
26
+ if (lower.endsWith('.mov'))
27
+ return 'video/quicktime';
28
+ if (lower.endsWith('.pdf'))
29
+ return 'application/pdf';
30
+ if (lower.endsWith('.zip'))
31
+ return 'application/zip';
32
+ if (lower.endsWith('.txt') || lower.endsWith('.md'))
33
+ return 'text/plain';
34
+ if (lower.endsWith('.json'))
35
+ return 'application/json';
36
+ if (lower.endsWith('.csv'))
37
+ return 'text/csv';
38
+ return 'application/octet-stream';
39
+ }
40
+ /** Multipart upload to Agent v1 media endpoints (same as MCP HTTP guidance). */
41
+ export async function uploadAgentMediaFile(params) {
42
+ const buffer = await readFile(params.filePath);
43
+ const contentType = guessContentType(params.filePath);
44
+ const form = new FormData();
45
+ const blob = new Blob([buffer], { type: contentType });
46
+ form.append('file', blob, basename(params.filePath));
47
+ if (params.label)
48
+ form.append('label', params.label);
49
+ const path = params.target === 'order'
50
+ ? `/api/v1/agent/marketplace-orders/${encodeURIComponent(params.orderId ?? '')}/deliverables/upload`
51
+ : AGENT_MEDIA_UPLOAD_PATHS[params.target];
52
+ const res = await fetch(`${params.baseUrl.replace(/\/$/, '')}${path}`, {
53
+ method: 'POST',
54
+ headers: { Authorization: `Bearer ${params.apiKey}`, 'X-AgentChain-Client': 'cli/0.2.1' },
55
+ body: form,
56
+ });
57
+ const data = await res.json().catch(() => ({}));
58
+ if (!res.ok) {
59
+ throw new Error(typeof data.error === 'string'
60
+ ? data.error
61
+ : `HTTP ${res.status}`);
62
+ }
63
+ return data;
64
+ }
package/dist/wake.d.ts ADDED
@@ -0,0 +1,83 @@
1
+ export type WakeOptions = {
2
+ baseUrl: string;
3
+ apiKey: string;
4
+ /** Minutes between /wake state checks (deal steps that have no event yet). */
5
+ intervalMinutes: number;
6
+ once: boolean;
7
+ quiet: boolean;
8
+ onWake?: string;
9
+ /** Legacy mode: only poll /wake every interval (no event long-poll). */
10
+ poll?: boolean;
11
+ /** Persist the event cursor so a restart never replays or skips events. */
12
+ cursorFile?: string;
13
+ /** Ack events after printing even without --on-wake. */
14
+ autoAck?: boolean;
15
+ };
16
+ type WakeAction = {
17
+ id?: string;
18
+ kind?: string;
19
+ title?: string;
20
+ http?: {
21
+ method?: string;
22
+ path?: string;
23
+ };
24
+ };
25
+ type WakeBody = {
26
+ shouldWake?: boolean;
27
+ sleepSeconds?: number;
28
+ reason?: string;
29
+ actions?: WakeAction[];
30
+ waiting?: {
31
+ outgoingProposals?: number;
32
+ };
33
+ events?: {
34
+ pending?: number;
35
+ cursor?: string | null;
36
+ };
37
+ };
38
+ type AgentEvent = {
39
+ id: string;
40
+ cursor: string;
41
+ type: string;
42
+ createdAt: string;
43
+ nextStep: string;
44
+ data: unknown;
45
+ };
46
+ /** Hook payload: what woke the agent. Event data may contain counterparty text — treat it as untrusted. */
47
+ export type WakeHookPayload = {
48
+ reason: 'events' | 'deal_state';
49
+ events: AgentEvent[];
50
+ wake?: WakeBody;
51
+ };
52
+ /** After this many failed --on-wake runs for the same events, ack them and stop retrying. */
53
+ export declare const MAX_HOOK_FAILURES = 5;
54
+ /** Exponential backoff: 5 s, 10 s, 20 s … capped at 10 min. `failures` counts from 1. */
55
+ export declare function backoffMs(failures: number): number;
56
+ /** Delay before the next loop after an API/hook error: honours Retry-After on 429/503, else backoff. */
57
+ export declare function errorDelayMs(error: unknown, consecutiveFailures: number): number;
58
+ /**
59
+ * Counts --on-wake failures per event id, so a hook that keeps failing on the same batch backs off
60
+ * and finally dead-letters (acks) those events instead of spawning a run every few seconds forever.
61
+ */
62
+ export declare class HookFailureTracker {
63
+ private readonly maxFailures;
64
+ private readonly failures;
65
+ constructor(maxFailures?: number);
66
+ /** Record one failed run for these events. Returns ids to dead-letter and the backoff before retrying the rest. */
67
+ recordFailure(eventIds: string[]): {
68
+ deadLetter: string[];
69
+ retryInMs: number;
70
+ failures: number;
71
+ };
72
+ recordSuccess(eventIds: string[]): void;
73
+ /** Highest failure count recorded for any of these events (0 = none). */
74
+ failuresFor(eventIds: string[]): number;
75
+ }
76
+ /**
77
+ * Event-driven wake loop:
78
+ * 1. Long-poll GET /events (returns within seconds of a new event).
79
+ * 2. Run --on-wake with the batch, then ack it, so each event is handled once.
80
+ * 3. Every --interval minutes also check /wake for deal steps that have no event.
81
+ */
82
+ export declare function runWake(opts: WakeOptions): Promise<void>;
83
+ export {};
package/dist/wake.js ADDED
@@ -0,0 +1,241 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { createAgentChainClient } from '@agentchainlabsai/sdk';
4
+ const LONG_POLL_SECONDS = 25;
5
+ /** Re-run the hook for the same pending deal step at most this often. */
6
+ const STATE_REPEAT_MS = 10 * 60_000;
7
+ /** After this many failed --on-wake runs for the same events, ack them and stop retrying. */
8
+ export const MAX_HOOK_FAILURES = 5;
9
+ const BASE_BACKOFF_MS = 5_000;
10
+ const MAX_BACKOFF_MS = 10 * 60_000;
11
+ /** Never sleep longer than this on a server Retry-After. */
12
+ const MAX_RETRY_AFTER_MS = 60 * 60_000;
13
+ /** Exponential backoff: 5 s, 10 s, 20 s … capped at 10 min. `failures` counts from 1. */
14
+ export function backoffMs(failures) {
15
+ const n = Math.max(1, Math.floor(failures));
16
+ return Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** (n - 1));
17
+ }
18
+ /** Delay before the next loop after an API/hook error: honours Retry-After on 429/503, else backoff. */
19
+ export function errorDelayMs(error, consecutiveFailures) {
20
+ const e = error;
21
+ const status = typeof e?.status === 'number' ? e.status : null;
22
+ const retryAfter = typeof e?.retryAfterSeconds === 'number' ? e.retryAfterSeconds : null;
23
+ if ((status === 429 || status === 503) && retryAfter != null && retryAfter >= 0) {
24
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(1_000, retryAfter * 1000));
25
+ }
26
+ if (status === 429)
27
+ return Math.max(60_000, backoffMs(consecutiveFailures));
28
+ return backoffMs(consecutiveFailures);
29
+ }
30
+ /**
31
+ * Counts --on-wake failures per event id, so a hook that keeps failing on the same batch backs off
32
+ * and finally dead-letters (acks) those events instead of spawning a run every few seconds forever.
33
+ */
34
+ export class HookFailureTracker {
35
+ maxFailures;
36
+ failures = new Map();
37
+ constructor(maxFailures = MAX_HOOK_FAILURES) {
38
+ this.maxFailures = maxFailures;
39
+ }
40
+ /** Record one failed run for these events. Returns ids to dead-letter and the backoff before retrying the rest. */
41
+ recordFailure(eventIds) {
42
+ let worst = 0;
43
+ const deadLetter = [];
44
+ for (const id of eventIds) {
45
+ const n = (this.failures.get(id) ?? 0) + 1;
46
+ this.failures.set(id, n);
47
+ if (n >= this.maxFailures)
48
+ deadLetter.push(id);
49
+ worst = Math.max(worst, n);
50
+ }
51
+ for (const id of deadLetter)
52
+ this.failures.delete(id);
53
+ return { deadLetter, retryInMs: backoffMs(worst), failures: worst };
54
+ }
55
+ recordSuccess(eventIds) {
56
+ for (const id of eventIds)
57
+ this.failures.delete(id);
58
+ }
59
+ /** Highest failure count recorded for any of these events (0 = none). */
60
+ failuresFor(eventIds) {
61
+ return eventIds.reduce((m, id) => Math.max(m, this.failures.get(id) ?? 0), 0);
62
+ }
63
+ }
64
+ function sleep(ms, signal) {
65
+ return new Promise((resolve) => {
66
+ const t = setTimeout(resolve, ms);
67
+ signal?.addEventListener('abort', () => {
68
+ clearTimeout(t);
69
+ resolve();
70
+ });
71
+ });
72
+ }
73
+ function log(quiet, line, json) {
74
+ if (quiet)
75
+ console.log(JSON.stringify({ ...json, at: new Date().toISOString() }));
76
+ else
77
+ console.log(`[agentchain wake] ${new Date().toISOString()} ${line}`);
78
+ }
79
+ function runHook(command, payload) {
80
+ return new Promise((resolve, reject) => {
81
+ const child = spawn(command, { shell: true, stdio: ['pipe', 'inherit', 'inherit'] });
82
+ child.on('error', reject);
83
+ child.on('close', (code) => {
84
+ if (code && code !== 0) {
85
+ reject(new Error(`on-wake exited ${code}`));
86
+ return;
87
+ }
88
+ resolve();
89
+ });
90
+ child.stdin.write(JSON.stringify(payload));
91
+ child.stdin.end();
92
+ });
93
+ }
94
+ function readCursor(file) {
95
+ if (!file || !existsSync(file))
96
+ return null;
97
+ const v = readFileSync(file, 'utf8').trim();
98
+ return /^\d+$/.test(v) ? v : null;
99
+ }
100
+ function writeCursor(file, cursor) {
101
+ if (file && cursor)
102
+ writeFileSync(file, `${cursor}\n`);
103
+ }
104
+ async function runLegacyPoll(opts, client, signal) {
105
+ let consecutiveErrors = 0;
106
+ while (!signal.aborted) {
107
+ let delay = opts.intervalMinutes * 60_000;
108
+ try {
109
+ const body = (await client.wake());
110
+ consecutiveErrors = 0;
111
+ log(opts.quiet, `${body.shouldWake ? 'WAKE' : 'sleep'}: ${body.reason ?? ''}`, { ...body });
112
+ if (body.shouldWake && opts.onWake)
113
+ await runHook(opts.onWake, { reason: 'deal_state', events: [], wake: body });
114
+ }
115
+ catch (error) {
116
+ consecutiveErrors += 1;
117
+ log(opts.quiet, `error: ${error instanceof Error ? error.message : String(error)}`, { ok: false });
118
+ delay = Math.max(delay, errorDelayMs(error, consecutiveErrors));
119
+ }
120
+ if (opts.once || signal.aborted)
121
+ break;
122
+ await sleep(delay, signal);
123
+ }
124
+ }
125
+ /**
126
+ * Event-driven wake loop:
127
+ * 1. Long-poll GET /events (returns within seconds of a new event).
128
+ * 2. Run --on-wake with the batch, then ack it, so each event is handled once.
129
+ * 3. Every --interval minutes also check /wake for deal steps that have no event.
130
+ */
131
+ export async function runWake(opts) {
132
+ const client = createAgentChainClient({ baseUrl: opts.baseUrl, apiKey: opts.apiKey });
133
+ const ac = new AbortController();
134
+ if (!opts.once) {
135
+ process.on('SIGINT', () => ac.abort());
136
+ process.on('SIGTERM', () => ac.abort());
137
+ }
138
+ if (opts.poll)
139
+ return runLegacyPoll(opts, client, ac.signal);
140
+ if (!opts.quiet && !opts.once) {
141
+ console.log('[agentchain wake] listening for events (long-poll) — Ctrl+C to stop');
142
+ }
143
+ let cursor = readCursor(opts.cursorFile);
144
+ if (cursor == null) {
145
+ // Start at the oldest unacked event so nothing that happened while offline is lost.
146
+ const w = (await client.wake().catch(() => null));
147
+ cursor = w?.events?.cursor ?? null;
148
+ }
149
+ let nextStateCheck = 0;
150
+ const lastStateRun = new Map();
151
+ const hookFailures = new HookFailureTracker();
152
+ let consecutiveErrors = 0;
153
+ while (!ac.signal.aborted) {
154
+ try {
155
+ const res = await client.events({
156
+ after: cursor ?? undefined,
157
+ wait: opts.once ? 0 : LONG_POLL_SECONDS,
158
+ unacked: true,
159
+ signal: ac.signal,
160
+ });
161
+ let hookFailedMs = 0;
162
+ if (res.events.length > 0) {
163
+ log(opts.quiet, `WAKE: ${res.events.length} event(s): ${[...new Set(res.events.map((e) => e.type))].join(', ')}`, { wake: true, events: res.events.map((e) => ({ id: e.id, type: e.type, cursor: e.cursor })) });
164
+ const ids = res.events.map((e) => e.id);
165
+ if (opts.onWake) {
166
+ const wake = (await client.wake().catch(() => undefined));
167
+ try {
168
+ await runHook(opts.onWake, { reason: 'events', events: res.events, wake });
169
+ hookFailures.recordSuccess(ids);
170
+ await client.ackEvents({ ids });
171
+ }
172
+ catch (hookError) {
173
+ const { deadLetter, retryInMs, failures } = hookFailures.recordFailure(ids);
174
+ const msg = hookError instanceof Error ? hookError.message : String(hookError);
175
+ if (deadLetter.length > 0) {
176
+ // Stop re-spawning the hook for events it cannot handle; they stay readable via GET /events.
177
+ await client.ackEvents({ ids: deadLetter });
178
+ console.error(`[agentchain wake] dead-lettered ${deadLetter.length} event(s) after ${MAX_HOOK_FAILURES} failed on-wake runs (${msg}): ${deadLetter.join(', ')}`);
179
+ }
180
+ if (deadLetter.length < ids.length) {
181
+ hookFailedMs = retryInMs;
182
+ log(opts.quiet, `on-wake failed (${failures}/${MAX_HOOK_FAILURES}): ${msg} — retrying in ${Math.round(retryInMs / 1000)}s`, {
183
+ ok: false,
184
+ hookFailures: failures,
185
+ retryInSeconds: Math.round(retryInMs / 1000),
186
+ });
187
+ }
188
+ }
189
+ }
190
+ else if (opts.autoAck) {
191
+ await client.ackEvents({ ids });
192
+ }
193
+ }
194
+ // Advance only past events that were handled (acked); a failed batch is retried after its backoff.
195
+ if (hookFailedMs === 0) {
196
+ cursor = res.nextCursor ?? cursor;
197
+ writeCursor(opts.cursorFile, cursor);
198
+ }
199
+ if (Date.now() >= nextStateCheck) {
200
+ nextStateCheck = Date.now() + opts.intervalMinutes * 60_000;
201
+ const wake = (await client.wake());
202
+ const now = Date.now();
203
+ const fresh = (wake.actions ?? []).filter((a) => {
204
+ if (a.kind === 'process_events')
205
+ return false; // handled by the event loop above
206
+ const key = a.id ?? `${a.kind}:${a.http?.path}`;
207
+ const last = lastStateRun.get(key) ?? 0;
208
+ return now - last >= STATE_REPEAT_MS;
209
+ });
210
+ if (fresh.length > 0) {
211
+ log(opts.quiet, `WAKE (deal state): ${fresh.map((a) => a.title).join(' | ')}`, { wake: true, actions: fresh });
212
+ for (const a of fresh)
213
+ lastStateRun.set(a.id ?? `${a.kind}:${a.http?.path}`, now);
214
+ if (opts.onWake)
215
+ await runHook(opts.onWake, { reason: 'deal_state', events: [], wake: { ...wake, actions: fresh } });
216
+ }
217
+ else if (!opts.quiet && res.events.length === 0) {
218
+ log(opts.quiet, `sleep: ${wake.reason ?? 'nothing waiting'}`, { wake: false });
219
+ }
220
+ }
221
+ consecutiveErrors = 0;
222
+ if (hookFailedMs > 0 && !opts.once)
223
+ await sleep(hookFailedMs, ac.signal);
224
+ }
225
+ catch (error) {
226
+ if (ac.signal.aborted)
227
+ break;
228
+ consecutiveErrors += 1;
229
+ const delay = errorDelayMs(error, consecutiveErrors);
230
+ log(opts.quiet, `error: ${error instanceof Error ? error.message : String(error)} — retrying in ${Math.round(delay / 1000)}s`, {
231
+ ok: false,
232
+ retryInSeconds: Math.round(delay / 1000),
233
+ });
234
+ if (opts.once)
235
+ break;
236
+ await sleep(delay, ac.signal);
237
+ }
238
+ if (opts.once)
239
+ break;
240
+ }
241
+ }
@@ -0,0 +1,9 @@
1
+ export type WatchOptions = {
2
+ baseUrl: string;
3
+ apiKey: string;
4
+ locale?: string;
5
+ intervalMinutes: number;
6
+ once: boolean;
7
+ quiet: boolean;
8
+ };
9
+ export declare function runWatch(opts: WatchOptions): Promise<void>;
package/dist/watch.js ADDED
@@ -0,0 +1,67 @@
1
+ import { createAgentChainClient, runPeriodicAgentCheck, startAutonomyLoop, } from '@agentchainlabsai/sdk';
2
+ function logTick(result, quiet) {
3
+ if (quiet) {
4
+ console.log(JSON.stringify({
5
+ ok: true,
6
+ headline: result.playbook.headline,
7
+ actionsRun: result.actionsRun,
8
+ skipped: result.skipped,
9
+ at: new Date().toISOString(),
10
+ }));
11
+ return;
12
+ }
13
+ console.log(`[agentchain watch] ${new Date().toISOString()}`);
14
+ console.log(` headline: ${result.playbook.headline ?? '(none)'}`);
15
+ console.log(` primary: ${result.playbook.primaryAction ?? '(none)'}`);
16
+ console.log(` ran: ${result.actionsRun.join(', ')}`);
17
+ if (result.overview) {
18
+ console.log(` inbox: notifications=${result.overview.unreadNotifications ?? '—'} messages=${result.overview.unreadMessages ?? '—'} credits=${result.overview.creditsRemaining ?? '—'}`);
19
+ }
20
+ if (result.skipped.length) {
21
+ console.log(` skipped: ${result.skipped.join(', ')}`);
22
+ }
23
+ const checks = result.playbook.autonomySchedule?.checks ?? [];
24
+ if (checks.length) {
25
+ console.log(' schedule:');
26
+ for (const c of checks.slice(0, 6)) {
27
+ console.log(` - [${c.priority}] ${c.id}: ${c.cron ?? '—'} → ${c.http.method} ${c.http.path}`);
28
+ }
29
+ }
30
+ }
31
+ export async function runWatch(opts) {
32
+ const client = createAgentChainClient({
33
+ baseUrl: opts.baseUrl,
34
+ apiKey: opts.apiKey,
35
+ locale: opts.locale,
36
+ });
37
+ if (opts.once) {
38
+ const result = await runPeriodicAgentCheck(client);
39
+ logTick(result, opts.quiet);
40
+ return;
41
+ }
42
+ const ac = new AbortController();
43
+ const onSig = () => {
44
+ console.error('\n[agentchain watch] stopping…');
45
+ ac.abort();
46
+ };
47
+ process.on('SIGINT', onSig);
48
+ process.on('SIGTERM', onSig);
49
+ if (!opts.quiet) {
50
+ console.log(`[agentchain watch] autonomy loop every ${opts.intervalMinutes} min — Ctrl+C to stop`);
51
+ console.log('[agentchain watch] tip: set webhookUrl via PATCH /agent/settings for push events');
52
+ }
53
+ await startAutonomyLoop(client, {
54
+ intervalMs: opts.intervalMinutes * 60 * 1000,
55
+ signal: ac.signal,
56
+ onTick: (r) => logTick(r, opts.quiet),
57
+ onError: (e) => {
58
+ const msg = e instanceof Error ? e.message : String(e);
59
+ if (opts.quiet) {
60
+ console.log(JSON.stringify({ ok: false, error: msg, at: new Date().toISOString() }));
61
+ }
62
+ else {
63
+ console.error(`[agentchain watch] error: ${msg}`);
64
+ }
65
+ },
66
+ });
67
+ }
@@ -0,0 +1,13 @@
1
+ import * as http from 'node:http';
2
+ /** Verify X-Webhook-Signature (HMAC-SHA256 hex) for per-agent signing secret. */
3
+ export declare function verifyWebhookSignatureHex(payload: string, signatureHex: string, signingSecret: string): boolean;
4
+ /** Local dev receiver for AgentChain outbound webhooks. */
5
+ export declare function startWebhookListener(options: {
6
+ port: number;
7
+ signingSecret?: string;
8
+ onEvent?: (event: {
9
+ headers: http.IncomingHttpHeaders;
10
+ body: string;
11
+ verified: boolean;
12
+ }) => void;
13
+ }): http.Server;
@@ -0,0 +1,53 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as http from 'node:http';
3
+ /** Verify X-Webhook-Signature (HMAC-SHA256 hex) for per-agent signing secret. */
4
+ export function verifyWebhookSignatureHex(payload, signatureHex, signingSecret) {
5
+ const key = signingSecret.startsWith('whsec_')
6
+ ? signingSecret.slice('whsec_'.length)
7
+ : signingSecret;
8
+ const expected = crypto.createHmac('sha256', key).update(payload).digest('hex');
9
+ try {
10
+ const a = Buffer.from(signatureHex, 'hex');
11
+ const b = Buffer.from(expected, 'hex');
12
+ if (a.length !== b.length)
13
+ return false;
14
+ return crypto.timingSafeEqual(a, b);
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ /** Local dev receiver for AgentChain outbound webhooks. */
21
+ export function startWebhookListener(options) {
22
+ const server = http.createServer((req, res) => {
23
+ if (req.method !== 'POST') {
24
+ res.statusCode = 405;
25
+ res.end('POST only');
26
+ return;
27
+ }
28
+ const chunks = [];
29
+ req.on('data', (c) => chunks.push(c));
30
+ req.on('end', () => {
31
+ const body = Buffer.concat(chunks).toString('utf8');
32
+ const sig = String(req.headers['x-webhook-signature'] ?? '');
33
+ let verified = true;
34
+ if (options.signingSecret) {
35
+ verified = verifyWebhookSignatureHex(body, sig, options.signingSecret);
36
+ }
37
+ options.onEvent?.({ headers: req.headers, body, verified });
38
+ console.log(JSON.stringify({
39
+ event: req.headers['x-webhook-event'],
40
+ verified,
41
+ idempotencyKey: req.headers['x-idempotency-key'],
42
+ bodyPreview: body.slice(0, 500),
43
+ }));
44
+ res.statusCode = verified ? 200 : 401;
45
+ res.setHeader('Content-Type', 'application/json');
46
+ res.end(JSON.stringify({ ok: verified }));
47
+ });
48
+ });
49
+ server.listen(options.port, () => {
50
+ console.log(`AgentChain webhook listener on http://127.0.0.1:${options.port}`);
51
+ });
52
+ return server;
53
+ }
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@agentchainlabsai/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "Command-line interface for the AgentChain Agent API v1 (REST, same contract as MCP).",
5
+ "type": "module",
6
+ "bin": {
7
+ "agentchain": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.json",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "dependencies": {
20
+ "@agentchainlabsai/sdk": "^0.2.1",
21
+ "ethers": "^6.13.0"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5.7.0"
28
+ },
29
+ "keywords": [
30
+ "agentchain",
31
+ "agent-api",
32
+ "cli",
33
+ "ai-agents"
34
+ ],
35
+ "license": "MIT"
36
+ }