zyndo 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.
- package/dist/agentLoop.d.ts +14 -0
- package/dist/agentLoop.js +76 -0
- package/dist/banner.d.ts +1 -0
- package/dist/banner.js +25 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.js +109 -0
- package/dist/connection.d.ts +58 -0
- package/dist/connection.js +114 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +68 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +178 -0
- package/dist/mcp/mcpCore.d.ts +22 -0
- package/dist/mcp/mcpCore.js +448 -0
- package/dist/mcp/mcpServer.d.ts +1 -0
- package/dist/mcp/mcpServer.js +61 -0
- package/dist/providers/anthropic.d.ts +2 -0
- package/dist/providers/anthropic.js +52 -0
- package/dist/providers/claudeCode.d.ts +5 -0
- package/dist/providers/claudeCode.js +174 -0
- package/dist/providers/ollama.d.ts +2 -0
- package/dist/providers/ollama.js +90 -0
- package/dist/providers/openai.d.ts +2 -0
- package/dist/providers/openai.js +103 -0
- package/dist/providers/types.d.ts +33 -0
- package/dist/providers/types.js +4 -0
- package/dist/sellerDaemon.d.ts +9 -0
- package/dist/sellerDaemon.js +336 -0
- package/dist/state.d.ts +21 -0
- package/dist/state.js +63 -0
- package/dist/tools/askBuyer.d.ts +2 -0
- package/dist/tools/askBuyer.js +19 -0
- package/dist/tools/bash.d.ts +2 -0
- package/dist/tools/bash.js +35 -0
- package/dist/tools/glob.d.ts +2 -0
- package/dist/tools/glob.js +28 -0
- package/dist/tools/grep.d.ts +2 -0
- package/dist/tools/grep.js +36 -0
- package/dist/tools/pathSafety.d.ts +1 -0
- package/dist/tools/pathSafety.js +9 -0
- package/dist/tools/readFile.d.ts +2 -0
- package/dist/tools/readFile.js +35 -0
- package/dist/tools/types.d.ts +9 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/writeFile.d.ts +2 -0
- package/dist/tools/writeFile.js +28 -0
- package/package.json +36 -0
package/dist/init.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// zyndo-agent init — interactive seller config setup
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { writeFileSync, mkdirSync, existsSync, accessSync, readdirSync, constants as fsConstants } from 'node:fs';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { stringify as yamlStringify } from 'yaml';
|
|
9
|
+
import { printBanner } from './banner.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Readline helper
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function createPrompt() {
|
|
14
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
15
|
+
const ask = (question, defaultValue) => new Promise((resolve) => {
|
|
16
|
+
const suffix = defaultValue !== undefined ? ` (${defaultValue})` : '';
|
|
17
|
+
rl.question(` \x1b[36m?\x1b[0m ${question}${suffix}: `, (answer) => {
|
|
18
|
+
resolve(answer.trim() || defaultValue || '');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
return { ask, close: () => rl.close() };
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Harness detection
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
function binaryExists(name) {
|
|
27
|
+
// If it looks like an absolute path, check file exists and is executable
|
|
28
|
+
if (name.startsWith('/')) {
|
|
29
|
+
try {
|
|
30
|
+
accessSync(name, fsConstants.X_OK);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Otherwise check PATH via `which` (using execFileSync to avoid shell injection)
|
|
38
|
+
try {
|
|
39
|
+
execFileSync('which', [name], { stdio: 'pipe' });
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function findClaudeBinary() {
|
|
47
|
+
// Check PATH first
|
|
48
|
+
if (binaryExists('claude'))
|
|
49
|
+
return 'claude';
|
|
50
|
+
// Search common VS Code extension locations
|
|
51
|
+
const home = process.env.HOME ?? '';
|
|
52
|
+
if (home === '')
|
|
53
|
+
return undefined;
|
|
54
|
+
const vscodeDirs = [
|
|
55
|
+
resolve(home, '.vscode', 'extensions'),
|
|
56
|
+
resolve(home, '.vscode-insiders', 'extensions'),
|
|
57
|
+
resolve(home, '.cursor', 'extensions')
|
|
58
|
+
];
|
|
59
|
+
for (const extDir of vscodeDirs) {
|
|
60
|
+
if (!existsSync(extDir))
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
const entries = readdirSync(extDir).filter((e) => e.startsWith('anthropic.claude-code-'));
|
|
64
|
+
// Sort descending to prefer latest version
|
|
65
|
+
entries.sort().reverse();
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const candidate = resolve(extDir, entry, 'resources', 'native-binary', 'claude');
|
|
68
|
+
if (binaryExists(candidate))
|
|
69
|
+
return candidate;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Permission denied or other FS error, skip
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const HARNESS_OPTIONS = [
|
|
79
|
+
{ label: 'Claude Code', harness: 'claude', binary: 'claude', defaultModel: 'sonnet' },
|
|
80
|
+
{ label: 'Codex CLI', harness: 'codex', binary: 'codex', defaultModel: 'o4-mini' },
|
|
81
|
+
{ label: 'Other / Custom binary', harness: 'generic', binary: '', defaultModel: 'default' }
|
|
82
|
+
];
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Main
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
export async function runInit() {
|
|
87
|
+
printBanner();
|
|
88
|
+
process.stdout.write(' Welcome to the Zyndo marketplace agent CLI.\n');
|
|
89
|
+
process.stdout.write(' This will create your seller agent config.\n\n');
|
|
90
|
+
const { ask, close } = createPrompt();
|
|
91
|
+
try {
|
|
92
|
+
// Harness selection
|
|
93
|
+
process.stdout.write(' \x1b[1mSelect your AI harness:\x1b[0m\n');
|
|
94
|
+
for (let i = 0; i < HARNESS_OPTIONS.length; i++) {
|
|
95
|
+
const opt = HARNESS_OPTIONS[i];
|
|
96
|
+
const available = opt.binary !== '' && (binaryExists(opt.binary) || (opt.harness === 'claude' && findClaudeBinary() !== undefined));
|
|
97
|
+
const tag = available ? '\x1b[32m (found)\x1b[0m' : '';
|
|
98
|
+
process.stdout.write(` ${i + 1}) ${opt.label}${tag}\n`);
|
|
99
|
+
}
|
|
100
|
+
process.stdout.write('\n');
|
|
101
|
+
const choiceRaw = await ask('Choice [1-3]', '1');
|
|
102
|
+
const parsed = parseInt(choiceRaw, 10);
|
|
103
|
+
const choiceIdx = Number.isNaN(parsed) ? 0 : Math.max(0, Math.min(HARNESS_OPTIONS.length - 1, parsed - 1));
|
|
104
|
+
const selected = HARNESS_OPTIONS[choiceIdx];
|
|
105
|
+
let binary = selected.binary;
|
|
106
|
+
if (selected.harness === 'generic') {
|
|
107
|
+
binary = await ask('Binary path or command');
|
|
108
|
+
if (binary === '') {
|
|
109
|
+
process.stdout.write(' No binary specified. Exiting.\n');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Auto-detect claude binary in VS Code extensions if not on PATH
|
|
114
|
+
if (selected.harness === 'claude' && !binaryExists(binary)) {
|
|
115
|
+
const found = findClaudeBinary();
|
|
116
|
+
if (found !== undefined) {
|
|
117
|
+
process.stdout.write(` \x1b[32mFound:\x1b[0m ${found}\n`);
|
|
118
|
+
binary = found;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Verify binary exists
|
|
122
|
+
if (binary !== '' && !binaryExists(binary)) {
|
|
123
|
+
process.stdout.write(` \x1b[33mWarning:\x1b[0m "${binary}" not found on PATH. You may need to provide the full path.\n`);
|
|
124
|
+
const fullPath = await ask('Full path to binary (or press Enter to keep as-is)', binary);
|
|
125
|
+
if (fullPath !== '')
|
|
126
|
+
binary = fullPath;
|
|
127
|
+
}
|
|
128
|
+
process.stdout.write('\n');
|
|
129
|
+
// Agent details
|
|
130
|
+
const name = await ask('Agent name', 'My Zyndo Seller');
|
|
131
|
+
const description = await ask('Description', 'AI agent offering services on the Zyndo marketplace');
|
|
132
|
+
// Skills
|
|
133
|
+
process.stdout.write('\n \x1b[1mSkills\x1b[0m (define what your agent can do)\n');
|
|
134
|
+
const skillId = await ask('Skill ID', 'coding.review.v1');
|
|
135
|
+
const skillName = await ask('Skill name', 'Code Review');
|
|
136
|
+
const skillDesc = await ask('Skill description', 'Reviews code for bugs and improvements');
|
|
137
|
+
const category = skillId.split('.')[0] || 'coding';
|
|
138
|
+
process.stdout.write('\n');
|
|
139
|
+
const model = await ask('Model', selected.defaultModel);
|
|
140
|
+
const workingDir = await ask('Working directory', '.');
|
|
141
|
+
process.stdout.write('\n \x1b[1mZyndo Marketplace\x1b[0m\n');
|
|
142
|
+
const bridgeApiKey = await ask('Bridge API key (leave empty if not required)', '');
|
|
143
|
+
// Build config object and serialize with yaml library
|
|
144
|
+
// Save to .zyndo/ in cwd (per-folder seller identity)
|
|
145
|
+
const configDir = resolve(process.cwd(), '.zyndo');
|
|
146
|
+
const configPath = resolve(configDir, 'seller.yaml');
|
|
147
|
+
const configObj = {
|
|
148
|
+
name,
|
|
149
|
+
description,
|
|
150
|
+
provider: 'claude-code',
|
|
151
|
+
harness: selected.harness,
|
|
152
|
+
...(binary !== selected.binary ? { claude_code_binary: binary } : {}),
|
|
153
|
+
model,
|
|
154
|
+
...(bridgeApiKey !== '' ? { api_key: bridgeApiKey } : {}),
|
|
155
|
+
working_directory: workingDir,
|
|
156
|
+
max_concurrent_tasks: 1,
|
|
157
|
+
skills: [{ id: skillId, name: skillName, description: skillDesc }],
|
|
158
|
+
categories: [category]
|
|
159
|
+
};
|
|
160
|
+
const yaml = yamlStringify(configObj);
|
|
161
|
+
mkdirSync(configDir, { recursive: true });
|
|
162
|
+
if (existsSync(configPath)) {
|
|
163
|
+
const overwrite = await ask(`${configPath} already exists. Overwrite? [y/N]`, 'N');
|
|
164
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
165
|
+
process.stdout.write(' Aborted. Config not changed.\n');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
writeFileSync(configPath, yaml, 'utf-8');
|
|
170
|
+
process.stdout.write('\n');
|
|
171
|
+
process.stdout.write(` \x1b[32mConfig saved to ${configPath}\x1b[0m\n`);
|
|
172
|
+
process.stdout.write(` This seller lives in this folder. Add a CLAUDE.md for custom skills/rules.\n`);
|
|
173
|
+
process.stdout.write(` Run: \x1b[1mzyndo-agent serve\x1b[0m (from this directory)\n\n`);
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AgentSession } from '../connection.js';
|
|
2
|
+
export type McpToolDefinition = Readonly<{
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: Record<string, unknown>;
|
|
6
|
+
}>;
|
|
7
|
+
export type McpSessionState = {
|
|
8
|
+
agentSession: AgentSession | undefined;
|
|
9
|
+
lastConnectName: string;
|
|
10
|
+
lastEventId: number;
|
|
11
|
+
bridgeUrl: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
};
|
|
14
|
+
export declare const TOOLS: ReadonlyArray<McpToolDefinition>;
|
|
15
|
+
export declare const TERMINAL_STATES: Set<string>;
|
|
16
|
+
export declare const WAIT_POLL_INTERVAL_MS = 10000;
|
|
17
|
+
export declare const MAX_WAIT_SECONDS = 600;
|
|
18
|
+
export declare function eventToState(eventType: string): string;
|
|
19
|
+
export declare function handleToolCall(state: McpSessionState, name: string, args: Record<string, unknown>): Promise<string>;
|
|
20
|
+
export declare function mcpSuccess(id: string | number, result: unknown): string;
|
|
21
|
+
export declare function mcpError(id: string | number | null, code: number, message: string): string;
|
|
22
|
+
export declare function handleMcpMethod(state: McpSessionState, method: string, id: string | number, params?: Record<string, unknown>): Promise<string>;
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// MCP Core — shared tool definitions and handlers
|
|
3
|
+
//
|
|
4
|
+
// Used by both the stdio transport (mcpServer.ts) and the broker-side
|
|
5
|
+
// Streamable HTTP transport (mcpHttpServer.ts). All handlers are pure
|
|
6
|
+
// functions that take a McpSessionState instead of reading module-level vars.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
import { connect, reconnect, pollEvents } from '../connection.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Tool definitions
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export const TOOLS = [
|
|
13
|
+
{
|
|
14
|
+
name: 'zyndo_connect',
|
|
15
|
+
description: 'Connect to the Zyndo marketplace as a buyer agent. Must be called before any other Zyndo tool. Returns a reconnectToken — save it to resume your session if it expires.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
name: { type: 'string', description: 'Display name for this buyer (e.g., "My Claude Code")' },
|
|
20
|
+
reconnectToken: { type: 'string', description: 'Token from a previous session to reconnect and recover your tasks. If provided, reconnects instead of creating a new session.' }
|
|
21
|
+
},
|
|
22
|
+
required: ['name']
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'zyndo_browse_sellers',
|
|
27
|
+
description: 'Browse available seller agents on the Zyndo marketplace. Returns a list of sellers with their skills and descriptions.',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
category: { type: 'string', description: 'Optional category filter (e.g., "code-review", "data-analysis")' }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'zyndo_hire',
|
|
37
|
+
description: 'Hire a seller agent to perform a task. Provide the sellerAgentId and skillId from browse results, plus context describing what you need done.',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
sellerAgentId: { type: 'string', description: 'The agentId of the seller to hire' },
|
|
42
|
+
skillId: { type: 'string', description: 'The specific skill to request (from the seller\'s skills list)' },
|
|
43
|
+
context: { type: 'string', description: 'Detailed description of what you need done' }
|
|
44
|
+
},
|
|
45
|
+
required: ['sellerAgentId', 'skillId', 'context']
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'zyndo_check_tasks',
|
|
50
|
+
description: 'Check for new events across all your active tasks. Returns task state updates since your last check.',
|
|
51
|
+
inputSchema: { type: 'object', properties: {} }
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'zyndo_get_messages',
|
|
55
|
+
description: 'Get the message thread for a specific task. Use when a seller has a question or you need to review the conversation.',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
taskId: { type: 'string', description: 'The task ID to get messages for' }
|
|
60
|
+
},
|
|
61
|
+
required: ['taskId']
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'zyndo_send_response',
|
|
66
|
+
description: 'Send a response to a seller\'s question on a task. ONLY use this when the seller asked a question (inputType: "question"). Do NOT use after a delivery — use zyndo_request_revision instead.',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
taskId: { type: 'string', description: 'The task ID to respond to' },
|
|
71
|
+
message: { type: 'string', description: 'Your response message' }
|
|
72
|
+
},
|
|
73
|
+
required: ['taskId', 'message']
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'zyndo_get_delivery',
|
|
78
|
+
description: 'Get the delivered output from a completed or delivered task.',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
taskId: { type: 'string', description: 'The task ID to get delivery for' }
|
|
83
|
+
},
|
|
84
|
+
required: ['taskId']
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'zyndo_wait_for_completion',
|
|
89
|
+
description: 'Check whether a task has reached an actionable state (delivered, completed, failed, or seller question). Returns quickly (within ~25 seconds). Call this repeatedly in a loop until you get a terminal or actionable state.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
taskId: { type: 'string', description: 'The task ID to wait for' }
|
|
94
|
+
},
|
|
95
|
+
required: ['taskId']
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'zyndo_complete_task',
|
|
100
|
+
description: 'Accept or reject a delivered task. Use "complete" to accept the delivery, or "reject" to decline it.',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
taskId: { type: 'string', description: 'The task ID' },
|
|
105
|
+
action: { type: 'string', enum: ['complete', 'reject'], description: '"complete" to accept, "reject" to decline' },
|
|
106
|
+
reason: { type: 'string', description: 'Reason for rejection (required if action is "reject")' }
|
|
107
|
+
},
|
|
108
|
+
required: ['taskId', 'action']
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'zyndo_request_revision',
|
|
113
|
+
description: 'Request a revision on a delivered task. The seller will rework based on your feedback. Max 3 revisions per task.',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: {
|
|
117
|
+
taskId: { type: 'string', description: 'The task ID to request revision for' },
|
|
118
|
+
feedback: { type: 'string', description: 'Feedback explaining what needs to change' }
|
|
119
|
+
},
|
|
120
|
+
required: ['taskId', 'feedback']
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
];
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Constants
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
export const TERMINAL_STATES = new Set(['completed', 'failed', 'canceled', 'rejected']);
|
|
128
|
+
export const WAIT_POLL_INTERVAL_MS = 10_000;
|
|
129
|
+
export const MAX_WAIT_SECONDS = 600;
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Auto-reconnect helpers
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
async function tryReconnect(state) {
|
|
134
|
+
if (state.agentSession === undefined)
|
|
135
|
+
return false;
|
|
136
|
+
try {
|
|
137
|
+
state.agentSession = await reconnect(state.agentSession, {
|
|
138
|
+
role: 'buyer', name: state.lastConnectName, description: 'Claude Code buyer via MCP'
|
|
139
|
+
});
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
try {
|
|
144
|
+
state.agentSession = await connect(state.bridgeUrl, state.apiKey, {
|
|
145
|
+
role: 'buyer', name: state.lastConnectName, description: 'Claude Code buyer via MCP'
|
|
146
|
+
});
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function fetchWithReconnect(state, urlFn, optsFn) {
|
|
155
|
+
if (state.agentSession === undefined)
|
|
156
|
+
throw new Error('Not connected. Call zyndo_connect first.');
|
|
157
|
+
let res = await fetch(urlFn(state.agentSession), optsFn(state.agentSession));
|
|
158
|
+
if (res.status === 401) {
|
|
159
|
+
const reconnected = await tryReconnect(state);
|
|
160
|
+
if (reconnected && state.agentSession !== undefined) {
|
|
161
|
+
res = await fetch(urlFn(state.agentSession), optsFn(state.agentSession));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return res;
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Event state mapping
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
export function eventToState(eventType) {
|
|
170
|
+
if (eventType === 'task.assigned')
|
|
171
|
+
return 'submitted';
|
|
172
|
+
if (eventType === 'task.accepted')
|
|
173
|
+
return 'working';
|
|
174
|
+
if (eventType === 'task.delivered')
|
|
175
|
+
return 'delivered (review needed)';
|
|
176
|
+
if (eventType === 'task.completed')
|
|
177
|
+
return 'completed';
|
|
178
|
+
if (eventType === 'task.rejected')
|
|
179
|
+
return 'rejected';
|
|
180
|
+
if (eventType === 'task.canceled')
|
|
181
|
+
return 'canceled';
|
|
182
|
+
if (eventType === 'task.message.received')
|
|
183
|
+
return 'seller question (check messages)';
|
|
184
|
+
if (eventType === 'task.revision.requested')
|
|
185
|
+
return 'working (revision requested)';
|
|
186
|
+
return eventType;
|
|
187
|
+
}
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Tool handlers (pure functions, take state as first param)
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
async function handleConnect(state, args) {
|
|
192
|
+
const name = args.name ?? 'Claude Code Buyer';
|
|
193
|
+
const reconnectToken = args.reconnectToken;
|
|
194
|
+
state.lastConnectName = name;
|
|
195
|
+
if (reconnectToken !== undefined && reconnectToken !== '') {
|
|
196
|
+
try {
|
|
197
|
+
const tempSession = {
|
|
198
|
+
agentId: '', token: '', reconnectToken, bridgeUrl: state.bridgeUrl
|
|
199
|
+
};
|
|
200
|
+
state.agentSession = await reconnect(tempSession, {
|
|
201
|
+
role: 'buyer', name, description: 'Claude Code buyer via MCP'
|
|
202
|
+
});
|
|
203
|
+
state.lastEventId = 0;
|
|
204
|
+
return JSON.stringify({
|
|
205
|
+
status: 'reconnected',
|
|
206
|
+
agentId: state.agentSession.agentId,
|
|
207
|
+
reconnectToken: state.agentSession.reconnectToken,
|
|
208
|
+
message: 'Session restored. Your previous tasks are accessible again.'
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Reconnect token expired — fall through to fresh connect
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
state.agentSession = await connect(state.bridgeUrl, state.apiKey, {
|
|
216
|
+
role: 'buyer', name, description: 'Claude Code buyer via MCP'
|
|
217
|
+
});
|
|
218
|
+
state.lastEventId = 0;
|
|
219
|
+
return JSON.stringify({
|
|
220
|
+
status: 'connected',
|
|
221
|
+
agentId: state.agentSession.agentId,
|
|
222
|
+
reconnectToken: state.agentSession.reconnectToken,
|
|
223
|
+
message: 'Save the reconnectToken. If your session expires, pass it to zyndo_connect to recover your tasks.'
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async function handleBrowseSellers(state, args) {
|
|
227
|
+
if (state.agentSession === undefined)
|
|
228
|
+
return JSON.stringify({ error: 'Not connected. Call zyndo_connect first.' });
|
|
229
|
+
const category = args.category;
|
|
230
|
+
const params = new URLSearchParams();
|
|
231
|
+
if (category !== undefined)
|
|
232
|
+
params.set('category', category);
|
|
233
|
+
const qs = params.toString();
|
|
234
|
+
const res = await fetchWithReconnect(state, (s) => `${state.bridgeUrl}/agent/sellers${qs.length > 0 ? `?${qs}` : ''}`, (s) => ({ headers: { authorization: `Bearer ${s.token}` } }));
|
|
235
|
+
if (!res.ok)
|
|
236
|
+
return JSON.stringify({ error: `Browse sellers failed (${res.status})` });
|
|
237
|
+
const data = (await res.json());
|
|
238
|
+
return JSON.stringify({
|
|
239
|
+
sellers: data.sellers.map((a) => ({
|
|
240
|
+
agentId: a.agentId, name: a.name, description: a.description,
|
|
241
|
+
skills: a.skills, categories: a.categories, status: a.status,
|
|
242
|
+
activeTaskCount: a.activeTaskCount, maxConcurrentTasks: a.maxConcurrentTasks
|
|
243
|
+
}))
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
async function handleHire(state, args) {
|
|
247
|
+
if (state.agentSession === undefined)
|
|
248
|
+
return JSON.stringify({ error: 'Not connected. Call zyndo_connect first.' });
|
|
249
|
+
const res = await fetchWithReconnect(state, (s) => `${state.bridgeUrl}/agent/hire`, (s) => ({
|
|
250
|
+
method: 'POST',
|
|
251
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${s.token}` },
|
|
252
|
+
body: JSON.stringify({
|
|
253
|
+
sellerAgentId: args.sellerAgentId, skillId: args.skillId, context: args.context
|
|
254
|
+
})
|
|
255
|
+
}));
|
|
256
|
+
if (!res.ok) {
|
|
257
|
+
const text = await res.text();
|
|
258
|
+
return JSON.stringify({ error: `Hire failed (${res.status}): ${text}` });
|
|
259
|
+
}
|
|
260
|
+
const data = (await res.json());
|
|
261
|
+
return JSON.stringify({ taskId: data.taskId, status: 'submitted' });
|
|
262
|
+
}
|
|
263
|
+
async function handleCheckTasks(state) {
|
|
264
|
+
if (state.agentSession === undefined)
|
|
265
|
+
return JSON.stringify({ error: 'Not connected. Call zyndo_connect first.' });
|
|
266
|
+
const events = await pollEvents(state.agentSession, state.lastEventId);
|
|
267
|
+
const taskEvents = {};
|
|
268
|
+
for (const event of events) {
|
|
269
|
+
if (event.eventId > state.lastEventId)
|
|
270
|
+
state.lastEventId = event.eventId;
|
|
271
|
+
const taskId = event.payload.taskId;
|
|
272
|
+
if (taskId === undefined)
|
|
273
|
+
continue;
|
|
274
|
+
taskEvents[taskId] = { state: eventToState(event.type), lastEvent: event.type };
|
|
275
|
+
}
|
|
276
|
+
return JSON.stringify({ tasks: taskEvents, newEvents: events.length });
|
|
277
|
+
}
|
|
278
|
+
async function handleGetMessages(state, args) {
|
|
279
|
+
if (state.agentSession === undefined)
|
|
280
|
+
return JSON.stringify({ error: 'Not connected. Call zyndo_connect first.' });
|
|
281
|
+
const taskId = args.taskId;
|
|
282
|
+
const res = await fetchWithReconnect(state, (s) => `${state.bridgeUrl}/agent/tasks/${taskId}/messages`, (s) => ({ headers: { authorization: `Bearer ${s.token}` } }));
|
|
283
|
+
if (!res.ok)
|
|
284
|
+
return JSON.stringify({ error: `Failed to get messages (${res.status})` });
|
|
285
|
+
const data = (await res.json());
|
|
286
|
+
return JSON.stringify({ messages: data.messages });
|
|
287
|
+
}
|
|
288
|
+
async function handleSendResponse(state, args) {
|
|
289
|
+
if (state.agentSession === undefined)
|
|
290
|
+
return JSON.stringify({ error: 'Not connected. Call zyndo_connect first.' });
|
|
291
|
+
const taskId = args.taskId;
|
|
292
|
+
const message = args.message;
|
|
293
|
+
const res = await fetchWithReconnect(state, (s) => `${state.bridgeUrl}/agent/tasks/${taskId}/messages`, (s) => ({
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${s.token}` },
|
|
296
|
+
body: JSON.stringify({ type: 'answer', content: message })
|
|
297
|
+
}));
|
|
298
|
+
if (!res.ok)
|
|
299
|
+
return JSON.stringify({ error: `Send failed (${res.status}): ${await res.text()}` });
|
|
300
|
+
return JSON.stringify({ status: 'sent' });
|
|
301
|
+
}
|
|
302
|
+
async function handleGetDelivery(state, args) {
|
|
303
|
+
if (state.agentSession === undefined)
|
|
304
|
+
return JSON.stringify({ error: 'Not connected. Call zyndo_connect first.' });
|
|
305
|
+
const taskId = args.taskId;
|
|
306
|
+
const res = await fetchWithReconnect(state, (s) => `${state.bridgeUrl}/agent/tasks/${taskId}`, (s) => ({ headers: { authorization: `Bearer ${s.token}` } }));
|
|
307
|
+
if (res.status === 403) {
|
|
308
|
+
return JSON.stringify({ error: 'Access denied (403). This task belongs to a different session. Reconnect with your original reconnectToken using zyndo_connect to recover it.' });
|
|
309
|
+
}
|
|
310
|
+
if (!res.ok)
|
|
311
|
+
return JSON.stringify({ error: `Failed to get task (${res.status})` });
|
|
312
|
+
const data = (await res.json());
|
|
313
|
+
if (data.inputType === 'question') {
|
|
314
|
+
return JSON.stringify({ status: 'question', message: 'Seller is asking a question. Use zyndo_get_messages to read it and zyndo_send_response to answer.' });
|
|
315
|
+
}
|
|
316
|
+
if (data.output === undefined) {
|
|
317
|
+
return JSON.stringify({ status: data.state, message: 'No delivery yet.' });
|
|
318
|
+
}
|
|
319
|
+
return JSON.stringify({
|
|
320
|
+
status: data.state,
|
|
321
|
+
delivery: data.output.content,
|
|
322
|
+
nextSteps: 'Review the delivery. If acceptable: zyndo_complete_task with action "complete". If changes needed: zyndo_request_revision with feedback. Do NOT use zyndo_send_response after a delivery.'
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
async function handleCompleteTask(state, args) {
|
|
326
|
+
if (state.agentSession === undefined)
|
|
327
|
+
return JSON.stringify({ error: 'Not connected. Call zyndo_connect first.' });
|
|
328
|
+
const taskId = args.taskId;
|
|
329
|
+
const action = args.action;
|
|
330
|
+
const reason = args.reason;
|
|
331
|
+
const res = await fetchWithReconnect(state, (s) => `${state.bridgeUrl}/agent/tasks/${taskId}/${action}`, (s) => ({
|
|
332
|
+
method: 'POST',
|
|
333
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${s.token}` },
|
|
334
|
+
body: JSON.stringify(action === 'reject' && reason !== undefined ? { reason } : {})
|
|
335
|
+
}));
|
|
336
|
+
if (!res.ok)
|
|
337
|
+
return JSON.stringify({ error: `${action} failed (${res.status}): ${await res.text()}` });
|
|
338
|
+
const data = (await res.json());
|
|
339
|
+
return JSON.stringify(data);
|
|
340
|
+
}
|
|
341
|
+
async function handleRequestRevision(state, args) {
|
|
342
|
+
if (state.agentSession === undefined)
|
|
343
|
+
return JSON.stringify({ error: 'Not connected. Call zyndo_connect first.' });
|
|
344
|
+
const taskId = args.taskId;
|
|
345
|
+
const feedback = args.feedback;
|
|
346
|
+
const res = await fetchWithReconnect(state, (s) => `${state.bridgeUrl}/agent/tasks/${taskId}/revise`, (s) => ({
|
|
347
|
+
method: 'POST',
|
|
348
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${s.token}` },
|
|
349
|
+
body: JSON.stringify({ feedback })
|
|
350
|
+
}));
|
|
351
|
+
if (!res.ok)
|
|
352
|
+
return JSON.stringify({ error: `Revision request failed (${res.status}): ${await res.text()}` });
|
|
353
|
+
const data = (await res.json());
|
|
354
|
+
return JSON.stringify({
|
|
355
|
+
...data,
|
|
356
|
+
message: `Revision #${data.revisionNumber} requested. Use zyndo_wait_for_completion to wait for the updated delivery.`
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const WAIT_SHORT_WINDOW_MS = 25_000; // Max wall time per tool call — safely under MCP client timeouts
|
|
360
|
+
async function handleWaitForCompletion(state, args) {
|
|
361
|
+
if (state.agentSession === undefined)
|
|
362
|
+
return JSON.stringify({ error: 'Not connected. Call zyndo_connect first.' });
|
|
363
|
+
const taskId = args.taskId;
|
|
364
|
+
const windowDeadline = Date.now() + WAIT_SHORT_WINDOW_MS;
|
|
365
|
+
while (Date.now() < windowDeadline) {
|
|
366
|
+
const res = await fetchWithReconnect(state, (s) => `${state.bridgeUrl}/agent/tasks/${taskId}`, (s) => ({ headers: { authorization: `Bearer ${s.token}` } }));
|
|
367
|
+
if (res.status === 403) {
|
|
368
|
+
return JSON.stringify({
|
|
369
|
+
error: 'Access denied (403). This task belongs to a different session. Use zyndo_connect with your original reconnectToken to recover it.',
|
|
370
|
+
taskId
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (!res.ok) {
|
|
374
|
+
await new Promise((resolve) => setTimeout(resolve, WAIT_POLL_INTERVAL_MS));
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const detail = (await res.json());
|
|
378
|
+
if (TERMINAL_STATES.has(detail.state)) {
|
|
379
|
+
return JSON.stringify({ taskId, state: detail.state, message: `Task reached terminal state: ${detail.state}` });
|
|
380
|
+
}
|
|
381
|
+
if (detail.state === 'input-required') {
|
|
382
|
+
const hint = detail.inputType === 'delivery'
|
|
383
|
+
? 'Seller has delivered. Use zyndo_get_delivery to retrieve the output, then zyndo_complete_task or zyndo_request_revision.'
|
|
384
|
+
: 'Seller has a question. Use zyndo_get_messages to read it and zyndo_send_response to answer.';
|
|
385
|
+
return JSON.stringify({ taskId, state: detail.state, inputType: detail.inputType, message: hint });
|
|
386
|
+
}
|
|
387
|
+
await new Promise((resolve) => setTimeout(resolve, WAIT_POLL_INTERVAL_MS));
|
|
388
|
+
}
|
|
389
|
+
return JSON.stringify({ taskId, state: 'still-working', message: 'Task is still in progress. Call zyndo_wait_for_completion again to keep waiting.' });
|
|
390
|
+
}
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// Tool dispatch
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
export async function handleToolCall(state, name, args) {
|
|
395
|
+
switch (name) {
|
|
396
|
+
case 'zyndo_connect': return handleConnect(state, args);
|
|
397
|
+
case 'zyndo_browse_sellers': return handleBrowseSellers(state, args);
|
|
398
|
+
case 'zyndo_hire': return handleHire(state, args);
|
|
399
|
+
case 'zyndo_check_tasks': return handleCheckTasks(state);
|
|
400
|
+
case 'zyndo_get_messages': return handleGetMessages(state, args);
|
|
401
|
+
case 'zyndo_send_response': return handleSendResponse(state, args);
|
|
402
|
+
case 'zyndo_get_delivery': return handleGetDelivery(state, args);
|
|
403
|
+
case 'zyndo_wait_for_completion': return handleWaitForCompletion(state, args);
|
|
404
|
+
case 'zyndo_complete_task': return handleCompleteTask(state, args);
|
|
405
|
+
case 'zyndo_request_revision': return handleRequestRevision(state, args);
|
|
406
|
+
default: return JSON.stringify({ error: `Unknown tool: ${name}` });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
// JSON-RPC helpers
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
export function mcpSuccess(id, result) {
|
|
413
|
+
return JSON.stringify({ jsonrpc: '2.0', id, result });
|
|
414
|
+
}
|
|
415
|
+
export function mcpError(id, code, message) {
|
|
416
|
+
return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
|
|
417
|
+
}
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// MCP protocol handler (shared by stdio and HTTP transports)
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
export async function handleMcpMethod(state, method, id, params) {
|
|
422
|
+
switch (method) {
|
|
423
|
+
case 'initialize':
|
|
424
|
+
return mcpSuccess(id, {
|
|
425
|
+
protocolVersion: '2024-11-05',
|
|
426
|
+
capabilities: { tools: {} },
|
|
427
|
+
serverInfo: { name: 'zyndo-buyer', version: '0.3.0' }
|
|
428
|
+
});
|
|
429
|
+
case 'notifications/initialized':
|
|
430
|
+
return '';
|
|
431
|
+
case 'tools/list':
|
|
432
|
+
return mcpSuccess(id, { tools: TOOLS });
|
|
433
|
+
case 'tools/call': {
|
|
434
|
+
const name = (params?.name ?? '');
|
|
435
|
+
const args = (params?.arguments ?? {});
|
|
436
|
+
try {
|
|
437
|
+
const result = await handleToolCall(state, name, args);
|
|
438
|
+
return mcpSuccess(id, { content: [{ type: 'text', text: result }] });
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
442
|
+
return mcpSuccess(id, { content: [{ type: 'text', text: JSON.stringify({ error: msg }) }], isError: true });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
default:
|
|
446
|
+
return mcpError(id, -32601, `Method not found: ${method}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startMcpServer(): Promise<void>;
|