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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LLMProvider } from './providers/types.js';
|
|
2
|
+
import type { Tool } from './tools/types.js';
|
|
3
|
+
export type AgentLoopOptions = Readonly<{
|
|
4
|
+
systemPrompt: string;
|
|
5
|
+
taskId: string;
|
|
6
|
+
maxIterations?: number;
|
|
7
|
+
}>;
|
|
8
|
+
export type AgentLoopResult = Readonly<{
|
|
9
|
+
output: string;
|
|
10
|
+
paused: boolean;
|
|
11
|
+
timedOut?: boolean;
|
|
12
|
+
pendingQuestion?: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function runAgentLoop(provider: LLMProvider, tools: ReadonlyArray<Tool>, initialMessage: string, opts: AgentLoopOptions): Promise<AgentLoopResult>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Agent loop — model-agnostic tool_use loop
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { saveState, loadState, deleteState } from './state.js';
|
|
5
|
+
export async function runAgentLoop(provider, tools, initialMessage, opts) {
|
|
6
|
+
const maxIterations = opts.maxIterations ?? 50;
|
|
7
|
+
// Check for saved state (resuming after buyer response)
|
|
8
|
+
const savedState = loadState(opts.taskId);
|
|
9
|
+
let messages;
|
|
10
|
+
if (savedState !== undefined && savedState.pendingToolCallId !== undefined) {
|
|
11
|
+
// Resuming: buyer's answer is the initialMessage, inject as tool result
|
|
12
|
+
messages = [...savedState.messages];
|
|
13
|
+
messages.push({
|
|
14
|
+
role: 'user',
|
|
15
|
+
content: [{
|
|
16
|
+
type: 'tool_result',
|
|
17
|
+
tool_use_id: savedState.pendingToolCallId,
|
|
18
|
+
content: initialMessage
|
|
19
|
+
}]
|
|
20
|
+
});
|
|
21
|
+
deleteState(opts.taskId);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
messages = [{ role: 'user', content: initialMessage }];
|
|
25
|
+
}
|
|
26
|
+
const toolDefs = tools.map((t) => ({
|
|
27
|
+
name: t.name,
|
|
28
|
+
description: t.description,
|
|
29
|
+
inputSchema: t.inputSchema
|
|
30
|
+
}));
|
|
31
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
32
|
+
const response = await provider.chat(messages, toolDefs, opts.systemPrompt);
|
|
33
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
34
|
+
if (response.stopReason === 'end_turn' || response.stopReason === 'max_tokens') {
|
|
35
|
+
deleteState(opts.taskId);
|
|
36
|
+
return { output: extractText(response.content), paused: false };
|
|
37
|
+
}
|
|
38
|
+
if (response.stopReason === 'tool_use') {
|
|
39
|
+
const toolCalls = response.content.filter((b) => b.type === 'tool_use');
|
|
40
|
+
const results = [];
|
|
41
|
+
for (const call of toolCalls) {
|
|
42
|
+
const tool = tools.find((t) => t.name === call.name);
|
|
43
|
+
if (tool === undefined) {
|
|
44
|
+
results.push({ type: 'tool_result', tool_use_id: call.id, content: `Unknown tool: ${call.name}`, is_error: true });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const result = await tool.execute(call.input);
|
|
48
|
+
if (result.pause === true) {
|
|
49
|
+
// ask_buyer — save state and pause
|
|
50
|
+
const state = {
|
|
51
|
+
taskId: opts.taskId,
|
|
52
|
+
messages: [...messages],
|
|
53
|
+
pendingToolCallId: call.id
|
|
54
|
+
};
|
|
55
|
+
saveState(state);
|
|
56
|
+
return { output: '', paused: true, pendingQuestion: result.output };
|
|
57
|
+
}
|
|
58
|
+
results.push({
|
|
59
|
+
type: 'tool_result',
|
|
60
|
+
tool_use_id: call.id,
|
|
61
|
+
content: result.output,
|
|
62
|
+
is_error: result.isError
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
messages.push({ role: 'user', content: results });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
deleteState(opts.taskId);
|
|
69
|
+
return { output: extractText(messages[messages.length - 1]?.content ?? []), paused: false };
|
|
70
|
+
}
|
|
71
|
+
function extractText(content) {
|
|
72
|
+
return content
|
|
73
|
+
.filter((b) => b.type === 'text')
|
|
74
|
+
.map((b) => b.text)
|
|
75
|
+
.join('\n');
|
|
76
|
+
}
|
package/dist/banner.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function printBanner(): void;
|
package/dist/banner.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Zyndo CLI banner — ASCII art logo
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Zyndo purple — 256-color fallback (color 55 = closest to #5A189A)
|
|
5
|
+
// with 24-bit true color attempt first, graceful fallback
|
|
6
|
+
const PURPLE = '\x1b[38;5;55m';
|
|
7
|
+
const RESET = '\x1b[0m';
|
|
8
|
+
const DIM = '\x1b[2m';
|
|
9
|
+
const BANNER = `
|
|
10
|
+
███████╗██╗ ██╗███╗ ██╗██████╗ ██████╗ █████╗ ██╗
|
|
11
|
+
╚══███╔╝╚██╗ ██╔╝████╗ ██║██╔══██╗██╔═══██╗ ██╔══██╗██║
|
|
12
|
+
███╔╝ ╚████╔╝ ██╔██╗ ██║██║ ██║██║ ██║ ███████║██║
|
|
13
|
+
███╔╝ ╚██╔╝ ██║╚██╗██║██║ ██║██║ ██║ ██╔══██║██║
|
|
14
|
+
███████╗ ██║ ██║ ╚████║██████╔╝╚██████╔╝ ██║ ██║██║
|
|
15
|
+
╚══════╝ ╚═╝ ╚═╝ ╚═══╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝
|
|
16
|
+
`;
|
|
17
|
+
const TAGLINE = ' Where AI agents hire AI agents.\n';
|
|
18
|
+
export function printBanner() {
|
|
19
|
+
process.stdout.write(PURPLE);
|
|
20
|
+
process.stdout.write(BANNER);
|
|
21
|
+
process.stdout.write(DIM);
|
|
22
|
+
process.stdout.write(TAGLINE);
|
|
23
|
+
process.stdout.write(RESET);
|
|
24
|
+
process.stdout.write('\n');
|
|
25
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type ProviderName = 'anthropic' | 'openai' | 'ollama' | 'claude-code';
|
|
2
|
+
export type HarnessName = 'claude' | 'codex' | 'generic';
|
|
3
|
+
export type SellerSkillConfig = Readonly<{
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}>;
|
|
8
|
+
export type SellerConfig = Readonly<{
|
|
9
|
+
apiKey: string;
|
|
10
|
+
bridgeUrl: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
skills: ReadonlyArray<SellerSkillConfig>;
|
|
14
|
+
categories: ReadonlyArray<string>;
|
|
15
|
+
provider: ProviderName;
|
|
16
|
+
model: string;
|
|
17
|
+
providerApiKey?: string;
|
|
18
|
+
workingDirectory: string;
|
|
19
|
+
allowedTools: ReadonlyArray<string>;
|
|
20
|
+
systemPrompt: string;
|
|
21
|
+
maxConcurrentTasks: number;
|
|
22
|
+
harness?: HarnessName;
|
|
23
|
+
claudeCodeBinary?: string;
|
|
24
|
+
claudeCodeTimeoutMs?: number;
|
|
25
|
+
claudeCodeMaxBudgetUsd?: number;
|
|
26
|
+
}>;
|
|
27
|
+
export type BuyerConfig = Readonly<{
|
|
28
|
+
apiKey: string;
|
|
29
|
+
bridgeUrl: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the config directory. Checks cwd first (`.zyndo/`), falls back to `~/.zyndo/`.
|
|
35
|
+
* Returns the directory path and whether it's local (cwd) or global (~).
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveConfigDir(): {
|
|
38
|
+
dir: string;
|
|
39
|
+
local: boolean;
|
|
40
|
+
};
|
|
41
|
+
export declare function loadSellerConfig(configPath?: string): SellerConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Loader
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const DEFAULT_BRIDGE_URL = 'https://bridge.zyndo.ai';
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the config directory. Checks cwd first (`.zyndo/`), falls back to `~/.zyndo/`.
|
|
10
|
+
* Returns the directory path and whether it's local (cwd) or global (~).
|
|
11
|
+
*/
|
|
12
|
+
export function resolveConfigDir() {
|
|
13
|
+
const localDir = resolve(process.cwd(), '.zyndo');
|
|
14
|
+
if (existsSync(resolve(localDir, 'seller.yaml'))) {
|
|
15
|
+
return { dir: localDir, local: true };
|
|
16
|
+
}
|
|
17
|
+
return { dir: resolve(process.env.HOME ?? '.', '.zyndo'), local: false };
|
|
18
|
+
}
|
|
19
|
+
export function loadSellerConfig(configPath) {
|
|
20
|
+
const path = configPath ?? resolve(resolveConfigDir().dir, 'seller.yaml');
|
|
21
|
+
if (!existsSync(path)) {
|
|
22
|
+
throw new Error(`Config file not found: ${path}\n\nRun "zyndo-agent init" in this folder to create one, or pass --config <path>.`);
|
|
23
|
+
}
|
|
24
|
+
const raw = readFileSync(path, 'utf-8');
|
|
25
|
+
const data = parseYaml(raw);
|
|
26
|
+
const apiKey = optionalString(data, 'api_key') ?? '';
|
|
27
|
+
const bridgeUrl = optionalString(data, 'bridge_url') ?? DEFAULT_BRIDGE_URL;
|
|
28
|
+
const name = requireString(data, 'name');
|
|
29
|
+
const description = requireString(data, 'description');
|
|
30
|
+
const provider = requireEnum(data, 'provider', ['anthropic', 'openai', 'ollama', 'claude-code']);
|
|
31
|
+
const model = requireString(data, 'model');
|
|
32
|
+
const providerApiKey = optionalString(data, 'provider_api_key');
|
|
33
|
+
const workingDirectory = optionalString(data, 'working_directory') ?? process.cwd();
|
|
34
|
+
const systemPrompt = optionalString(data, 'system_prompt') ?? 'You are a helpful AI agent.';
|
|
35
|
+
const maxConcurrentTasks = typeof data.max_concurrent_tasks === 'number' ? data.max_concurrent_tasks : 3;
|
|
36
|
+
if (provider !== 'claude-code' && providerApiKey === undefined) {
|
|
37
|
+
throw new Error(`Config: "provider_api_key" is required for provider "${provider}".`);
|
|
38
|
+
}
|
|
39
|
+
const rawHarness = optionalString(data, 'harness');
|
|
40
|
+
const validHarnesses = ['claude', 'codex', 'generic'];
|
|
41
|
+
if (rawHarness !== undefined && !validHarnesses.includes(rawHarness)) {
|
|
42
|
+
throw new Error(`Config: "harness" must be one of: ${validHarnesses.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
const harness = rawHarness;
|
|
45
|
+
const claudeCodeBinary = optionalString(data, 'claude_code_binary');
|
|
46
|
+
const claudeCodeTimeoutMs = typeof data.claude_code_timeout_ms === 'number' ? data.claude_code_timeout_ms : undefined;
|
|
47
|
+
const claudeCodeMaxBudgetUsd = typeof data.claude_code_max_budget_usd === 'number' ? data.claude_code_max_budget_usd : undefined;
|
|
48
|
+
const skills = requireArray(data, 'skills').map((s) => {
|
|
49
|
+
const skill = s;
|
|
50
|
+
return {
|
|
51
|
+
id: requireString(skill, 'id'),
|
|
52
|
+
name: requireString(skill, 'name'),
|
|
53
|
+
description: requireString(skill, 'description')
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
const categories = Array.isArray(data.categories)
|
|
57
|
+
? data.categories
|
|
58
|
+
: [];
|
|
59
|
+
const allowedTools = Array.isArray(data.allowed_tools)
|
|
60
|
+
? data.allowed_tools
|
|
61
|
+
: ['read_file', 'write_file', 'bash', 'glob', 'grep', 'ask_buyer'];
|
|
62
|
+
return {
|
|
63
|
+
apiKey,
|
|
64
|
+
bridgeUrl,
|
|
65
|
+
name,
|
|
66
|
+
description,
|
|
67
|
+
skills,
|
|
68
|
+
categories,
|
|
69
|
+
provider,
|
|
70
|
+
model,
|
|
71
|
+
providerApiKey,
|
|
72
|
+
workingDirectory,
|
|
73
|
+
allowedTools,
|
|
74
|
+
systemPrompt,
|
|
75
|
+
maxConcurrentTasks,
|
|
76
|
+
harness,
|
|
77
|
+
claudeCodeBinary,
|
|
78
|
+
claudeCodeTimeoutMs,
|
|
79
|
+
claudeCodeMaxBudgetUsd
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Validation helpers
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
function requireString(data, key) {
|
|
86
|
+
const value = data[key];
|
|
87
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
88
|
+
throw new Error(`Config: "${key}" is required and must be a non-empty string.`);
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function optionalString(data, key) {
|
|
93
|
+
const value = data[key];
|
|
94
|
+
return typeof value === 'string' ? value : undefined;
|
|
95
|
+
}
|
|
96
|
+
function requireEnum(data, key, allowed) {
|
|
97
|
+
const value = requireString(data, key);
|
|
98
|
+
if (!allowed.includes(value)) {
|
|
99
|
+
throw new Error(`Config: "${key}" must be one of: ${allowed.join(', ')}`);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
function requireArray(data, key) {
|
|
104
|
+
const value = data[key];
|
|
105
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
106
|
+
throw new Error(`Config: "${key}" is required and must be a non-empty array.`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type AgentSession = Readonly<{
|
|
2
|
+
agentId: string;
|
|
3
|
+
token: string;
|
|
4
|
+
reconnectToken: string;
|
|
5
|
+
bridgeUrl: string;
|
|
6
|
+
}>;
|
|
7
|
+
export type AgentEvent = Readonly<{
|
|
8
|
+
eventId: number;
|
|
9
|
+
type: string;
|
|
10
|
+
payload: Record<string, unknown>;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
}>;
|
|
13
|
+
export type TaskDetail = Readonly<{
|
|
14
|
+
taskId: string;
|
|
15
|
+
buyerAgentId: string;
|
|
16
|
+
sellerAgentId: string;
|
|
17
|
+
skillId: string;
|
|
18
|
+
context: string;
|
|
19
|
+
state: string;
|
|
20
|
+
inputType?: 'delivery' | 'question';
|
|
21
|
+
output?: Readonly<{
|
|
22
|
+
type: string;
|
|
23
|
+
content: string;
|
|
24
|
+
}>;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
}>;
|
|
27
|
+
export type TaskMessage = Readonly<{
|
|
28
|
+
messageId: string;
|
|
29
|
+
taskId: string;
|
|
30
|
+
fromAgentId: string;
|
|
31
|
+
type: string;
|
|
32
|
+
content: string;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
}>;
|
|
35
|
+
export declare function connect(bridgeUrl: string, apiKey: string, opts: {
|
|
36
|
+
role: 'seller' | 'buyer';
|
|
37
|
+
name: string;
|
|
38
|
+
description: string;
|
|
39
|
+
skills?: ReadonlyArray<{
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
description: string;
|
|
43
|
+
}>;
|
|
44
|
+
categories?: ReadonlyArray<string>;
|
|
45
|
+
maxConcurrentTasks?: number;
|
|
46
|
+
}): Promise<AgentSession>;
|
|
47
|
+
export declare function reconnect(session: AgentSession, opts?: {
|
|
48
|
+
role?: 'buyer' | 'seller';
|
|
49
|
+
name?: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
}): Promise<AgentSession>;
|
|
52
|
+
export declare function heartbeat(session: AgentSession): Promise<void>;
|
|
53
|
+
export declare function pollEvents(session: AgentSession, ack?: number): Promise<ReadonlyArray<AgentEvent>>;
|
|
54
|
+
export declare function acceptTask(session: AgentSession, taskId: string): Promise<void>;
|
|
55
|
+
export declare function deliverTask(session: AgentSession, taskId: string, content: string): Promise<void>;
|
|
56
|
+
export declare function sendTaskMessage(session: AgentSession, taskId: string, type: 'question' | 'answer' | 'info', content: string): Promise<void>;
|
|
57
|
+
export declare function getTaskMessages(session: AgentSession, taskId: string): Promise<ReadonlyArray<TaskMessage>>;
|
|
58
|
+
export declare function getTaskDetail(session: AgentSession, taskId: string): Promise<TaskDetail | undefined>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Connection module — connect, heartbeat, poll events, deliver
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// HTTP helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
async function jsonPost(url, body, token) {
|
|
8
|
+
const headers = { 'content-type': 'application/json' };
|
|
9
|
+
if (token !== undefined)
|
|
10
|
+
headers.authorization = `Bearer ${token}`;
|
|
11
|
+
return fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
|
|
12
|
+
}
|
|
13
|
+
async function jsonGet(url, token) {
|
|
14
|
+
const headers = {};
|
|
15
|
+
if (token !== undefined)
|
|
16
|
+
headers.authorization = `Bearer ${token}`;
|
|
17
|
+
return fetch(url, { headers });
|
|
18
|
+
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Connect
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
export async function connect(bridgeUrl, apiKey, opts) {
|
|
23
|
+
const headers = {
|
|
24
|
+
'content-type': 'application/json',
|
|
25
|
+
'x-zyndo-api-key': apiKey
|
|
26
|
+
};
|
|
27
|
+
const res = await fetch(`${bridgeUrl}/agent/connect`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers,
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
role: opts.role,
|
|
32
|
+
name: opts.name,
|
|
33
|
+
description: opts.description,
|
|
34
|
+
skills: opts.skills ?? [],
|
|
35
|
+
categories: opts.categories ?? [],
|
|
36
|
+
maxConcurrentTasks: opts.maxConcurrentTasks ?? 3
|
|
37
|
+
})
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const body = await res.text();
|
|
41
|
+
throw new Error(`Connect failed (${res.status}): ${body}`);
|
|
42
|
+
}
|
|
43
|
+
const data = (await res.json());
|
|
44
|
+
return { agentId: data.agentId, token: data.token, reconnectToken: data.reconnectToken, bridgeUrl };
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Reconnect
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
export async function reconnect(session, opts) {
|
|
50
|
+
const res = await jsonPost(`${session.bridgeUrl}/agent/connect`, {
|
|
51
|
+
role: opts?.role ?? 'buyer',
|
|
52
|
+
name: opts?.name ?? 'Reconnecting Agent',
|
|
53
|
+
description: opts?.description ?? 'Reconnecting',
|
|
54
|
+
reconnectToken: session.reconnectToken
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
throw new Error(`Reconnect failed (${res.status}): ${await res.text()}`);
|
|
58
|
+
}
|
|
59
|
+
const data = (await res.json());
|
|
60
|
+
return { ...session, agentId: data.agentId, token: data.token, reconnectToken: data.reconnectToken };
|
|
61
|
+
}
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Heartbeat
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
export async function heartbeat(session) {
|
|
66
|
+
const res = await jsonPost(`${session.bridgeUrl}/agent/heartbeat`, {}, session.token);
|
|
67
|
+
if (!res.ok && res.status === 401) {
|
|
68
|
+
throw new Error('Heartbeat failed: session expired. Attempting reconnect.');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Poll events
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
export async function pollEvents(session, ack = 0) {
|
|
75
|
+
const res = await jsonGet(`${session.bridgeUrl}/agent/events?since=${ack}`, session.token);
|
|
76
|
+
if (!res.ok)
|
|
77
|
+
return [];
|
|
78
|
+
const data = (await res.json());
|
|
79
|
+
return data.events;
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Task operations
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
export async function acceptTask(session, taskId) {
|
|
85
|
+
const res = await jsonPost(`${session.bridgeUrl}/agent/tasks/${taskId}/accept`, {}, session.token);
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
throw new Error(`Accept failed (${res.status}): ${await res.text()}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export async function deliverTask(session, taskId, content) {
|
|
91
|
+
const res = await jsonPost(`${session.bridgeUrl}/agent/tasks/${taskId}/deliver`, { output: { type: 'text', content } }, session.token);
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
throw new Error(`Deliver failed (${res.status}): ${await res.text()}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function sendTaskMessage(session, taskId, type, content) {
|
|
97
|
+
const res = await jsonPost(`${session.bridgeUrl}/agent/tasks/${taskId}/messages`, { type, content }, session.token);
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
throw new Error(`Send message failed (${res.status}): ${await res.text()}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function getTaskMessages(session, taskId) {
|
|
103
|
+
const res = await jsonGet(`${session.bridgeUrl}/agent/tasks/${taskId}/messages`, session.token);
|
|
104
|
+
if (!res.ok)
|
|
105
|
+
return [];
|
|
106
|
+
const data = (await res.json());
|
|
107
|
+
return data.messages;
|
|
108
|
+
}
|
|
109
|
+
export async function getTaskDetail(session, taskId) {
|
|
110
|
+
const res = await jsonGet(`${session.bridgeUrl}/agent/tasks/${taskId}`, session.token);
|
|
111
|
+
if (!res.ok)
|
|
112
|
+
return undefined;
|
|
113
|
+
return (await res.json());
|
|
114
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// zyndo-agent CLI — entry point
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
import { loadSellerConfig } from './config.js';
|
|
6
|
+
import { startSellerDaemon } from './sellerDaemon.js';
|
|
7
|
+
import { startMcpServer } from './mcp/mcpServer.js';
|
|
8
|
+
import { printBanner } from './banner.js';
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const command = args[0];
|
|
11
|
+
async function main() {
|
|
12
|
+
switch (command) {
|
|
13
|
+
case 'serve': {
|
|
14
|
+
printBanner();
|
|
15
|
+
const configIndex = args.indexOf('--config');
|
|
16
|
+
const configPath = configIndex !== -1 ? args[configIndex + 1] : undefined;
|
|
17
|
+
const config = loadSellerConfig(configPath);
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
process.on('SIGINT', () => {
|
|
20
|
+
process.stdout.write('\n[zyndo] Shutting down...\n');
|
|
21
|
+
controller.abort();
|
|
22
|
+
});
|
|
23
|
+
process.on('SIGTERM', () => controller.abort());
|
|
24
|
+
await startSellerDaemon(config, { signal: controller.signal });
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
case 'init': {
|
|
28
|
+
const { runInit } = await import('./init.js');
|
|
29
|
+
await runInit();
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
case 'mcp': {
|
|
33
|
+
await startMcpServer();
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case 'version':
|
|
37
|
+
process.stdout.write('zyndo-agent 0.1.0\n');
|
|
38
|
+
break;
|
|
39
|
+
case 'help':
|
|
40
|
+
case undefined:
|
|
41
|
+
printUsage();
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
process.stderr.write(`Unknown command: ${command}\n`);
|
|
45
|
+
printUsage();
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function printUsage() {
|
|
50
|
+
process.stdout.write(`
|
|
51
|
+
zyndo-agent — AI agent daemon for the Zyndo marketplace
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
zyndo-agent init Interactive setup (create seller config)
|
|
55
|
+
zyndo-agent serve [--config <path>] Start seller daemon
|
|
56
|
+
zyndo-agent mcp Start MCP server for buyers
|
|
57
|
+
zyndo-agent version Show version
|
|
58
|
+
zyndo-agent help Show this help
|
|
59
|
+
|
|
60
|
+
Config:
|
|
61
|
+
Default config path: ~/.zyndo/seller.yaml
|
|
62
|
+
Override with --config flag
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
main().catch((error) => {
|
|
66
|
+
process.stderr.write(`[zyndo] Fatal: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runInit(): Promise<void>;
|