zyndo 0.1.0 → 0.1.2

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.js CHANGED
@@ -2,6 +2,15 @@
2
2
  // Agent loop — model-agnostic tool_use loop
3
3
  // ---------------------------------------------------------------------------
4
4
  import { saveState, loadState, deleteState } from './state.js';
5
+ import { MAX_AGENT_MESSAGE_CHARS } from './connection.js';
6
+ // Soft margin below the broker's hard cap so the truncation marker fits.
7
+ const QUESTION_SAFE_LIMIT = MAX_AGENT_MESSAGE_CHARS - 200;
8
+ function truncateForBuyer(content) {
9
+ if (content.length <= QUESTION_SAFE_LIMIT)
10
+ return content;
11
+ const omitted = content.length - QUESTION_SAFE_LIMIT;
12
+ return `${content.slice(0, QUESTION_SAFE_LIMIT)}\n\n[truncated: ${omitted} characters omitted to stay within marketplace message limit]`;
13
+ }
5
14
  export async function runAgentLoop(provider, tools, initialMessage, opts) {
6
15
  const maxIterations = opts.maxIterations ?? 50;
7
16
  // Check for saved state (resuming after buyer response)
@@ -53,7 +62,10 @@ export async function runAgentLoop(provider, tools, initialMessage, opts) {
53
62
  pendingToolCallId: call.id
54
63
  };
55
64
  saveState(state);
56
- return { output: '', paused: true, pendingQuestion: result.output };
65
+ // Truncate the question if the agent produced something larger than the
66
+ // marketplace 50k limit. Better to send a clipped question than crash the
67
+ // task. The truncation marker tells the buyer their counterpart was verbose.
68
+ return { output: '', paused: true, pendingQuestion: truncateForBuyer(result.output) };
57
69
  }
58
70
  results.push({
59
71
  type: 'tool_result',
package/dist/banner.js CHANGED
@@ -6,13 +6,13 @@
6
6
  const PURPLE = '\x1b[38;5;55m';
7
7
  const RESET = '\x1b[0m';
8
8
  const DIM = '\x1b[2m';
9
- const BANNER = `
10
- ███████╗██╗ ██╗███╗ ██╗██████╗ ██████╗ █████╗ ██╗
11
- ╚══███╔╝╚██╗ ██╔╝████╗ ██║██╔══██╗██╔═══██╗ ██╔══██╗██║
12
- ███╔╝ ╚████╔╝ ██╔██╗ ██║██║ ██║██║ ██║ ███████║██║
13
- ███╔╝ ╚██╔╝ ██║╚██╗██║██║ ██║██║ ██║ ██╔══██║██║
14
- ███████╗ ██║ ██║ ╚████║██████╔╝╚██████╔╝ ██║ ██║██║
15
- ╚══════╝ ╚═╝ ╚═╝ ╚═══╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝
9
+ const BANNER = `
10
+ ███████╗██╗ ██╗███╗ ██╗██████╗ ██████╗ █████╗ ██╗
11
+ ╚══███╔╝╚██╗ ██╔╝████╗ ██║██╔══██╗██╔═══██╗ ██╔══██╗██║
12
+ ███╔╝ ╚████╔╝ ██╔██╗ ██║██║ ██║██║ ██║ ███████║██║
13
+ ███╔╝ ╚██╔╝ ██║╚██╗██║██║ ██║██║ ██║ ██╔══██║██║
14
+ ███████╗ ██║ ██║ ╚████║██████╔╝╚██████╔╝ ██║ ██║██║
15
+ ╚══════╝ ╚═╝ ╚═╝ ╚═══╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝
16
16
  `;
17
17
  const TAGLINE = ' Where AI agents hire AI agents.\n';
18
18
  export function printBanner() {
@@ -0,0 +1,7 @@
1
+ export declare function buildDashboardUrl(tab: 'wallet' | 'earnings', params?: Readonly<{
2
+ topup?: string;
3
+ }>, baseUrl?: string): string;
4
+ export declare function openBrowser(url: string): void;
5
+ export declare function handleWalletCommand(args: ReadonlyArray<string>, opener?: (url: string) => void): void;
6
+ export declare function handleCashoutCommand(opener?: (url: string) => void): void;
7
+ export declare function handleConnectCommand(args: ReadonlyArray<string>, opener?: (url: string) => void): void;
@@ -0,0 +1,117 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Wallet, Cashout, and Connect CLI commands
3
+ // ---------------------------------------------------------------------------
4
+ // These commands open the Zyndo dashboard in the user's browser.
5
+ // Wallet management is a human activity best done in the browser UI,
6
+ // where the user is already authenticated via Google OAuth.
7
+ // ---------------------------------------------------------------------------
8
+ import { exec } from 'node:child_process';
9
+ import { platform } from 'node:os';
10
+ const DEFAULT_DASHBOARD_BASE = 'https://zyndo.ai';
11
+ // ---------------------------------------------------------------------------
12
+ // URL builder
13
+ // ---------------------------------------------------------------------------
14
+ export function buildDashboardUrl(tab, params, baseUrl) {
15
+ const base = baseUrl ?? process.env.ZYNDO_DASHBOARD_URL ?? DEFAULT_DASHBOARD_BASE;
16
+ const url = new URL(`${base}/dashboard`);
17
+ url.searchParams.set('tab', tab);
18
+ if (params?.topup !== undefined) {
19
+ url.searchParams.set('topup', params.topup);
20
+ }
21
+ return url.toString();
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // Browser opener
25
+ // ---------------------------------------------------------------------------
26
+ export function openBrowser(url) {
27
+ const cmd = platform() === 'darwin' ? 'open' :
28
+ platform() === 'win32' ? 'start' :
29
+ 'xdg-open';
30
+ exec(`${cmd} "${url}"`);
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Command: zyndo wallet [subcommand]
34
+ // ---------------------------------------------------------------------------
35
+ export function handleWalletCommand(args, opener = openBrowser) {
36
+ const sub = args[0];
37
+ switch (sub) {
38
+ case undefined:
39
+ case 'balance': {
40
+ const url = buildDashboardUrl('wallet');
41
+ process.stdout.write(`Opening wallet dashboard in your browser...\n`);
42
+ opener(url);
43
+ break;
44
+ }
45
+ case 'topup': {
46
+ const amount = args[1];
47
+ if (amount === undefined || amount.trim() === '') {
48
+ process.stderr.write('Usage: zyndo wallet topup <amount>\n');
49
+ return;
50
+ }
51
+ const url = buildDashboardUrl('wallet', { topup: amount });
52
+ process.stdout.write(`Opening wallet with top-up of $${amount}...\n`);
53
+ opener(url);
54
+ break;
55
+ }
56
+ case 'ledger': {
57
+ const url = buildDashboardUrl('wallet');
58
+ process.stdout.write(`Opening wallet dashboard (ledger view) in your browser...\n`);
59
+ opener(url);
60
+ break;
61
+ }
62
+ default:
63
+ process.stdout.write(`Unknown wallet subcommand: ${sub}\n\n`);
64
+ printWalletUsage();
65
+ }
66
+ }
67
+ function printWalletUsage() {
68
+ process.stdout.write(`Usage: zyndo wallet <subcommand>
69
+
70
+ Subcommands:
71
+ balance View wallet balance (default)
72
+ topup <amount> Add funds to your wallet
73
+ ledger View transaction history
74
+
75
+ All wallet operations open the Zyndo dashboard in your browser.
76
+ `);
77
+ }
78
+ // ---------------------------------------------------------------------------
79
+ // Command: zyndo cashout
80
+ // ---------------------------------------------------------------------------
81
+ export function handleCashoutCommand(opener = openBrowser) {
82
+ const url = buildDashboardUrl('earnings');
83
+ process.stdout.write(`Opening earnings dashboard in your browser...\n`);
84
+ opener(url);
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Command: zyndo connect [subcommand]
88
+ // ---------------------------------------------------------------------------
89
+ export function handleConnectCommand(args, opener = openBrowser) {
90
+ const sub = args[0];
91
+ switch (sub) {
92
+ case 'onboard': {
93
+ const url = buildDashboardUrl('earnings');
94
+ process.stdout.write(`Opening Stripe Connect onboarding in your browser...\n`);
95
+ opener(url);
96
+ break;
97
+ }
98
+ case 'status': {
99
+ const url = buildDashboardUrl('earnings');
100
+ process.stdout.write(`Opening earnings dashboard to check connect status...\n`);
101
+ opener(url);
102
+ break;
103
+ }
104
+ default:
105
+ printConnectUsage();
106
+ }
107
+ }
108
+ function printConnectUsage() {
109
+ process.stdout.write(`Usage: zyndo connect <subcommand>
110
+
111
+ Subcommands:
112
+ onboard Start Stripe Connect onboarding
113
+ status Check your connect status
114
+
115
+ All connect operations open the Zyndo dashboard in your browser.
116
+ `);
117
+ }
package/dist/config.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  export type ProviderName = 'anthropic' | 'openai' | 'ollama' | 'claude-code';
2
2
  export type HarnessName = 'claude' | 'codex' | 'generic';
3
+ export declare const MIN_SKILL_PRICE_CENTS = 10;
4
+ export declare const MAX_SKILL_PRICE_CENTS = 1000000;
3
5
  export type SellerSkillConfig = Readonly<{
4
6
  id: string;
5
7
  name: string;
6
8
  description: string;
9
+ priceCents: number;
7
10
  }>;
8
11
  export type SellerConfig = Readonly<{
9
12
  apiKey: string;
package/dist/config.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { parse as parseYaml } from 'yaml';
4
+ // Mirror of @zyndo/contracts MIN_SKILL_PRICE_CENTS / MAX_SKILL_PRICE_CENTS.
5
+ // The CLI ships standalone and does not depend on @zyndo/contracts. Keep these
6
+ // in sync if the contract floor or cap ever changes.
7
+ export const MIN_SKILL_PRICE_CENTS = 10;
8
+ export const MAX_SKILL_PRICE_CENTS = 10_000_00;
4
9
  // ---------------------------------------------------------------------------
5
10
  // Loader
6
11
  // ---------------------------------------------------------------------------
@@ -19,7 +24,7 @@ export function resolveConfigDir() {
19
24
  export function loadSellerConfig(configPath) {
20
25
  const path = configPath ?? resolve(resolveConfigDir().dir, 'seller.yaml');
21
26
  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>.`);
27
+ throw new Error(`Config file not found: ${path}\n\nRun "zyndo init" in this folder to create one, or pass --config <path>.`);
23
28
  }
24
29
  const raw = readFileSync(path, 'utf-8');
25
30
  const data = parseYaml(raw);
@@ -47,10 +52,24 @@ export function loadSellerConfig(configPath) {
47
52
  const claudeCodeMaxBudgetUsd = typeof data.claude_code_max_budget_usd === 'number' ? data.claude_code_max_budget_usd : undefined;
48
53
  const skills = requireArray(data, 'skills').map((s) => {
49
54
  const skill = s;
55
+ const id = requireString(skill, 'id');
56
+ // Pricing is mandatory for every skill. Closes a dead path where
57
+ // priceless skills made it onto the marketplace and failed at hire time.
58
+ // Local fast-fail beats waiting for the broker to reject the connect.
59
+ const rawPrice = skill.price_cents;
60
+ if (typeof rawPrice !== 'number' || !Number.isInteger(rawPrice) || rawPrice < MIN_SKILL_PRICE_CENTS) {
61
+ throw new Error(`Skill "${id}" is missing "price_cents" or it is below the $0.10 minimum (${MIN_SKILL_PRICE_CENTS} cents). ` +
62
+ `Every skill must declare a price. Add "price_cents: 500" (for $5.00) under the skill in your seller.yaml.`);
63
+ }
64
+ if (rawPrice > MAX_SKILL_PRICE_CENTS) {
65
+ throw new Error(`Skill "${id}" has "price_cents: ${rawPrice}" which exceeds the $10,000 cap (${MAX_SKILL_PRICE_CENTS} cents). ` +
66
+ `Lower the price or split the work into smaller skills.`);
67
+ }
50
68
  return {
51
- id: requireString(skill, 'id'),
69
+ id,
52
70
  name: requireString(skill, 'name'),
53
- description: requireString(skill, 'description')
71
+ description: requireString(skill, 'description'),
72
+ priceCents: rawPrice
54
73
  };
55
74
  });
56
75
  const categories = Array.isArray(data.categories)
@@ -1,3 +1,4 @@
1
+ export declare const MAX_AGENT_MESSAGE_CHARS = 50000;
1
2
  export type AgentSession = Readonly<{
2
3
  agentId: string;
3
4
  token: string;
@@ -40,6 +41,7 @@ export declare function connect(bridgeUrl: string, apiKey: string, opts: {
40
41
  id: string;
41
42
  name: string;
42
43
  description: string;
44
+ priceCents: number;
43
45
  }>;
44
46
  categories?: ReadonlyArray<string>;
45
47
  maxConcurrentTasks?: number;
@@ -1,6 +1,11 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // Connection module — connect, heartbeat, poll events, deliver
3
3
  // ---------------------------------------------------------------------------
4
+ // Mirror of @zyndo/contracts MAX_AGENT_MESSAGE_CHARS. The CLI ships as a
5
+ // standalone npm package, so we duplicate this small constant rather than
6
+ // pulling in the contracts workspace as a dependency. Keep these values in
7
+ // sync if the broker-side limit ever changes.
8
+ export const MAX_AGENT_MESSAGE_CHARS = 50_000;
4
9
  // ---------------------------------------------------------------------------
5
10
  // HTTP helpers
6
11
  // ---------------------------------------------------------------------------
@@ -94,6 +99,13 @@ export async function deliverTask(session, taskId, content) {
94
99
  }
95
100
  }
96
101
  export async function sendTaskMessage(session, taskId, type, content) {
102
+ // Pre-flight guard: fail fast locally instead of round-tripping to the broker
103
+ // when the message obviously exceeds the marketplace limit. The broker will
104
+ // also reject this — this is just for faster, clearer feedback to the agent.
105
+ if (content.length > MAX_AGENT_MESSAGE_CHARS) {
106
+ throw new Error(`Message content is ${content.length} characters, which exceeds the ${MAX_AGENT_MESSAGE_CHARS}-character marketplace limit. ` +
107
+ `Trim the message before sending. Long context should be summarized or attached as a file delivery, not pasted into a chat message.`);
108
+ }
97
109
  const res = await jsonPost(`${session.bridgeUrl}/agent/tasks/${taskId}/messages`, { type, content }, session.token);
98
110
  if (!res.ok) {
99
111
  throw new Error(`Send message failed (${res.status}): ${await res.text()}`);
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  // ---------------------------------------------------------------------------
3
- // zyndo-agent CLI — entry point
3
+ // zyndo CLI — entry point
4
4
  // ---------------------------------------------------------------------------
5
5
  import { loadSellerConfig } from './config.js';
6
6
  import { startSellerDaemon } from './sellerDaemon.js';
7
7
  import { startMcpServer } from './mcp/mcpServer.js';
8
8
  import { printBanner } from './banner.js';
9
+ import { handleWalletCommand, handleCashoutCommand, handleConnectCommand } from './commands/wallet.js';
9
10
  const args = process.argv.slice(2);
10
11
  const command = args[0];
11
12
  async function main() {
@@ -33,8 +34,20 @@ async function main() {
33
34
  await startMcpServer();
34
35
  break;
35
36
  }
37
+ case 'wallet': {
38
+ handleWalletCommand(args.slice(1));
39
+ break;
40
+ }
41
+ case 'cashout': {
42
+ handleCashoutCommand();
43
+ break;
44
+ }
45
+ case 'connect': {
46
+ handleConnectCommand(args.slice(1));
47
+ break;
48
+ }
36
49
  case 'version':
37
- process.stdout.write('zyndo-agent 0.1.0\n');
50
+ process.stdout.write('zyndo 0.1.0\n');
38
51
  break;
39
52
  case 'help':
40
53
  case undefined:
@@ -47,19 +60,22 @@ async function main() {
47
60
  }
48
61
  }
49
62
  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
+ process.stdout.write(`
64
+ zyndo — AI agent daemon for the Zyndo marketplace
65
+
66
+ Usage:
67
+ zyndo init Interactive setup (create seller config)
68
+ zyndo serve [--config <path>] Start seller daemon
69
+ zyndo mcp Start MCP server for buyers
70
+ zyndo wallet [balance|topup|ledger] Manage wallet (opens dashboard)
71
+ zyndo cashout View earnings and cashout (opens dashboard)
72
+ zyndo connect [onboard|status] Stripe Connect setup (opens dashboard)
73
+ zyndo version Show version
74
+ zyndo help Show this help
75
+
76
+ Config:
77
+ Default config path: ~/.zyndo/seller.yaml
78
+ Override with --config flag
63
79
  `);
64
80
  }
65
81
  main().catch((error) => {
package/dist/init.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // ---------------------------------------------------------------------------
2
- // zyndo-agent init — interactive seller config setup
2
+ // zyndo init — interactive seller config setup
3
3
  // ---------------------------------------------------------------------------
4
4
  import { createInterface } from 'node:readline';
5
5
  import { writeFileSync, mkdirSync, existsSync, accessSync, readdirSync, constants as fsConstants } from 'node:fs';
@@ -134,6 +134,11 @@ export async function runInit() {
134
134
  const skillId = await ask('Skill ID', 'coding.review.v1');
135
135
  const skillName = await ask('Skill name', 'Code Review');
136
136
  const skillDesc = await ask('Skill description', 'Reviews code for bugs and improvements');
137
+ const skillPriceRaw = await ask('Skill price in cents (REQUIRED, $0.10 minimum)', '500');
138
+ const skillPriceCents = Number.parseInt(skillPriceRaw, 10);
139
+ if (!Number.isInteger(skillPriceCents) || skillPriceCents < 10) {
140
+ throw new Error(`Skill price must be an integer of at least 10 cents ($0.10). Got: "${skillPriceRaw}"`);
141
+ }
137
142
  const category = skillId.split('.')[0] || 'coding';
138
143
  process.stdout.write('\n');
139
144
  const model = await ask('Model', selected.defaultModel);
@@ -154,7 +159,7 @@ export async function runInit() {
154
159
  ...(bridgeApiKey !== '' ? { api_key: bridgeApiKey } : {}),
155
160
  working_directory: workingDir,
156
161
  max_concurrent_tasks: 1,
157
- skills: [{ id: skillId, name: skillName, description: skillDesc }],
162
+ skills: [{ id: skillId, name: skillName, description: skillDesc, price_cents: skillPriceCents }],
158
163
  categories: [category]
159
164
  };
160
165
  const yaml = yamlStringify(configObj);
@@ -170,7 +175,7 @@ export async function runInit() {
170
175
  process.stdout.write('\n');
171
176
  process.stdout.write(` \x1b[32mConfig saved to ${configPath}\x1b[0m\n`);
172
177
  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`);
178
+ process.stdout.write(` Run: \x1b[1mzyndo serve\x1b[0m (from this directory)\n\n`);
174
179
  }
175
180
  finally {
176
181
  close();
package/package.json CHANGED
@@ -1,36 +1,44 @@
1
- {
2
- "name": "zyndo",
3
- "version": "0.1.0",
4
- "description": "The agent-to-agent CLI tool for sellers in the Zyndo Marketplace",
5
- "type": "module",
6
- "license": "MIT",
7
- "bin": {
8
- "zyndo": "./dist/index.js"
9
- },
10
- "main": "dist/index.js",
11
- "types": "dist/index.d.ts",
12
- "files": ["dist"],
13
- "exports": {
14
- ".": {
15
- "types": "./dist/index.d.ts",
16
- "import": "./dist/index.js"
17
- }
18
- },
19
- "engines": {
20
- "node": ">=18"
21
- },
22
- "keywords": ["zyndo", "ai-agents", "marketplace", "cli", "agent-to-agent"],
23
- "scripts": {
24
- "build": "tsc -p tsconfig.json",
25
- "test": "vitest run --config vitest.config.ts",
26
- "lint": "tsc -p tsconfig.json --noEmit"
27
- },
28
- "dependencies": {
29
- "yaml": "^2.7.0"
30
- },
31
- "devDependencies": {
32
- "@types/node": "^22.12.0",
33
- "typescript": "^5.7.3",
34
- "vitest": "^2.1.9"
35
- }
36
- }
1
+ {
2
+ "name": "zyndo",
3
+ "version": "0.1.2",
4
+ "description": "The agent-to-agent CLI tool for sellers in the Zyndo Marketplace",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "zyndo": "dist/index.js"
9
+ },
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "keywords": [
25
+ "zyndo",
26
+ "ai-agents",
27
+ "marketplace",
28
+ "cli",
29
+ "agent-to-agent"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json",
33
+ "test": "vitest run --config vitest.config.ts",
34
+ "lint": "tsc -p tsconfig.json --noEmit"
35
+ },
36
+ "dependencies": {
37
+ "yaml": "^2.7.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.12.0",
41
+ "typescript": "^5.7.3",
42
+ "vitest": "^2.1.9"
43
+ }
44
+ }