zyndo 0.1.5 → 0.1.6
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/mcp/mcpCore.d.ts +1 -2
- package/dist/mcp/mcpCore.js +43 -30
- package/dist/providers/claudeCode.js +26 -52
- package/package.json +3 -1
package/dist/mcp/mcpCore.d.ts
CHANGED
|
@@ -13,8 +13,7 @@ export type McpSessionState = {
|
|
|
13
13
|
};
|
|
14
14
|
export declare const TOOLS: ReadonlyArray<McpToolDefinition>;
|
|
15
15
|
export declare const TERMINAL_STATES: Set<string>;
|
|
16
|
-
export declare const
|
|
17
|
-
export declare const MAX_WAIT_SECONDS = 600;
|
|
16
|
+
export declare const WAIT_LONG_POLL_MS = 270000;
|
|
18
17
|
export declare function eventToState(eventType: string): string;
|
|
19
18
|
export declare function handleToolCall(state: McpSessionState, name: string, args: Record<string, unknown>): Promise<string>;
|
|
20
19
|
export declare function mcpSuccess(id: string | number, result: unknown): string;
|
package/dist/mcp/mcpCore.js
CHANGED
|
@@ -24,7 +24,7 @@ export const TOOLS = [
|
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
name: 'zyndo_browse_sellers',
|
|
27
|
-
description: 'Browse available seller agents on the Zyndo marketplace. Returns a list of sellers with
|
|
27
|
+
description: 'Browse available seller agents on the Zyndo marketplace. Returns a list of sellers, each with skills that include `buyerPriceCents` — the total amount in USD cents your buyer will be charged for that skill (already includes the Zyndo marketplace fee). This is the ONLY price field: quote `buyerPriceCents` verbatim to your human and confirm before calling zyndo_hire.',
|
|
28
28
|
inputSchema: {
|
|
29
29
|
type: 'object',
|
|
30
30
|
properties: {
|
|
@@ -86,7 +86,7 @@ export const TOOLS = [
|
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
name: 'zyndo_wait_for_completion',
|
|
89
|
-
description: '
|
|
89
|
+
description: 'Blocks until the seller delivers, asks a question, or the task reaches a terminal state (completed, failed, rejected, canceled). Wakes on the broker event bus, NOT a polling loop. Call this EXACTLY ONCE after zyndo_hire and wait for it to return — do NOT call it in a loop. If it returns state "still-working" after ~4-5 minutes, you may call it again.',
|
|
90
90
|
inputSchema: {
|
|
91
91
|
type: 'object',
|
|
92
92
|
properties: {
|
|
@@ -125,8 +125,10 @@ export const TOOLS = [
|
|
|
125
125
|
// Constants
|
|
126
126
|
// ---------------------------------------------------------------------------
|
|
127
127
|
export const TERMINAL_STATES = new Set(['completed', 'failed', 'canceled', 'rejected']);
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
// Long-poll window the CLI asks the broker to hold the connection open for.
|
|
129
|
+
// Capped under Railway's 300s idle timeout so the proxy never tears it down
|
|
130
|
+
// mid-wait. See broker /agent/tasks/:id/wait for the server-side cap.
|
|
131
|
+
export const WAIT_LONG_POLL_MS = 270_000;
|
|
130
132
|
// ---------------------------------------------------------------------------
|
|
131
133
|
// Auto-reconnect helpers
|
|
132
134
|
// ---------------------------------------------------------------------------
|
|
@@ -356,37 +358,48 @@ async function handleRequestRevision(state, args) {
|
|
|
356
358
|
message: `Revision #${data.revisionNumber} requested. Use zyndo_wait_for_completion to wait for the updated delivery.`
|
|
357
359
|
});
|
|
358
360
|
}
|
|
359
|
-
const WAIT_SHORT_WINDOW_MS = 25_000; // Max wall time per tool call — safely under MCP client timeouts
|
|
360
361
|
async function handleWaitForCompletion(state, args) {
|
|
361
362
|
if (state.agentSession === undefined)
|
|
362
363
|
return JSON.stringify({ error: 'Not connected. Call zyndo_connect first.' });
|
|
363
364
|
const taskId = args.taskId;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
365
|
+
// ONE long-poll request. The broker holds the connection open and wakes on
|
|
366
|
+
// the task event bus — no client-side polling loop. The client-side fetch
|
|
367
|
+
// gets a slightly longer AbortController timeout than the broker's wait
|
|
368
|
+
// ceiling so we never abort a response that's about to arrive.
|
|
369
|
+
const controller = new AbortController();
|
|
370
|
+
const abortTimer = setTimeout(() => controller.abort(), WAIT_LONG_POLL_MS + 10_000);
|
|
371
|
+
let res;
|
|
372
|
+
try {
|
|
373
|
+
res = await fetchWithReconnect(state, (s) => `${state.bridgeUrl}/agent/tasks/${taskId}/wait?timeoutMs=${WAIT_LONG_POLL_MS}`, (s) => ({ headers: { authorization: `Bearer ${s.token}` }, signal: controller.signal }));
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
clearTimeout(abortTimer);
|
|
377
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
378
|
+
return JSON.stringify({ taskId, state: 'still-working', message: `Long-poll interrupted (${message}). Call zyndo_wait_for_completion again to resume waiting.` });
|
|
379
|
+
}
|
|
380
|
+
clearTimeout(abortTimer);
|
|
381
|
+
if (res.status === 403) {
|
|
382
|
+
return JSON.stringify({
|
|
383
|
+
error: 'Access denied (403). This task belongs to a different session. Use zyndo_connect with your original reconnectToken to recover it.',
|
|
384
|
+
taskId
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (!res.ok) {
|
|
388
|
+
return JSON.stringify({ error: `Broker returned ${res.status}`, taskId });
|
|
389
|
+
}
|
|
390
|
+
const detail = (await res.json());
|
|
391
|
+
if (TERMINAL_STATES.has(detail.state)) {
|
|
392
|
+
return JSON.stringify({ taskId, state: detail.state, message: `Task reached terminal state: ${detail.state}` });
|
|
393
|
+
}
|
|
394
|
+
if (detail.state === 'input-required') {
|
|
395
|
+
const hint = detail.inputType === 'delivery'
|
|
396
|
+
? 'Seller has delivered. Use zyndo_get_delivery to retrieve the output, then zyndo_complete_task or zyndo_request_revision.'
|
|
397
|
+
: 'Seller has a question. Use zyndo_get_messages to read it and zyndo_send_response to answer.';
|
|
398
|
+
return JSON.stringify({ taskId, state: detail.state, inputType: detail.inputType, message: hint });
|
|
388
399
|
}
|
|
389
|
-
|
|
400
|
+
// Broker hit its timeout cap before any state change. Safe for the LLM to
|
|
401
|
+
// call again — a second call is still ~30x cheaper than the legacy 10s poll.
|
|
402
|
+
return JSON.stringify({ taskId, state: 'still-working', message: 'Seller is still working. Call zyndo_wait_for_completion again to keep waiting.' });
|
|
390
403
|
}
|
|
391
404
|
// ---------------------------------------------------------------------------
|
|
392
405
|
// Tool dispatch
|
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
// Claude Code provider — spawns a CLI harness (claude, codex, or generic)
|
|
3
3
|
// instead of making raw LLM API calls.
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
|
-
|
|
5
|
+
// Use cross-spawn instead of node:child_process spawn. cross-spawn resolves
|
|
6
|
+
// Windows .cmd/.bat shims (codex.cmd, claude.cmd installed via `npm i -g`)
|
|
7
|
+
// directly to their underlying node entry script, so we can spawn without
|
|
8
|
+
// shell:true. Piping the prompt through node:child_process spawn + shell:true
|
|
9
|
+
// on Windows deadlocks: cmd.exe does not reliably forward stdin EOF to the
|
|
10
|
+
// grandchild node process behind the .cmd shim, and codex blocks forever
|
|
11
|
+
// on `stdin.read()`, hitting our 600s abort.
|
|
12
|
+
import spawn from 'cross-spawn';
|
|
6
13
|
import { basename, join } from 'node:path';
|
|
7
14
|
import { tmpdir } from 'node:os';
|
|
8
15
|
import { randomBytes } from 'node:crypto';
|
|
@@ -140,49 +147,6 @@ function parseOutput(raw, harness) {
|
|
|
140
147
|
return { output: trimmed, paused: false };
|
|
141
148
|
}
|
|
142
149
|
// ---------------------------------------------------------------------------
|
|
143
|
-
// Cross-platform spawn helpers
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
/**
|
|
146
|
-
* Quote a single arg for cmd.exe. Wraps in double quotes if it contains
|
|
147
|
-
* any whitespace, quote, or shell metacharacter. Internal double quotes are
|
|
148
|
-
* doubled per cmd.exe convention. We only ever pass flag values and paths
|
|
149
|
-
* here (the prompt goes via stdin), so this is sufficient.
|
|
150
|
-
*/
|
|
151
|
-
function quoteWinArg(arg) {
|
|
152
|
-
if (arg === '')
|
|
153
|
-
return '""';
|
|
154
|
-
if (!/[\s"&|<>^()]/.test(arg))
|
|
155
|
-
return arg;
|
|
156
|
-
return `"${arg.replace(/"/g, '""')}"`;
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Spawn a child process. On Windows, builds a single shell-quoted command
|
|
160
|
-
* string and passes shell:true so .cmd / .bat shims (codex.cmd, claude.cmd)
|
|
161
|
-
* resolve correctly. On POSIX, spawns the binary directly with the args
|
|
162
|
-
* array — no shell, no quoting concerns.
|
|
163
|
-
*
|
|
164
|
-
* Avoids Node DEP0190 (the warning fires when you pass shell:true together
|
|
165
|
-
* with an args array, because Node concatenates without escaping).
|
|
166
|
-
*/
|
|
167
|
-
function spawnHarness(binary, args, opts) {
|
|
168
|
-
if (process.platform === 'win32') {
|
|
169
|
-
const cmdLine = [binary, ...args].map(quoteWinArg).join(' ');
|
|
170
|
-
return spawn(cmdLine, {
|
|
171
|
-
cwd: opts.cwd,
|
|
172
|
-
signal: opts.signal,
|
|
173
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
174
|
-
env: opts.env,
|
|
175
|
-
shell: true
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
return spawn(binary, [...args], {
|
|
179
|
-
cwd: opts.cwd,
|
|
180
|
-
signal: opts.signal,
|
|
181
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
182
|
-
env: opts.env
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
150
|
// Main entry point
|
|
187
151
|
// ---------------------------------------------------------------------------
|
|
188
152
|
const DEFAULT_TIMEOUT_MS = 600_000; // 10 minutes
|
|
@@ -197,19 +161,20 @@ export async function runClaudeCodeTask(taskContext, config, logger) {
|
|
|
197
161
|
return new Promise((resolve) => {
|
|
198
162
|
const controller = new AbortController();
|
|
199
163
|
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
200
|
-
// Windows .cmd
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
164
|
+
// cross-spawn handles Windows .cmd/.bat shim resolution without shell:true,
|
|
165
|
+
// so stdin pipes propagate EOF correctly on both Windows and POSIX. Without
|
|
166
|
+
// this, Windows + codex.cmd deadlocks because cmd.exe does not forward the
|
|
167
|
+
// stdin close to the real node child.
|
|
168
|
+
const proc = spawn(binary, [...args], {
|
|
204
169
|
cwd: config.workingDirectory,
|
|
205
170
|
signal: controller.signal,
|
|
171
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
206
172
|
env: { ...process.env }
|
|
207
173
|
});
|
|
208
174
|
const stdoutChunks = [];
|
|
209
175
|
const stderrChunks = [];
|
|
210
|
-
// stdio is always ['pipe','pipe','pipe']
|
|
211
|
-
//
|
|
212
|
-
// does not narrow that, so assert.
|
|
176
|
+
// stdio is always ['pipe','pipe','pipe'] so the streams are guaranteed
|
|
177
|
+
// non-null at runtime. The wrapped return type does not narrow that, so assert.
|
|
213
178
|
proc.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
|
|
214
179
|
proc.stderr.on('data', (chunk) => stderrChunks.push(chunk));
|
|
215
180
|
proc.on('error', (err) => {
|
|
@@ -252,7 +217,16 @@ export async function runClaudeCodeTask(taskContext, config, logger) {
|
|
|
252
217
|
}
|
|
253
218
|
resolve(parseOutput(agentOutput, harness));
|
|
254
219
|
});
|
|
255
|
-
|
|
220
|
+
// Surface stdin write failures loudly. If the child closes its stdin
|
|
221
|
+
// early (e.g. a shim-resolution regression), silence would mean a 600s
|
|
222
|
+
// abort with no signal. Logging the error lets us fail fast.
|
|
223
|
+
proc.stdin.on('error', (err) => {
|
|
224
|
+
logger.error(`Harness stdin error: ${err.message}`);
|
|
225
|
+
});
|
|
226
|
+
proc.stdin.write(prompt, (err) => {
|
|
227
|
+
if (err)
|
|
228
|
+
logger.error(`Harness stdin write failed: ${err.message}`);
|
|
229
|
+
});
|
|
256
230
|
proc.stdin.end();
|
|
257
231
|
});
|
|
258
232
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zyndo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "The agent-to-agent CLI tool for sellers in the Zyndo Marketplace",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,9 +35,11 @@
|
|
|
35
35
|
"lint": "tsc -p tsconfig.json --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"cross-spawn": "^7.0.6",
|
|
38
39
|
"yaml": "^2.7.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
42
|
+
"@types/cross-spawn": "^6.0.6",
|
|
41
43
|
"@types/node": "^22.12.0",
|
|
42
44
|
"typescript": "^5.7.3",
|
|
43
45
|
"vitest": "^2.1.9"
|