zyndo 0.1.3 → 0.1.4
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/config.js +9 -0
- package/dist/index.js +3 -2
- package/dist/init.d.ts +1 -1
- package/dist/init.js +418 -92
- package/dist/providers/claudeCode.js +98 -14
- package/examples/seller.yaml.example +73 -0
- package/package.json +3 -2
package/dist/config.js
CHANGED
|
@@ -65,6 +65,15 @@ export function loadSellerConfig(configPath) {
|
|
|
65
65
|
throw new Error(`Config: harness="${harness}" does not match binary "${effectiveBinary}" (detected as "${detected}" harness). ` +
|
|
66
66
|
`Pick one: either set "harness: ${detected}" to match the binary, or set "claude_code_binary" to a ${harness} binary path.`);
|
|
67
67
|
}
|
|
68
|
+
// Bare names on Windows are fragile because spawn uses shell:true to
|
|
69
|
+
// resolve .cmd shims, and a bare name passed through cmd.exe gets
|
|
70
|
+
// re-tokenized in surprising ways when the cwd contains spaces. Warn
|
|
71
|
+
// (do not fail) so existing configs keep working but the user knows
|
|
72
|
+
// to re-run init for a fully-qualified path.
|
|
73
|
+
if (process.platform === 'win32' && !effectiveBinary.includes('\\') && !effectiveBinary.includes('/')) {
|
|
74
|
+
process.stderr.write(`[zyndo] Warning: claude_code_binary "${effectiveBinary}" is a bare name. ` +
|
|
75
|
+
`On Windows this is fragile. Re-run \`zyndo init\` to write a fully-qualified path.\n`);
|
|
76
|
+
}
|
|
68
77
|
}
|
|
69
78
|
const claudeCodeTimeoutMs = typeof data.claude_code_timeout_ms === 'number' ? data.claude_code_timeout_ms : undefined;
|
|
70
79
|
const claudeCodeMaxBudgetUsd = typeof data.claude_code_max_budget_usd === 'number' ? data.claude_code_max_budget_usd : undefined;
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ async function main() {
|
|
|
27
27
|
}
|
|
28
28
|
case 'init': {
|
|
29
29
|
const { runInit } = await import('./init.js');
|
|
30
|
-
await runInit();
|
|
30
|
+
await runInit(args.slice(1));
|
|
31
31
|
break;
|
|
32
32
|
}
|
|
33
33
|
case 'mcp': {
|
|
@@ -47,7 +47,7 @@ async function main() {
|
|
|
47
47
|
break;
|
|
48
48
|
}
|
|
49
49
|
case 'version':
|
|
50
|
-
process.stdout.write('zyndo 0.1.
|
|
50
|
+
process.stdout.write('zyndo 0.1.4\n');
|
|
51
51
|
break;
|
|
52
52
|
case 'help':
|
|
53
53
|
case undefined:
|
|
@@ -65,6 +65,7 @@ zyndo — AI agent daemon for the Zyndo marketplace
|
|
|
65
65
|
|
|
66
66
|
Usage:
|
|
67
67
|
zyndo init Interactive setup (create seller config)
|
|
68
|
+
zyndo init --non-interactive Non-interactive setup (use --name, --skill-id, etc.)
|
|
68
69
|
zyndo serve [--config <path>] Start seller daemon
|
|
69
70
|
zyndo mcp Start MCP server for buyers
|
|
70
71
|
zyndo wallet [balance|topup|ledger] Manage wallet (opens dashboard)
|
package/dist/init.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function runInit(): Promise<void>;
|
|
1
|
+
export declare function runInit(argv?: ReadonlyArray<string>): Promise<void>;
|
package/dist/init.js
CHANGED
|
@@ -1,131 +1,447 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// zyndo init — interactive seller config setup
|
|
2
|
+
// zyndo init — interactive and non-interactive seller config setup
|
|
3
|
+
//
|
|
4
|
+
// Cross-platform: works on Windows (USERPROFILE, PATHEXT, .cmd shims) and
|
|
5
|
+
// POSIX. Resolves binaries via manual PATH walk so we never depend on the
|
|
6
|
+
// `which` command being present, and always writes a fully-qualified binary
|
|
7
|
+
// path into the config so the daemon never has to re-resolve at runtime.
|
|
3
8
|
// ---------------------------------------------------------------------------
|
|
4
9
|
import { createInterface } from 'node:readline';
|
|
5
10
|
import { writeFileSync, mkdirSync, existsSync, accessSync, readdirSync, constants as fsConstants } from 'node:fs';
|
|
6
|
-
import { resolve } from 'node:path';
|
|
7
|
-
import {
|
|
11
|
+
import { resolve, delimiter, sep, isAbsolute } from 'node:path';
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
8
13
|
import { stringify as yamlStringify } from 'yaml';
|
|
9
14
|
import { printBanner } from './banner.js';
|
|
10
15
|
// ---------------------------------------------------------------------------
|
|
11
|
-
//
|
|
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
|
|
16
|
+
// Cross-platform binary resolution
|
|
25
17
|
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a binary name to an absolute path. Returns undefined if not found.
|
|
20
|
+
*
|
|
21
|
+
* Honors PATHEXT on Windows so `codex` resolves to `codex.cmd`. Skips the
|
|
22
|
+
* external `which` command entirely (Linux-only) and walks PATH manually.
|
|
23
|
+
*
|
|
24
|
+
* If `name` already contains a path separator or is absolute, just verifies
|
|
25
|
+
* the file exists.
|
|
26
|
+
*/
|
|
27
|
+
function resolveBinary(name) {
|
|
28
|
+
if (name === '')
|
|
29
|
+
return undefined;
|
|
30
|
+
if (isAbsolute(name) || name.includes(sep) || name.includes('/')) {
|
|
29
31
|
try {
|
|
30
|
-
accessSync(name, fsConstants.
|
|
31
|
-
return
|
|
32
|
+
accessSync(name, fsConstants.F_OK);
|
|
33
|
+
return name;
|
|
32
34
|
}
|
|
33
35
|
catch {
|
|
34
|
-
return
|
|
36
|
+
return undefined;
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
const pathDirs = (process.env.PATH ?? '').split(delimiter).filter((d) => d.length > 0);
|
|
40
|
+
const exts = process.platform === 'win32'
|
|
41
|
+
? ['', ...(process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').map((e) => e.toLowerCase())]
|
|
42
|
+
: [''];
|
|
43
|
+
for (const dir of pathDirs) {
|
|
44
|
+
for (const ext of exts) {
|
|
45
|
+
const candidate = resolve(dir, `${name}${ext}`);
|
|
46
|
+
try {
|
|
47
|
+
accessSync(candidate, fsConstants.F_OK);
|
|
48
|
+
return candidate;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// try next
|
|
52
|
+
}
|
|
53
|
+
}
|
|
44
54
|
}
|
|
55
|
+
return undefined;
|
|
45
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Try harder to find Claude Code by checking common VS Code / Cursor
|
|
59
|
+
* extension paths on macOS, Linux, and Windows.
|
|
60
|
+
*/
|
|
46
61
|
function findClaudeBinary() {
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
const home = process.env.HOME ?? '';
|
|
62
|
+
const onPath = resolveBinary('claude');
|
|
63
|
+
if (onPath !== undefined)
|
|
64
|
+
return onPath;
|
|
65
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
52
66
|
if (home === '')
|
|
53
67
|
return undefined;
|
|
54
|
-
const
|
|
68
|
+
const candidateDirs = [
|
|
55
69
|
resolve(home, '.vscode', 'extensions'),
|
|
56
70
|
resolve(home, '.vscode-insiders', 'extensions'),
|
|
57
71
|
resolve(home, '.cursor', 'extensions')
|
|
58
72
|
];
|
|
59
|
-
|
|
73
|
+
// Windows VS Code per-user extension dirs
|
|
74
|
+
if (process.platform === 'win32') {
|
|
75
|
+
const appData = process.env.APPDATA ?? '';
|
|
76
|
+
const localAppData = process.env.LOCALAPPDATA ?? '';
|
|
77
|
+
if (appData !== '')
|
|
78
|
+
candidateDirs.push(resolve(appData, 'Code', 'User', 'extensions'));
|
|
79
|
+
if (localAppData !== '')
|
|
80
|
+
candidateDirs.push(resolve(localAppData, 'Programs', 'Microsoft VS Code', 'resources', 'app', 'extensions'));
|
|
81
|
+
}
|
|
82
|
+
for (const extDir of candidateDirs) {
|
|
60
83
|
if (!existsSync(extDir))
|
|
61
84
|
continue;
|
|
62
85
|
try {
|
|
63
86
|
const entries = readdirSync(extDir).filter((e) => e.startsWith('anthropic.claude-code-'));
|
|
64
|
-
// Sort descending to prefer latest version
|
|
65
87
|
entries.sort().reverse();
|
|
66
88
|
for (const entry of entries) {
|
|
67
|
-
const
|
|
68
|
-
|
|
89
|
+
const binName = process.platform === 'win32' ? 'claude.exe' : 'claude';
|
|
90
|
+
const candidate = resolve(extDir, entry, 'resources', 'native-binary', binName);
|
|
91
|
+
try {
|
|
92
|
+
accessSync(candidate, fsConstants.F_OK);
|
|
69
93
|
return candidate;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// try next
|
|
97
|
+
}
|
|
70
98
|
}
|
|
71
99
|
}
|
|
72
100
|
catch {
|
|
73
|
-
//
|
|
101
|
+
// permission denied or read failure, skip
|
|
74
102
|
}
|
|
75
103
|
}
|
|
76
104
|
return undefined;
|
|
77
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Codex is usually installed via npm-global. On Windows that puts a
|
|
108
|
+
* `codex.cmd` shim under `%APPDATA%\npm`. Check that explicitly in case
|
|
109
|
+
* PATH does not include it (some shells launched without inheriting the
|
|
110
|
+
* user PATH).
|
|
111
|
+
*/
|
|
112
|
+
function findCodexBinary() {
|
|
113
|
+
const onPath = resolveBinary('codex');
|
|
114
|
+
if (onPath !== undefined)
|
|
115
|
+
return onPath;
|
|
116
|
+
if (process.platform === 'win32') {
|
|
117
|
+
const appData = process.env.APPDATA ?? '';
|
|
118
|
+
if (appData !== '') {
|
|
119
|
+
const candidate = resolve(appData, 'npm', 'codex.cmd');
|
|
120
|
+
try {
|
|
121
|
+
accessSync(candidate, fsConstants.F_OK);
|
|
122
|
+
return candidate;
|
|
123
|
+
}
|
|
124
|
+
catch { /* not there */ }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Run `<binary> --version` to confirm the binary actually starts. Catches
|
|
131
|
+
* the silent-failure case where init writes a config that crashes at first
|
|
132
|
+
* task because the binary is broken or wrong.
|
|
133
|
+
*/
|
|
134
|
+
function quoteWinArg(arg) {
|
|
135
|
+
if (arg === '')
|
|
136
|
+
return '""';
|
|
137
|
+
if (!/[\s"&|<>^()]/.test(arg))
|
|
138
|
+
return arg;
|
|
139
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
140
|
+
}
|
|
141
|
+
function testBinary(binary) {
|
|
142
|
+
try {
|
|
143
|
+
// Same DEP0190 dance as the harness spawn: on Windows pass a quoted
|
|
144
|
+
// command string with shell:true (no args array) so .cmd shims work
|
|
145
|
+
// without triggering the deprecation. On POSIX spawn directly.
|
|
146
|
+
const result = process.platform === 'win32'
|
|
147
|
+
? spawnSync(`${quoteWinArg(binary)} --version`, {
|
|
148
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
149
|
+
timeout: 5000,
|
|
150
|
+
shell: true,
|
|
151
|
+
encoding: 'utf-8'
|
|
152
|
+
})
|
|
153
|
+
: spawnSync(binary, ['--version'], {
|
|
154
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
155
|
+
timeout: 5000,
|
|
156
|
+
encoding: 'utf-8'
|
|
157
|
+
});
|
|
158
|
+
if (result.error)
|
|
159
|
+
return { ok: false, error: result.error.message };
|
|
160
|
+
if (result.status !== 0) {
|
|
161
|
+
return { ok: false, error: `exit code ${result.status}: ${(result.stderr ?? '').trim().slice(0, 200)}` };
|
|
162
|
+
}
|
|
163
|
+
const version = ((result.stdout ?? '') + (result.stderr ?? '')).trim().split('\n')[0] ?? '';
|
|
164
|
+
return { ok: true, version };
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
78
170
|
const HARNESS_OPTIONS = [
|
|
79
171
|
{ label: 'Claude Code', harness: 'claude', binary: 'claude', defaultModel: 'sonnet' },
|
|
80
|
-
{ label: 'Codex CLI', harness: 'codex', binary: 'codex', defaultModel: '
|
|
172
|
+
{ label: 'Codex CLI', harness: 'codex', binary: 'codex', defaultModel: 'gpt-5-codex' },
|
|
81
173
|
{ label: 'Other / Custom binary', harness: 'generic', binary: '', defaultModel: 'default' }
|
|
82
174
|
];
|
|
175
|
+
function detectInstalledHarness() {
|
|
176
|
+
const claudeBin = findClaudeBinary();
|
|
177
|
+
if (claudeBin !== undefined)
|
|
178
|
+
return { harness: 'claude', binary: claudeBin, model: 'sonnet' };
|
|
179
|
+
const codexBin = findCodexBinary();
|
|
180
|
+
if (codexBin !== undefined)
|
|
181
|
+
return { harness: 'codex', binary: codexBin, model: 'gpt-5-codex' };
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
function parseFlags(argv) {
|
|
185
|
+
const flags = {};
|
|
186
|
+
for (let i = 0; i < argv.length; i++) {
|
|
187
|
+
const a = argv[i];
|
|
188
|
+
if (!a.startsWith('--'))
|
|
189
|
+
continue;
|
|
190
|
+
const key = a.slice(2);
|
|
191
|
+
const next = argv[i + 1];
|
|
192
|
+
if (next === undefined || next.startsWith('--')) {
|
|
193
|
+
flags[key] = true;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
flags[key] = next;
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const harnessRaw = typeof flags.harness === 'string' ? flags.harness : undefined;
|
|
201
|
+
const harness = harnessRaw === 'claude' || harnessRaw === 'codex' || harnessRaw === 'generic' ? harnessRaw : undefined;
|
|
202
|
+
const skillPriceRaw = typeof flags['skill-price-cents'] === 'string' ? flags['skill-price-cents'] : undefined;
|
|
203
|
+
const skillPriceCents = skillPriceRaw !== undefined ? Number.parseInt(skillPriceRaw, 10) : undefined;
|
|
204
|
+
return {
|
|
205
|
+
nonInteractive: flags['non-interactive'] === true,
|
|
206
|
+
name: typeof flags.name === 'string' ? flags.name : undefined,
|
|
207
|
+
description: typeof flags.description === 'string' ? flags.description : undefined,
|
|
208
|
+
harness,
|
|
209
|
+
binary: typeof flags.binary === 'string' ? flags.binary : undefined,
|
|
210
|
+
model: typeof flags.model === 'string' ? flags.model : undefined,
|
|
211
|
+
workingDirectory: typeof flags['working-directory'] === 'string' ? flags['working-directory'] : undefined,
|
|
212
|
+
apiKey: typeof flags['api-key'] === 'string' ? flags['api-key'] : undefined,
|
|
213
|
+
bridgeUrl: typeof flags['bridge-url'] === 'string' ? flags['bridge-url'] : undefined,
|
|
214
|
+
skillId: typeof flags['skill-id'] === 'string' ? flags['skill-id'] : undefined,
|
|
215
|
+
skillName: typeof flags['skill-name'] === 'string' ? flags['skill-name'] : undefined,
|
|
216
|
+
skillDesc: typeof flags['skill-desc'] === 'string' ? flags['skill-desc'] : undefined,
|
|
217
|
+
skillPriceCents,
|
|
218
|
+
force: flags.force === true
|
|
219
|
+
};
|
|
220
|
+
}
|
|
83
221
|
// ---------------------------------------------------------------------------
|
|
84
|
-
//
|
|
222
|
+
// Readline helper
|
|
85
223
|
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
224
|
+
function createPrompt() {
|
|
225
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
226
|
+
const ask = (question, defaultValue) => new Promise((resolveAns) => {
|
|
227
|
+
const suffix = defaultValue !== undefined ? ` (${defaultValue})` : '';
|
|
228
|
+
rl.question(` \x1b[36m?\x1b[0m ${question}${suffix}: `, (answer) => {
|
|
229
|
+
resolveAns(answer.trim() || defaultValue || '');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
return { ask, close: () => rl.close() };
|
|
233
|
+
}
|
|
234
|
+
const YAML_HEADER = `# Zyndo seller configuration.
|
|
235
|
+
#
|
|
236
|
+
# Generated by \`zyndo init\`. Edit fields as needed and re-run \`zyndo serve\`.
|
|
237
|
+
# Get an API key from https://zyndo.ai/dashboard (API Keys tab).
|
|
238
|
+
#
|
|
239
|
+
# Field reference:
|
|
240
|
+
# name Display name shown to buyer agents on the marketplace.
|
|
241
|
+
# description One-line pitch shown next to your name in browse results.
|
|
242
|
+
# bridge_url Zyndo broker URL. Leave as default unless self-hosting.
|
|
243
|
+
# api_key Your Zyndo API key (zyndo_live_sk_...). Required to connect.
|
|
244
|
+
# provider Always 'claude-code' (the spawn-a-CLI provider).
|
|
245
|
+
# harness 'claude' | 'codex' | 'generic'. MUST match the binary below.
|
|
246
|
+
# claude_code_binary Absolute path to your harness binary. Set by \`zyndo init\`.
|
|
247
|
+
# model Model name passed to the harness (e.g. 'sonnet', 'gpt-5-codex').
|
|
248
|
+
# working_directory Where the seller can read/write files.
|
|
249
|
+
# max_concurrent_tasks How many tasks the daemon will run in parallel.
|
|
250
|
+
# skills What you offer. price_cents is REQUIRED ($0.10 floor).
|
|
251
|
+
# categories Free-form tags used in marketplace browse filters.
|
|
252
|
+
`;
|
|
253
|
+
function writeConfig(out, force) {
|
|
254
|
+
const configDir = resolve(process.cwd(), '.zyndo');
|
|
255
|
+
const configPath = resolve(configDir, 'seller.yaml');
|
|
256
|
+
const configObj = {
|
|
257
|
+
name: out.name,
|
|
258
|
+
description: out.description,
|
|
259
|
+
bridge_url: out.bridgeUrl,
|
|
260
|
+
api_key: out.apiKey,
|
|
261
|
+
provider: 'claude-code',
|
|
262
|
+
harness: out.harness,
|
|
263
|
+
claude_code_binary: out.binary,
|
|
264
|
+
model: out.model,
|
|
265
|
+
working_directory: out.workingDirectory,
|
|
266
|
+
max_concurrent_tasks: 1,
|
|
267
|
+
skills: [
|
|
268
|
+
{
|
|
269
|
+
id: out.skillId,
|
|
270
|
+
name: out.skillName,
|
|
271
|
+
description: out.skillDesc,
|
|
272
|
+
price_cents: out.skillPriceCents
|
|
273
|
+
}
|
|
274
|
+
],
|
|
275
|
+
categories: [out.skillId.split('.')[0] || 'general']
|
|
276
|
+
};
|
|
277
|
+
const yaml = YAML_HEADER + '\n' + yamlStringify(configObj);
|
|
278
|
+
mkdirSync(configDir, { recursive: true });
|
|
279
|
+
if (existsSync(configPath) && !force) {
|
|
280
|
+
return { path: configPath, written: false };
|
|
281
|
+
}
|
|
282
|
+
writeFileSync(configPath, yaml, 'utf-8');
|
|
283
|
+
return { path: configPath, written: true };
|
|
284
|
+
}
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Non-interactive entry
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
function runNonInteractive(flags) {
|
|
289
|
+
const missing = [];
|
|
290
|
+
if (flags.name === undefined)
|
|
291
|
+
missing.push('--name');
|
|
292
|
+
if (flags.skillId === undefined)
|
|
293
|
+
missing.push('--skill-id');
|
|
294
|
+
if (flags.skillName === undefined)
|
|
295
|
+
missing.push('--skill-name');
|
|
296
|
+
if (flags.skillDesc === undefined)
|
|
297
|
+
missing.push('--skill-desc');
|
|
298
|
+
if (flags.skillPriceCents === undefined)
|
|
299
|
+
missing.push('--skill-price-cents');
|
|
300
|
+
if (flags.apiKey === undefined)
|
|
301
|
+
missing.push('--api-key');
|
|
302
|
+
if (missing.length > 0) {
|
|
303
|
+
throw new Error(`Non-interactive init requires: ${missing.join(', ')}\n\n` +
|
|
304
|
+
`Example:\n` +
|
|
305
|
+
` zyndo init --non-interactive \\\n` +
|
|
306
|
+
` --name "My Code Reviewer" \\\n` +
|
|
307
|
+
` --skill-id coding.review.v1 \\\n` +
|
|
308
|
+
` --skill-name "Code Review" \\\n` +
|
|
309
|
+
` --skill-desc "Reviews TypeScript code for bugs" \\\n` +
|
|
310
|
+
` --skill-price-cents 500 \\\n` +
|
|
311
|
+
` --api-key zyndo_live_sk_xxxxx`);
|
|
312
|
+
}
|
|
313
|
+
if (!Number.isInteger(flags.skillPriceCents) || flags.skillPriceCents < 10) {
|
|
314
|
+
throw new Error(`--skill-price-cents must be an integer >= 10 ($0.10 minimum). Got: ${flags.skillPriceCents}`);
|
|
315
|
+
}
|
|
316
|
+
// Resolve harness + binary
|
|
317
|
+
let harness;
|
|
318
|
+
let binary;
|
|
319
|
+
let model;
|
|
320
|
+
if (flags.harness !== undefined && flags.binary !== undefined) {
|
|
321
|
+
harness = flags.harness;
|
|
322
|
+
const resolved = resolveBinary(flags.binary);
|
|
323
|
+
if (resolved === undefined) {
|
|
324
|
+
throw new Error(`Binary "${flags.binary}" not found on PATH. Pass an absolute path with --binary.`);
|
|
325
|
+
}
|
|
326
|
+
binary = resolved;
|
|
327
|
+
}
|
|
328
|
+
else if (flags.harness !== undefined) {
|
|
329
|
+
// Harness specified, binary not. Look up the default binary for that harness.
|
|
330
|
+
harness = flags.harness;
|
|
331
|
+
const lookup = harness === 'claude' ? findClaudeBinary() : harness === 'codex' ? findCodexBinary() : undefined;
|
|
332
|
+
if (lookup === undefined) {
|
|
333
|
+
throw new Error(`--harness ${harness} specified but no ${harness} binary found on PATH. Install it or pass --binary <path>.`);
|
|
334
|
+
}
|
|
335
|
+
binary = lookup;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
// Auto-detect
|
|
339
|
+
const detected = detectInstalledHarness();
|
|
340
|
+
if (detected === undefined) {
|
|
341
|
+
throw new Error('No AI harness found. Install Claude Code or Codex CLI, or pass --harness and --binary explicitly.');
|
|
342
|
+
}
|
|
343
|
+
harness = detected.harness;
|
|
344
|
+
binary = detected.binary;
|
|
345
|
+
}
|
|
346
|
+
// Pick model: explicit > harness default
|
|
347
|
+
if (flags.model !== undefined) {
|
|
348
|
+
model = flags.model;
|
|
349
|
+
}
|
|
350
|
+
else if (harness === 'claude') {
|
|
351
|
+
model = 'sonnet';
|
|
352
|
+
}
|
|
353
|
+
else if (harness === 'codex') {
|
|
354
|
+
model = 'gpt-5-codex';
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
model = 'default';
|
|
358
|
+
}
|
|
359
|
+
// Functional test
|
|
360
|
+
const test = testBinary(binary);
|
|
361
|
+
if (!test.ok) {
|
|
362
|
+
throw new Error(`Binary at ${binary} failed --version test: ${test.error}`);
|
|
363
|
+
}
|
|
364
|
+
process.stdout.write(` \x1b[32mBinary OK:\x1b[0m ${binary} (${test.version})\n`);
|
|
365
|
+
const result = writeConfig({
|
|
366
|
+
name: flags.name,
|
|
367
|
+
description: flags.description ?? `AI agent offering ${flags.skillName} on the Zyndo marketplace`,
|
|
368
|
+
harness,
|
|
369
|
+
binary,
|
|
370
|
+
model,
|
|
371
|
+
workingDirectory: flags.workingDirectory ?? process.cwd(),
|
|
372
|
+
apiKey: flags.apiKey,
|
|
373
|
+
bridgeUrl: flags.bridgeUrl ?? 'https://bridge.zyndo.ai',
|
|
374
|
+
skillId: flags.skillId,
|
|
375
|
+
skillName: flags.skillName,
|
|
376
|
+
skillDesc: flags.skillDesc,
|
|
377
|
+
skillPriceCents: flags.skillPriceCents
|
|
378
|
+
}, flags.force === true);
|
|
379
|
+
if (!result.written) {
|
|
380
|
+
throw new Error(`${result.path} already exists. Pass --force to overwrite.`);
|
|
381
|
+
}
|
|
382
|
+
process.stdout.write(`\n \x1b[32mConfig saved to ${result.path}\x1b[0m\n`);
|
|
383
|
+
process.stdout.write(` Run: \x1b[1mzyndo serve\x1b[0m (from this directory)\n\n`);
|
|
384
|
+
}
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// Interactive entry
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
async function runInteractive() {
|
|
87
389
|
printBanner();
|
|
88
390
|
process.stdout.write(' Welcome to the Zyndo marketplace agent CLI.\n');
|
|
89
391
|
process.stdout.write(' This will create your seller agent config.\n\n');
|
|
90
392
|
const { ask, close } = createPrompt();
|
|
91
393
|
try {
|
|
92
|
-
//
|
|
394
|
+
// Auto-detect available harnesses up front so the menu shows what is found.
|
|
395
|
+
const detectedClaude = findClaudeBinary();
|
|
396
|
+
const detectedCodex = findCodexBinary();
|
|
397
|
+
const autoPick = detectedClaude !== undefined ? 0 : detectedCodex !== undefined ? 1 : -1;
|
|
93
398
|
process.stdout.write(' \x1b[1mSelect your AI harness:\x1b[0m\n');
|
|
94
399
|
for (let i = 0; i < HARNESS_OPTIONS.length; i++) {
|
|
95
400
|
const opt = HARNESS_OPTIONS[i];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
401
|
+
let tag = '';
|
|
402
|
+
if (opt.harness === 'claude' && detectedClaude !== undefined)
|
|
403
|
+
tag = '\x1b[32m (found)\x1b[0m';
|
|
404
|
+
if (opt.harness === 'codex' && detectedCodex !== undefined)
|
|
405
|
+
tag = '\x1b[32m (found)\x1b[0m';
|
|
406
|
+
const marker = i === autoPick ? ' \x1b[33m← default\x1b[0m' : '';
|
|
407
|
+
process.stdout.write(` ${i + 1}) ${opt.label}${tag}${marker}\n`);
|
|
99
408
|
}
|
|
100
409
|
process.stdout.write('\n');
|
|
101
|
-
const
|
|
102
|
-
const
|
|
410
|
+
const defaultChoice = autoPick === -1 ? '1' : String(autoPick + 1);
|
|
411
|
+
const choiceRaw = await ask('Choice [1-3]', defaultChoice);
|
|
412
|
+
const parsed = Number.parseInt(choiceRaw, 10);
|
|
103
413
|
const choiceIdx = Number.isNaN(parsed) ? 0 : Math.max(0, Math.min(HARNESS_OPTIONS.length - 1, parsed - 1));
|
|
104
414
|
const selected = HARNESS_OPTIONS[choiceIdx];
|
|
105
|
-
let binary =
|
|
106
|
-
if (selected.harness === '
|
|
107
|
-
binary =
|
|
108
|
-
|
|
415
|
+
let binary = '';
|
|
416
|
+
if (selected.harness === 'claude') {
|
|
417
|
+
binary = detectedClaude ?? '';
|
|
418
|
+
}
|
|
419
|
+
else if (selected.harness === 'codex') {
|
|
420
|
+
binary = detectedCodex ?? '';
|
|
421
|
+
}
|
|
422
|
+
if (binary === '') {
|
|
423
|
+
const promptDefault = selected.binary !== '' ? selected.binary : '';
|
|
424
|
+
const entered = await ask('Binary path or command', promptDefault);
|
|
425
|
+
if (entered === '') {
|
|
109
426
|
process.stdout.write(' No binary specified. Exiting.\n');
|
|
110
427
|
return;
|
|
111
428
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (found !== undefined) {
|
|
117
|
-
process.stdout.write(` \x1b[32mFound:\x1b[0m ${found}\n`);
|
|
118
|
-
binary = found;
|
|
429
|
+
const resolved = resolveBinary(entered);
|
|
430
|
+
if (resolved === undefined) {
|
|
431
|
+
process.stdout.write(` \x1b[31mNot found:\x1b[0m "${entered}" is not on PATH and is not an absolute path.\n`);
|
|
432
|
+
return;
|
|
119
433
|
}
|
|
434
|
+
binary = resolved;
|
|
120
435
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
436
|
+
process.stdout.write(` \x1b[32mUsing:\x1b[0m ${binary}\n`);
|
|
437
|
+
// Functional test
|
|
438
|
+
const test = testBinary(binary);
|
|
439
|
+
if (!test.ok) {
|
|
440
|
+
process.stdout.write(` \x1b[31mBinary test failed:\x1b[0m ${test.error}\n`);
|
|
441
|
+
process.stdout.write(` Make sure ${selected.label} is installed correctly, then re-run \`zyndo init\`.\n`);
|
|
442
|
+
return;
|
|
127
443
|
}
|
|
128
|
-
process.stdout.write(
|
|
444
|
+
process.stdout.write(` \x1b[32mVersion:\x1b[0m ${test.version}\n\n`);
|
|
129
445
|
// Agent details
|
|
130
446
|
const name = await ask('Agent name', 'My Zyndo Seller');
|
|
131
447
|
const description = await ask('Description', 'AI agent offering services on the Zyndo marketplace');
|
|
@@ -139,41 +455,40 @@ export async function runInit() {
|
|
|
139
455
|
if (!Number.isInteger(skillPriceCents) || skillPriceCents < 10) {
|
|
140
456
|
throw new Error(`Skill price must be an integer of at least 10 cents ($0.10). Got: "${skillPriceRaw}"`);
|
|
141
457
|
}
|
|
142
|
-
const category = skillId.split('.')[0] || 'coding';
|
|
143
458
|
process.stdout.write('\n');
|
|
144
459
|
const model = await ask('Model', selected.defaultModel);
|
|
145
|
-
const workingDir = await ask('Working directory',
|
|
460
|
+
const workingDir = await ask('Working directory', process.cwd());
|
|
146
461
|
process.stdout.write('\n \x1b[1mZyndo Marketplace\x1b[0m\n');
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const configPath = resolve(
|
|
152
|
-
|
|
153
|
-
name,
|
|
154
|
-
description,
|
|
155
|
-
provider: 'claude-code',
|
|
156
|
-
harness: selected.harness,
|
|
157
|
-
...(binary !== selected.binary ? { claude_code_binary: binary } : {}),
|
|
158
|
-
model,
|
|
159
|
-
...(bridgeApiKey !== '' ? { api_key: bridgeApiKey } : {}),
|
|
160
|
-
working_directory: workingDir,
|
|
161
|
-
max_concurrent_tasks: 1,
|
|
162
|
-
skills: [{ id: skillId, name: skillName, description: skillDesc, price_cents: skillPriceCents }],
|
|
163
|
-
categories: [category]
|
|
164
|
-
};
|
|
165
|
-
const yaml = yamlStringify(configObj);
|
|
166
|
-
mkdirSync(configDir, { recursive: true });
|
|
462
|
+
process.stdout.write(' Get an API key at https://zyndo.ai/dashboard (API Keys tab)\n');
|
|
463
|
+
const bridgeApiKey = await ask('Bridge API key', '');
|
|
464
|
+
const bridgeUrl = await ask('Bridge URL', 'https://bridge.zyndo.ai');
|
|
465
|
+
// Confirm overwrite if exists
|
|
466
|
+
const configPath = resolve(process.cwd(), '.zyndo', 'seller.yaml');
|
|
467
|
+
let force = true;
|
|
167
468
|
if (existsSync(configPath)) {
|
|
168
469
|
const overwrite = await ask(`${configPath} already exists. Overwrite? [y/N]`, 'N');
|
|
169
470
|
if (overwrite.toLowerCase() !== 'y') {
|
|
170
471
|
process.stdout.write(' Aborted. Config not changed.\n');
|
|
171
472
|
return;
|
|
172
473
|
}
|
|
474
|
+
force = true;
|
|
173
475
|
}
|
|
174
|
-
|
|
476
|
+
const result = writeConfig({
|
|
477
|
+
name,
|
|
478
|
+
description,
|
|
479
|
+
harness: selected.harness,
|
|
480
|
+
binary,
|
|
481
|
+
model,
|
|
482
|
+
workingDirectory: workingDir,
|
|
483
|
+
apiKey: bridgeApiKey,
|
|
484
|
+
bridgeUrl,
|
|
485
|
+
skillId,
|
|
486
|
+
skillName,
|
|
487
|
+
skillDesc,
|
|
488
|
+
skillPriceCents
|
|
489
|
+
}, force);
|
|
175
490
|
process.stdout.write('\n');
|
|
176
|
-
process.stdout.write(` \x1b[32mConfig saved to ${
|
|
491
|
+
process.stdout.write(` \x1b[32mConfig saved to ${result.path}\x1b[0m\n`);
|
|
177
492
|
process.stdout.write(` This seller lives in this folder. Add a CLAUDE.md for custom skills/rules.\n`);
|
|
178
493
|
process.stdout.write(` Run: \x1b[1mzyndo serve\x1b[0m (from this directory)\n\n`);
|
|
179
494
|
}
|
|
@@ -181,3 +496,14 @@ export async function runInit() {
|
|
|
181
496
|
close();
|
|
182
497
|
}
|
|
183
498
|
}
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// Main entry
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
export async function runInit(argv = []) {
|
|
503
|
+
const flags = parseFlags(argv);
|
|
504
|
+
if (flags.nonInteractive) {
|
|
505
|
+
runNonInteractive(flags);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
await runInteractive();
|
|
509
|
+
}
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// instead of making raw LLM API calls.
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
|
-
import { basename } from 'node:path';
|
|
6
|
+
import { basename, join } from 'node:path';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
9
|
+
import { readFileSync, unlinkSync } from 'node:fs';
|
|
7
10
|
// ---------------------------------------------------------------------------
|
|
8
11
|
// Harness detection and args
|
|
9
12
|
// ---------------------------------------------------------------------------
|
|
@@ -15,7 +18,7 @@ export function detectHarness(binary) {
|
|
|
15
18
|
return 'claude';
|
|
16
19
|
return 'generic';
|
|
17
20
|
}
|
|
18
|
-
function
|
|
21
|
+
function buildHarnessSpawn(harness, config, systemPrompt) {
|
|
19
22
|
switch (harness) {
|
|
20
23
|
case 'claude': {
|
|
21
24
|
const args = [
|
|
@@ -30,16 +33,33 @@ function buildHarnessArgs(harness, config, systemPrompt) {
|
|
|
30
33
|
if (config.claudeCodeMaxBudgetUsd !== undefined) {
|
|
31
34
|
args.push('--max-budget-usd', String(config.claudeCodeMaxBudgetUsd));
|
|
32
35
|
}
|
|
33
|
-
return args;
|
|
36
|
+
return { args };
|
|
34
37
|
}
|
|
35
|
-
case 'codex':
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
case 'codex': {
|
|
39
|
+
// Codex CLI 0.110+ uses `codex exec` for non-interactive runs.
|
|
40
|
+
// - `-` reads the prompt from stdin (we still pipe the seller prompt in).
|
|
41
|
+
// - `--dangerously-bypass-approvals-and-sandbox` is required because
|
|
42
|
+
// the seller daemon spawns codex headless and there is nobody to
|
|
43
|
+
// approve commands. The user explicitly opted in by running `zyndo serve`.
|
|
44
|
+
// - `-o <tempfile>` captures only the agent's last message, which is
|
|
45
|
+
// what we want as the deliverable text. Stdout includes progress
|
|
46
|
+
// events that would otherwise pollute parseOutput.
|
|
47
|
+
// - `--color never` strips ANSI escapes from any stderr we might log.
|
|
48
|
+
const outputFile = join(tmpdir(), `zyndo-codex-${Date.now()}-${randomBytes(4).toString('hex')}.txt`);
|
|
49
|
+
const args = [
|
|
50
|
+
'exec',
|
|
51
|
+
'-',
|
|
52
|
+
'-m', config.model,
|
|
53
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
54
|
+
'--skip-git-repo-check',
|
|
55
|
+
'-C', config.workingDirectory,
|
|
56
|
+
'--color', 'never',
|
|
57
|
+
'-o', outputFile
|
|
40
58
|
];
|
|
59
|
+
return { args, outputFile };
|
|
60
|
+
}
|
|
41
61
|
case 'generic':
|
|
42
|
-
return [];
|
|
62
|
+
return { args: [] };
|
|
43
63
|
}
|
|
44
64
|
}
|
|
45
65
|
// ---------------------------------------------------------------------------
|
|
@@ -120,6 +140,49 @@ function parseOutput(raw, harness) {
|
|
|
120
140
|
return { output: trimmed, paused: false };
|
|
121
141
|
}
|
|
122
142
|
// ---------------------------------------------------------------------------
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
123
186
|
// Main entry point
|
|
124
187
|
// ---------------------------------------------------------------------------
|
|
125
188
|
const DEFAULT_TIMEOUT_MS = 600_000; // 10 minutes
|
|
@@ -129,19 +192,24 @@ export async function runClaudeCodeTask(taskContext, config, logger) {
|
|
|
129
192
|
const timeoutMs = config.claudeCodeTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
130
193
|
const prompt = buildPrompt(taskContext, config);
|
|
131
194
|
const systemPrompt = config.systemPrompt;
|
|
132
|
-
const args =
|
|
195
|
+
const { args, outputFile } = buildHarnessSpawn(harness, config, systemPrompt);
|
|
133
196
|
logger.info(`Spawning ${harness} harness: ${binary} ${args.join(' ')}`);
|
|
134
197
|
return new Promise((resolve) => {
|
|
135
198
|
const controller = new AbortController();
|
|
136
199
|
const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
|
|
137
|
-
|
|
200
|
+
// Windows .cmd shims need shell:true to resolve. spawnHarness builds a
|
|
201
|
+
// pre-quoted command string for that case so we don't trigger DEP0190
|
|
202
|
+
// (passing shell:true + args array). On POSIX we spawn directly.
|
|
203
|
+
const proc = spawnHarness(binary, args, {
|
|
138
204
|
cwd: config.workingDirectory,
|
|
139
205
|
signal: controller.signal,
|
|
140
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
141
206
|
env: { ...process.env }
|
|
142
207
|
});
|
|
143
208
|
const stdoutChunks = [];
|
|
144
209
|
const stderrChunks = [];
|
|
210
|
+
// stdio is always ['pipe','pipe','pipe'] in spawnHarness, so the
|
|
211
|
+
// streams are guaranteed non-null at runtime. The wrapped return type
|
|
212
|
+
// does not narrow that, so assert.
|
|
145
213
|
proc.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
|
|
146
214
|
proc.stderr.on('data', (chunk) => stderrChunks.push(chunk));
|
|
147
215
|
proc.on('error', (err) => {
|
|
@@ -161,12 +229,28 @@ export async function runClaudeCodeTask(taskContext, config, logger) {
|
|
|
161
229
|
if (stderr.length > 0) {
|
|
162
230
|
logger.info(`Harness stderr: ${stderr.slice(0, 500)}`);
|
|
163
231
|
}
|
|
164
|
-
|
|
232
|
+
// Codex writes the agent's final message to a tempfile via `-o`.
|
|
233
|
+
// Read it back, then unlink. If the file is missing or unreadable
|
|
234
|
+
// (e.g. codex crashed before writing it), fall through to stdout.
|
|
235
|
+
let agentOutput = stdout;
|
|
236
|
+
if (outputFile !== undefined) {
|
|
237
|
+
try {
|
|
238
|
+
agentOutput = readFileSync(outputFile, 'utf-8');
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Tempfile not written. Keep stdout as a best-effort fallback.
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
unlinkSync(outputFile);
|
|
245
|
+
}
|
|
246
|
+
catch { /* ignore */ }
|
|
247
|
+
}
|
|
248
|
+
if (code !== 0 && agentOutput.length === 0) {
|
|
165
249
|
logger.error(`Harness exited with code ${code}`);
|
|
166
250
|
resolve({ output: `Harness failed (exit ${code}): ${stderr.slice(0, 1000)}`, paused: false });
|
|
167
251
|
return;
|
|
168
252
|
}
|
|
169
|
-
resolve(parseOutput(
|
|
253
|
+
resolve(parseOutput(agentOutput, harness));
|
|
170
254
|
});
|
|
171
255
|
proc.stdin.write(prompt);
|
|
172
256
|
proc.stdin.end();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Zyndo seller configuration — annotated example.
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to .zyndo/seller.yaml in the directory you want your seller
|
|
4
|
+
# to live in, then run `zyndo serve` from that directory. Most users should
|
|
5
|
+
# run `zyndo init` instead, which writes this file with the right defaults
|
|
6
|
+
# and a verified binary path.
|
|
7
|
+
#
|
|
8
|
+
# Get an API key at https://zyndo.ai/dashboard (API Keys tab).
|
|
9
|
+
|
|
10
|
+
# Display name shown to buyer agents on the marketplace.
|
|
11
|
+
name: My Code Reviewer
|
|
12
|
+
|
|
13
|
+
# One-line pitch shown next to your name in browse results.
|
|
14
|
+
description: Reviews TypeScript and Python code for bugs and improvements.
|
|
15
|
+
|
|
16
|
+
# Zyndo broker URL. Leave as default unless self-hosting the broker.
|
|
17
|
+
bridge_url: https://bridge.zyndo.ai
|
|
18
|
+
|
|
19
|
+
# Your Zyndo API key. REQUIRED. Format: zyndo_live_sk_xxxxx
|
|
20
|
+
api_key: zyndo_live_sk_REPLACE_ME
|
|
21
|
+
|
|
22
|
+
# Always 'claude-code' for now. This selects the spawn-a-CLI provider that
|
|
23
|
+
# routes tasks to your local Claude Code or Codex binary.
|
|
24
|
+
provider: claude-code
|
|
25
|
+
|
|
26
|
+
# Which CLI harness to use. MUST match the binary below.
|
|
27
|
+
# claude → Anthropic Claude Code (`claude` or `claude.exe`)
|
|
28
|
+
# codex → OpenAI Codex CLI (`codex` or `codex.cmd`)
|
|
29
|
+
# generic → any other CLI; you handle args via env / wrapper
|
|
30
|
+
harness: claude
|
|
31
|
+
|
|
32
|
+
# Absolute path to the harness binary. Always use a full path here.
|
|
33
|
+
# zyndo init writes this for you. Examples:
|
|
34
|
+
# macOS/Linux: /usr/local/bin/claude
|
|
35
|
+
# Windows: C:\Users\you\AppData\Roaming\npm\codex.cmd
|
|
36
|
+
claude_code_binary: /usr/local/bin/claude
|
|
37
|
+
|
|
38
|
+
# Model name passed to the harness.
|
|
39
|
+
# claude harness: sonnet | opus | haiku | claude-sonnet-4-6 | ...
|
|
40
|
+
# codex harness: gpt-5-codex | o4-mini | ...
|
|
41
|
+
model: sonnet
|
|
42
|
+
|
|
43
|
+
# The directory the seller can read/write inside. Use an absolute path
|
|
44
|
+
# for clarity. The seller daemon does NOT chroot — the harness binary's
|
|
45
|
+
# own permission model controls what is reachable.
|
|
46
|
+
working_directory: /Users/you/projects/zyndo-seller-workspace
|
|
47
|
+
|
|
48
|
+
# How many buyer tasks the daemon will process in parallel. Start at 1.
|
|
49
|
+
max_concurrent_tasks: 1
|
|
50
|
+
|
|
51
|
+
# What you offer. Each skill MUST have price_cents (minimum 10 = $0.10).
|
|
52
|
+
# The marketplace adds a 25% buyer markup on top of price_cents, so a
|
|
53
|
+
# price_cents of 500 ($5.00) is shown to buyers as $6.25.
|
|
54
|
+
skills:
|
|
55
|
+
- id: coding.review.v1
|
|
56
|
+
name: Code Review
|
|
57
|
+
description: Reviews TypeScript or Python code for bugs and improvements.
|
|
58
|
+
price_cents: 500
|
|
59
|
+
|
|
60
|
+
# Add more skills as needed. Each must be unique by id.
|
|
61
|
+
# - id: coding.test.v1
|
|
62
|
+
# name: Test Generation
|
|
63
|
+
# description: Writes Vitest unit tests for an existing module.
|
|
64
|
+
# price_cents: 800
|
|
65
|
+
|
|
66
|
+
# Free-form tags used in marketplace browse filters. The first segment of
|
|
67
|
+
# your skill id is a good default (e.g. coding.review.v1 → coding).
|
|
68
|
+
categories:
|
|
69
|
+
- coding
|
|
70
|
+
|
|
71
|
+
# Optional: how to spawn the harness.
|
|
72
|
+
# claude_code_timeout_ms: 600000 # 10 minutes per task (default)
|
|
73
|
+
# claude_code_max_budget_usd: 5.0 # cap per task (claude only)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zyndo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "The agent-to-agent CLI tool for sellers in the Zyndo Marketplace",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"main": "dist/index.js",
|
|
11
11
|
"types": "dist/index.d.ts",
|
|
12
12
|
"files": [
|
|
13
|
-
"dist"
|
|
13
|
+
"dist",
|
|
14
|
+
"examples"
|
|
14
15
|
],
|
|
15
16
|
"exports": {
|
|
16
17
|
".": {
|