create-syrinx-agent 4.4.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/LICENSE +22 -0
- package/README.md +91 -0
- package/dist/index.js +1100 -0
- package/dist/index.js.map +7 -0
- package/package.json +45 -0
- package/src/cli.ts +76 -0
- package/src/exit-codes.ts +20 -0
- package/src/generate.ts +35 -0
- package/src/help.ts +45 -0
- package/src/index.ts +16 -0
- package/src/options.test.ts +130 -0
- package/src/options.ts +176 -0
- package/src/providers/realtime.ts +31 -0
- package/src/providers/reasoner.ts +69 -0
- package/src/providers/stt.ts +68 -0
- package/src/providers/tts.ts +69 -0
- package/src/providers/types.ts +58 -0
- package/src/providers/vad-endpointing.ts +36 -0
- package/src/templates/agent-module.ts +188 -0
- package/src/templates/agents-md.ts +65 -0
- package/src/templates/cloudflare-worker.ts +172 -0
- package/src/templates/dev-server.ts +104 -0
- package/src/templates/env-example.ts +58 -0
- package/src/templates/fixture.ts +34 -0
- package/src/templates/package-json.ts +119 -0
- package/src/templates/tsconfig.ts +25 -0
- package/src/version.ts +5 -0
- package/src/write-project.test.ts +163 -0
- package/src/write-project.ts +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
|
|
6
|
+
// src/exit-codes.ts
|
|
7
|
+
var EXIT_CODES = {
|
|
8
|
+
SUCCESS: 0,
|
|
9
|
+
INTERNAL: 1,
|
|
10
|
+
USAGE: 2
|
|
11
|
+
};
|
|
12
|
+
var CliError = class extends Error {
|
|
13
|
+
exitCode;
|
|
14
|
+
constructor(exitCode, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "CliError";
|
|
17
|
+
this.exitCode = exitCode;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/help.ts
|
|
22
|
+
var HELP_TEXT = `create-syrinx-agent \u2014 scaffold a Syrinx voice agent project.
|
|
23
|
+
|
|
24
|
+
One generator with conditional emission, not enumerated template directories:
|
|
25
|
+
pick a provider per pipeline stage with flags and get back a project that
|
|
26
|
+
depends on exactly those providers and typechecks out of the box.
|
|
27
|
+
|
|
28
|
+
USAGE
|
|
29
|
+
npm create syrinx-agent <target-dir> [options]
|
|
30
|
+
create-syrinx-agent <target-dir> [options]
|
|
31
|
+
|
|
32
|
+
PIPELINE (cascade mode \u2014 pick one provider per stage)
|
|
33
|
+
--stt <deepgram|google|elevenlabs|grok>
|
|
34
|
+
--tts <cartesia|elevenlabs|gemini|openai-tts|grok>
|
|
35
|
+
--reasoner <aisdk|kuralle|mastra>
|
|
36
|
+
--vad <silero-vad> optional, provider STT/endpointing owns it by default
|
|
37
|
+
--endpointing <pipecat-smart-turn|vap> optional, provider STT owns it by default
|
|
38
|
+
|
|
39
|
+
PIPELINE (speech-to-speech \u2014 exclusive with --stt/--tts)
|
|
40
|
+
--realtime <realtime|grok>
|
|
41
|
+
A speech-to-speech pipeline has no separate STT/reasoner/TTS stages.
|
|
42
|
+
Passing --stt or --tts alongside --realtime is refused.
|
|
43
|
+
|
|
44
|
+
TRANSPORT
|
|
45
|
+
--transport <browser|twilio|telnyx|smartpbx> default: browser
|
|
46
|
+
--runtime <node|cloudflare> default: node
|
|
47
|
+
--runtime cloudflare --transport telnyx is generated but warned as
|
|
48
|
+
deploy-unverified on the Workers edge.
|
|
49
|
+
|
|
50
|
+
PRESETS (flag bundles \u2014 an explicit flag always overrides the preset)
|
|
51
|
+
--preset <phone>
|
|
52
|
+
|
|
53
|
+
OPTIONS
|
|
54
|
+
--name <project-name> default: the target directory's basename
|
|
55
|
+
--yes accept defaults for anything not passed, never prompt
|
|
56
|
+
--no-install, --skip-install
|
|
57
|
+
write the project but skip \`npm install\`
|
|
58
|
+
--dry-run print the file list and exit; write nothing
|
|
59
|
+
--help, -h show this help
|
|
60
|
+
--version, -v print the generator's version
|
|
61
|
+
|
|
62
|
+
Never prompts when --yes is passed or stdin is not a TTY: missing required
|
|
63
|
+
flags fail with a USAGE error naming what is missing.
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
// src/version.ts
|
|
67
|
+
var GENERATOR_VERSION = "0.1.0";
|
|
68
|
+
|
|
69
|
+
// src/options.ts
|
|
70
|
+
import { resolve } from "node:path";
|
|
71
|
+
var STT_PROVIDERS = ["deepgram", "google", "elevenlabs", "grok"];
|
|
72
|
+
var TTS_PROVIDERS = ["cartesia", "elevenlabs", "gemini", "openai-tts", "grok"];
|
|
73
|
+
var REALTIME_PROVIDERS = ["realtime", "grok"];
|
|
74
|
+
var REASONER_PROVIDERS = ["aisdk", "kuralle", "mastra"];
|
|
75
|
+
var VAD_PROVIDERS = ["silero-vad"];
|
|
76
|
+
var ENDPOINTING_PROVIDERS = ["pipecat-smart-turn", "vap"];
|
|
77
|
+
var TRANSPORTS = ["browser", "twilio", "telnyx", "smartpbx"];
|
|
78
|
+
var RUNTIMES = ["node", "cloudflare"];
|
|
79
|
+
var PRESETS = {
|
|
80
|
+
phone: { stt: "deepgram", tts: "cartesia", reasoner: "aisdk", transport: "twilio", runtime: "node" }
|
|
81
|
+
};
|
|
82
|
+
function memberOf(list, value, flagName) {
|
|
83
|
+
if (!list.includes(value)) {
|
|
84
|
+
throw new CliError(EXIT_CODES.USAGE, `--${flagName} must be one of: ${list.join(", ")} (got "${value}")`);
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function resolveOptions(raw, positionals) {
|
|
89
|
+
if (raw.preset !== void 0 && !(raw.preset in PRESETS)) {
|
|
90
|
+
throw new CliError(EXIT_CODES.USAGE, `--preset must be one of: ${Object.keys(PRESETS).join(", ")} (got "${raw.preset}")`);
|
|
91
|
+
}
|
|
92
|
+
const preset = raw.preset !== void 0 ? PRESETS[raw.preset] : void 0;
|
|
93
|
+
const sttRaw = raw.stt ?? preset?.stt;
|
|
94
|
+
const ttsRaw = raw.tts ?? preset?.tts;
|
|
95
|
+
const reasonerRaw = raw.reasoner ?? preset?.reasoner;
|
|
96
|
+
const realtimeRaw = raw.realtime;
|
|
97
|
+
if (realtimeRaw !== void 0 && (sttRaw !== void 0 || ttsRaw !== void 0)) {
|
|
98
|
+
throw new CliError(
|
|
99
|
+
EXIT_CODES.USAGE,
|
|
100
|
+
`--realtime is a speech-to-speech pipeline with no separate STT/TTS stages \u2014 it cannot be combined with ${[
|
|
101
|
+
sttRaw !== void 0 ? "--stt" : void 0,
|
|
102
|
+
ttsRaw !== void 0 ? "--tts" : void 0
|
|
103
|
+
].filter((v) => v !== void 0).join(" and ")}.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const transport = memberOf(TRANSPORTS, raw.transport ?? preset?.transport ?? "browser", "transport");
|
|
107
|
+
const runtime = memberOf(RUNTIMES, raw.runtime ?? preset?.runtime ?? "node", "runtime");
|
|
108
|
+
const skipInstall = raw["no-install"] === true || raw["skip-install"] === true;
|
|
109
|
+
const dryRun = raw["dry-run"] === true;
|
|
110
|
+
const targetDir = positionals[0];
|
|
111
|
+
const name = raw.name ?? (targetDir !== void 0 ? targetDir.split("/").filter(Boolean).at(-1) : void 0);
|
|
112
|
+
if (name === void 0 || name.length === 0) {
|
|
113
|
+
throw new CliError(EXIT_CODES.USAGE, "a target directory or --name is required, e.g. `create-syrinx-agent my-agent`");
|
|
114
|
+
}
|
|
115
|
+
const resolvedTarget = resolve(process.cwd(), targetDir ?? name);
|
|
116
|
+
if (realtimeRaw !== void 0) {
|
|
117
|
+
const realtime = memberOf(REALTIME_PROVIDERS, realtimeRaw, "realtime");
|
|
118
|
+
return { mode: "realtime", realtime, transport, runtime, name, targetDir: resolvedTarget, skipInstall, dryRun };
|
|
119
|
+
}
|
|
120
|
+
const missing = [
|
|
121
|
+
sttRaw === void 0 ? "--stt" : void 0,
|
|
122
|
+
ttsRaw === void 0 ? "--tts" : void 0,
|
|
123
|
+
reasonerRaw === void 0 ? "--reasoner" : void 0
|
|
124
|
+
].filter((v) => v !== void 0);
|
|
125
|
+
if (missing.length > 0) {
|
|
126
|
+
throw new CliError(
|
|
127
|
+
EXIT_CODES.USAGE,
|
|
128
|
+
`missing required flag(s): ${missing.join(", ")} (or pass --realtime for a speech-to-speech pipeline, or --preset <name>)`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const stt = memberOf(STT_PROVIDERS, sttRaw, "stt");
|
|
132
|
+
const tts = memberOf(TTS_PROVIDERS, ttsRaw, "tts");
|
|
133
|
+
const reasoner = memberOf(REASONER_PROVIDERS, reasonerRaw, "reasoner");
|
|
134
|
+
const vad = raw.vad !== void 0 ? memberOf(VAD_PROVIDERS, raw.vad, "vad") : void 0;
|
|
135
|
+
const endpointing = raw.endpointing !== void 0 ? memberOf(ENDPOINTING_PROVIDERS, raw.endpointing, "endpointing") : void 0;
|
|
136
|
+
return { mode: "cascade", stt, tts, reasoner, vad, endpointing, transport, runtime, name, targetDir: resolvedTarget, skipInstall, dryRun };
|
|
137
|
+
}
|
|
138
|
+
function warningsFor(opts) {
|
|
139
|
+
const warnings = [];
|
|
140
|
+
if (opts.runtime === "cloudflare" && opts.transport === "telnyx") {
|
|
141
|
+
warnings.push(
|
|
142
|
+
"runtime=cloudflare + transport=telnyx is generated but not yet bound on the Workers edge \u2014 deploy-unverified. See @kuralle-syrinx/server-workers for current Cloudflare transport coverage before deploying."
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return warnings;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/write-project.ts
|
|
149
|
+
import { spawnSync } from "node:child_process";
|
|
150
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
151
|
+
import { dirname, join } from "node:path";
|
|
152
|
+
|
|
153
|
+
// src/providers/stt.ts
|
|
154
|
+
var PKG_VERSION = "^4.3.0";
|
|
155
|
+
var STT_STAGE_PROVIDERS = {
|
|
156
|
+
// packages/deepgram/src/stt.ts: DeepgramSTTPlugin, endpointingCapability.owner === "provider_stt".
|
|
157
|
+
// Config fields mirror examples/02-hello-voice-headless/src/hello-voice-agent.ts.
|
|
158
|
+
deepgram: {
|
|
159
|
+
packageName: "@kuralle-syrinx/deepgram",
|
|
160
|
+
packageVersion: PKG_VERSION,
|
|
161
|
+
className: "DeepgramSTTPlugin",
|
|
162
|
+
envKeys: [{ key: "DEEPGRAM_API_KEY", required: true }],
|
|
163
|
+
usesSocketFactory: true,
|
|
164
|
+
ownsEndpointing: true,
|
|
165
|
+
configFields: (envRef2) => [
|
|
166
|
+
`api_key: ${envRef2("DEEPGRAM_API_KEY")}`,
|
|
167
|
+
`model: "nova-3"`,
|
|
168
|
+
`sample_rate: 16000`,
|
|
169
|
+
`emit_eos_on_final: true`
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
// packages/google/src/index.ts: GoogleSTTPlugin (Cloud Speech-to-Text v2).
|
|
173
|
+
// api_key + project_id are required config; endpointingCapability.owner === "provider_stt".
|
|
174
|
+
google: {
|
|
175
|
+
packageName: "@kuralle-syrinx/google",
|
|
176
|
+
packageVersion: PKG_VERSION,
|
|
177
|
+
className: "GoogleSTTPlugin",
|
|
178
|
+
envKeys: [
|
|
179
|
+
{ key: "GOOGLE_CLOUD_SPEECH_API_KEY", required: true },
|
|
180
|
+
{ key: "GOOGLE_CLOUD_PROJECT_ID", required: true }
|
|
181
|
+
],
|
|
182
|
+
usesSocketFactory: true,
|
|
183
|
+
ownsEndpointing: true,
|
|
184
|
+
configFields: (envRef2) => [
|
|
185
|
+
`api_key: ${envRef2("GOOGLE_CLOUD_SPEECH_API_KEY")}`,
|
|
186
|
+
`project_id: ${envRef2("GOOGLE_CLOUD_PROJECT_ID")}`,
|
|
187
|
+
`sample_rate: 16000`
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
// packages/elevenlabs/src/stt.ts: ElevenLabsSTTPlugin (Scribe v2 Realtime).
|
|
191
|
+
elevenlabs: {
|
|
192
|
+
packageName: "@kuralle-syrinx/elevenlabs",
|
|
193
|
+
packageVersion: PKG_VERSION,
|
|
194
|
+
importFrom: "@kuralle-syrinx/elevenlabs/stt",
|
|
195
|
+
className: "ElevenLabsSTTPlugin",
|
|
196
|
+
envKeys: [{ key: "ELEVENLABS_API_KEY", required: true }],
|
|
197
|
+
usesSocketFactory: true,
|
|
198
|
+
ownsEndpointing: true,
|
|
199
|
+
configFields: (envRef2) => [`api_key: ${envRef2("ELEVENLABS_API_KEY")}`]
|
|
200
|
+
},
|
|
201
|
+
// packages/grok/src/stt.ts: GrokSTTPlugin. endpointingCapability.owner === "provider_stt".
|
|
202
|
+
grok: {
|
|
203
|
+
packageName: "@kuralle-syrinx/grok",
|
|
204
|
+
packageVersion: PKG_VERSION,
|
|
205
|
+
importFrom: "@kuralle-syrinx/grok/stt",
|
|
206
|
+
className: "GrokSTTPlugin",
|
|
207
|
+
envKeys: [{ key: "XAI_API_KEY", required: true }],
|
|
208
|
+
usesSocketFactory: true,
|
|
209
|
+
ownsEndpointing: true,
|
|
210
|
+
configFields: (envRef2) => [`api_key: ${envRef2("XAI_API_KEY")}`, `language: "en"`]
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/providers/tts.ts
|
|
215
|
+
var PKG_VERSION2 = "^4.3.0";
|
|
216
|
+
var TTS_STAGE_PROVIDERS = {
|
|
217
|
+
// packages/cartesia/src/index.ts: CartesiaTTSPlugin. Config mirrors hello-voice-agent.ts.
|
|
218
|
+
cartesia: {
|
|
219
|
+
packageName: "@kuralle-syrinx/cartesia",
|
|
220
|
+
packageVersion: PKG_VERSION2,
|
|
221
|
+
className: "CartesiaTTSPlugin",
|
|
222
|
+
envKeys: [
|
|
223
|
+
{ key: "CARTESIA_API_KEY", required: true },
|
|
224
|
+
{ key: "CARTESIA_VOICE_ID", required: true }
|
|
225
|
+
],
|
|
226
|
+
usesSocketFactory: true,
|
|
227
|
+
ownsEndpointing: false,
|
|
228
|
+
configFields: (envRef2) => [`api_key: ${envRef2("CARTESIA_API_KEY")}`, `voice_id: ${envRef2("CARTESIA_VOICE_ID")}`]
|
|
229
|
+
},
|
|
230
|
+
// packages/elevenlabs/src/tts.ts: ElevenLabsTTSPlugin.
|
|
231
|
+
elevenlabs: {
|
|
232
|
+
packageName: "@kuralle-syrinx/elevenlabs",
|
|
233
|
+
packageVersion: PKG_VERSION2,
|
|
234
|
+
importFrom: "@kuralle-syrinx/elevenlabs/tts",
|
|
235
|
+
className: "ElevenLabsTTSPlugin",
|
|
236
|
+
envKeys: [
|
|
237
|
+
{ key: "ELEVENLABS_API_KEY", required: true },
|
|
238
|
+
{ key: "ELEVENLABS_VOICE_ID", required: true }
|
|
239
|
+
],
|
|
240
|
+
usesSocketFactory: true,
|
|
241
|
+
ownsEndpointing: false,
|
|
242
|
+
configFields: (envRef2) => [`api_key: ${envRef2("ELEVENLABS_API_KEY")}`, `voice_id: ${envRef2("ELEVENLABS_VOICE_ID")}`]
|
|
243
|
+
},
|
|
244
|
+
// packages/gemini/src/index.ts: GeminiTTSPlugin. HTTP-based (@google/genai) — no SocketFactory.
|
|
245
|
+
gemini: {
|
|
246
|
+
packageName: "@kuralle-syrinx/gemini",
|
|
247
|
+
packageVersion: PKG_VERSION2,
|
|
248
|
+
className: "GeminiTTSPlugin",
|
|
249
|
+
envKeys: [{ key: "GOOGLE_GENERATIVE_AI_API_KEY", required: true }],
|
|
250
|
+
usesSocketFactory: false,
|
|
251
|
+
ownsEndpointing: false,
|
|
252
|
+
configFields: (envRef2) => [`api_key: ${envRef2("GOOGLE_GENERATIVE_AI_API_KEY")}`]
|
|
253
|
+
},
|
|
254
|
+
// packages/openai-tts/src/index.ts: OpenAICompatibleTTSPlugin. HTTP-based — no SocketFactory.
|
|
255
|
+
"openai-tts": {
|
|
256
|
+
packageName: "@kuralle-syrinx/openai-tts",
|
|
257
|
+
packageVersion: PKG_VERSION2,
|
|
258
|
+
className: "OpenAICompatibleTTSPlugin",
|
|
259
|
+
envKeys: [{ key: "OPENAI_API_KEY", required: true }],
|
|
260
|
+
usesSocketFactory: false,
|
|
261
|
+
ownsEndpointing: false,
|
|
262
|
+
configFields: (envRef2) => [`api_key: ${envRef2("OPENAI_API_KEY")}`]
|
|
263
|
+
},
|
|
264
|
+
// packages/grok/src/tts.ts: GrokTTSPlugin.
|
|
265
|
+
grok: {
|
|
266
|
+
packageName: "@kuralle-syrinx/grok",
|
|
267
|
+
packageVersion: PKG_VERSION2,
|
|
268
|
+
importFrom: "@kuralle-syrinx/grok/tts",
|
|
269
|
+
className: "GrokTTSPlugin",
|
|
270
|
+
envKeys: [{ key: "XAI_API_KEY", required: true }],
|
|
271
|
+
usesSocketFactory: true,
|
|
272
|
+
ownsEndpointing: false,
|
|
273
|
+
configFields: (envRef2) => [`api_key: ${envRef2("XAI_API_KEY")}`, `voice_id: "eve"`]
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/providers/reasoner.ts
|
|
278
|
+
var PKG_VERSION3 = "^4.3.0";
|
|
279
|
+
var OPENAI_SDK_VERSION = "^3.0.67";
|
|
280
|
+
var AI_SDK_VERSION = "^6.0.0";
|
|
281
|
+
var MASTRA_CORE_VERSION = "^1.41.0";
|
|
282
|
+
var SYSTEM_PROMPT_CONST = "SYSTEM_PROMPT";
|
|
283
|
+
var REASONER_PROVIDERS2 = {
|
|
284
|
+
// packages/aisdk/src/from-ai-sdk.ts: fromStreamText(config) -> Reasoner.
|
|
285
|
+
// Mirrors examples/02-hello-voice-headless/src/hello-voice-agent.ts.
|
|
286
|
+
aisdk: {
|
|
287
|
+
packageName: "@kuralle-syrinx/aisdk",
|
|
288
|
+
packageVersion: PKG_VERSION3,
|
|
289
|
+
extraPackages: { "@ai-sdk/openai": OPENAI_SDK_VERSION, ai: AI_SDK_VERSION },
|
|
290
|
+
envKeys: [{ key: "OPENAI_API_KEY", required: true }],
|
|
291
|
+
importLines: () => [
|
|
292
|
+
`import { fromStreamText } from "@kuralle-syrinx/aisdk";`,
|
|
293
|
+
`import { createOpenAI } from "@ai-sdk/openai";`
|
|
294
|
+
],
|
|
295
|
+
preludeLines: (envRef2) => [`const openai = createOpenAI({ apiKey: ${envRef2("OPENAI_API_KEY")} });`],
|
|
296
|
+
reasonerExpr: `fromStreamText({ model: openai("gpt-4.1-mini"), system: ${SYSTEM_PROMPT_CONST} })`
|
|
297
|
+
},
|
|
298
|
+
// packages/mastra/src/from-mastra.ts: fromMastraAgent(agent) -> Reasoner.
|
|
299
|
+
// Mirrors examples/02-hello-voice-headless/src/university-support-mastra.ts.
|
|
300
|
+
mastra: {
|
|
301
|
+
packageName: "@kuralle-syrinx/mastra",
|
|
302
|
+
packageVersion: PKG_VERSION3,
|
|
303
|
+
extraPackages: { "@ai-sdk/openai": OPENAI_SDK_VERSION, "@mastra/core": MASTRA_CORE_VERSION },
|
|
304
|
+
envKeys: [{ key: "OPENAI_API_KEY", required: true }],
|
|
305
|
+
importLines: () => [
|
|
306
|
+
`import { fromMastraAgent, type MastraAgentLike } from "@kuralle-syrinx/mastra";`,
|
|
307
|
+
`import { createOpenAI } from "@ai-sdk/openai";`,
|
|
308
|
+
`import { Agent as MastraAgent } from "@mastra/core/agent";`
|
|
309
|
+
],
|
|
310
|
+
preludeLines: (envRef2) => [
|
|
311
|
+
`const openai = createOpenAI({ apiKey: ${envRef2("OPENAI_API_KEY")} });`,
|
|
312
|
+
`const mastraAgent = new MastraAgent({`,
|
|
313
|
+
` id: "syrinx-agent",`,
|
|
314
|
+
` name: "syrinx-agent",`,
|
|
315
|
+
` instructions: ${SYSTEM_PROMPT_CONST},`,
|
|
316
|
+
` model: openai("gpt-4.1-mini"),`,
|
|
317
|
+
`});`
|
|
318
|
+
],
|
|
319
|
+
// Structural cast to MastraAgentLike, same pattern the reference agent uses —
|
|
320
|
+
// fromMastraAgent types against a structural interface, not @mastra/core's own type.
|
|
321
|
+
reasonerExpr: `fromMastraAgent(mastraAgent as unknown as MastraAgentLike)`
|
|
322
|
+
},
|
|
323
|
+
kuralle: void 0
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// src/providers/realtime.ts
|
|
327
|
+
var PKG_VERSION4 = "^4.3.0";
|
|
328
|
+
var REALTIME_PROVIDERS2 = {
|
|
329
|
+
realtime: {
|
|
330
|
+
packageName: "@kuralle-syrinx/realtime",
|
|
331
|
+
packageVersion: PKG_VERSION4,
|
|
332
|
+
envKeys: [{ key: "OPENAI_API_KEY", required: true }],
|
|
333
|
+
importLines: [`import { fromOpenAIRealtime } from "@kuralle-syrinx/realtime";`],
|
|
334
|
+
adapterExpr: (envRef2, socketFactoryIdent) => `fromOpenAIRealtime({ apiKey: ${envRef2("OPENAI_API_KEY")}, socketFactory: ${socketFactoryIdent} })`
|
|
335
|
+
},
|
|
336
|
+
grok: {
|
|
337
|
+
packageName: "@kuralle-syrinx/grok",
|
|
338
|
+
packageVersion: PKG_VERSION4,
|
|
339
|
+
envKeys: [{ key: "XAI_API_KEY", required: true }],
|
|
340
|
+
importLines: [`import { fromGrokRealtime } from "@kuralle-syrinx/grok/realtime";`],
|
|
341
|
+
adapterExpr: (envRef2, socketFactoryIdent) => `fromGrokRealtime({ apiKey: ${envRef2("XAI_API_KEY")}, socketFactory: ${socketFactoryIdent} })`
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/providers/vad-endpointing.ts
|
|
346
|
+
var PKG_VERSION5 = "^4.3.0";
|
|
347
|
+
var VAD_SIDECAR_PROVIDERS = {
|
|
348
|
+
// packages/silero-vad/src/index.ts: SileroVADPlugin. No API key — a bundled ONNX model.
|
|
349
|
+
"silero-vad": {
|
|
350
|
+
packageName: "@kuralle-syrinx/silero-vad",
|
|
351
|
+
packageVersion: PKG_VERSION5,
|
|
352
|
+
className: "SileroVADPlugin",
|
|
353
|
+
slot: "vad",
|
|
354
|
+
envKeys: [],
|
|
355
|
+
configFields: () => []
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
var ENDPOINTING_SIDECAR_PROVIDERS = {
|
|
359
|
+
// packages/pipecat-smart-turn/src/index.ts: PipecatEOSPlugin. No API key — a bundled ONNX model.
|
|
360
|
+
"pipecat-smart-turn": {
|
|
361
|
+
packageName: "@kuralle-syrinx/pipecat-smart-turn",
|
|
362
|
+
packageVersion: PKG_VERSION5,
|
|
363
|
+
className: "PipecatEOSPlugin",
|
|
364
|
+
slot: "eos",
|
|
365
|
+
envKeys: [],
|
|
366
|
+
configFields: () => []
|
|
367
|
+
},
|
|
368
|
+
vap: void 0
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// src/templates/agent-module.ts
|
|
372
|
+
var SYSTEM_PROMPT = "You are a helpful voice assistant. Keep your replies short.";
|
|
373
|
+
function nodeEnvRef(key) {
|
|
374
|
+
return `process.env["${key}"] ?? ""`;
|
|
375
|
+
}
|
|
376
|
+
function requireStage(provider, flag, value) {
|
|
377
|
+
if (provider === void 0) {
|
|
378
|
+
throw new CliError(EXIT_CODES.USAGE, `--${flag} ${value} is not yet implemented by this generator`);
|
|
379
|
+
}
|
|
380
|
+
return provider;
|
|
381
|
+
}
|
|
382
|
+
function importLine(className, from) {
|
|
383
|
+
return `import { ${className} } from "${from}";`;
|
|
384
|
+
}
|
|
385
|
+
function mergeImports(lines) {
|
|
386
|
+
const IMPORT_RE = /^import \{ (.+) \} from "(.+)";$/;
|
|
387
|
+
const sourceOrder = [];
|
|
388
|
+
const namesBySource = /* @__PURE__ */ new Map();
|
|
389
|
+
for (const line of lines) {
|
|
390
|
+
const match = IMPORT_RE.exec(line);
|
|
391
|
+
const names = match?.[1];
|
|
392
|
+
const from = match?.[2];
|
|
393
|
+
if (names === void 0 || from === void 0) continue;
|
|
394
|
+
let bucket = namesBySource.get(from);
|
|
395
|
+
if (bucket === void 0) {
|
|
396
|
+
bucket = [];
|
|
397
|
+
namesBySource.set(from, bucket);
|
|
398
|
+
sourceOrder.push(from);
|
|
399
|
+
}
|
|
400
|
+
for (const name of names.split(",").map((n) => n.trim())) {
|
|
401
|
+
if (!bucket.includes(name)) bucket.push(name);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return sourceOrder.map((from) => `import { ${(namesBySource.get(from) ?? []).join(", ")} } from "${from}";`);
|
|
405
|
+
}
|
|
406
|
+
function buildAgentModule(opts) {
|
|
407
|
+
if (opts.mode === "realtime") return buildRealtimeAgentModule(opts);
|
|
408
|
+
return buildCascadeAgentModule(opts);
|
|
409
|
+
}
|
|
410
|
+
function buildCascadeAgentModule(opts) {
|
|
411
|
+
const stt = requireStage(STT_STAGE_PROVIDERS[opts.stt], "stt", opts.stt);
|
|
412
|
+
const tts = requireStage(TTS_STAGE_PROVIDERS[opts.tts], "tts", opts.tts);
|
|
413
|
+
const reasoner = REASONER_PROVIDERS2[opts.reasoner];
|
|
414
|
+
if (reasoner === void 0) {
|
|
415
|
+
throw new CliError(EXIT_CODES.USAGE, `--reasoner ${opts.reasoner} is not yet implemented by this generator`);
|
|
416
|
+
}
|
|
417
|
+
const vad = opts.vad !== void 0 ? VAD_SIDECAR_PROVIDERS[opts.vad] : void 0;
|
|
418
|
+
if (opts.vad !== void 0 && vad === void 0) {
|
|
419
|
+
throw new CliError(EXIT_CODES.USAGE, `--vad ${opts.vad} is not yet implemented by this generator`);
|
|
420
|
+
}
|
|
421
|
+
const eos = opts.endpointing !== void 0 ? ENDPOINTING_SIDECAR_PROVIDERS[opts.endpointing] : void 0;
|
|
422
|
+
if (opts.endpointing !== void 0 && eos === void 0) {
|
|
423
|
+
throw new CliError(EXIT_CODES.USAGE, `--endpointing ${opts.endpointing} is not yet implemented by this generator`);
|
|
424
|
+
}
|
|
425
|
+
const endpointingOwner = eos !== void 0 ? "smart_turn" : stt.ownsEndpointing ? "provider_stt" : void 0;
|
|
426
|
+
if (endpointingOwner === void 0) {
|
|
427
|
+
throw new CliError(
|
|
428
|
+
EXIT_CODES.USAGE,
|
|
429
|
+
`--stt ${opts.stt} cannot own endpointing on its own \u2014 pass --endpointing pipecat-smart-turn`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
const imports = [
|
|
433
|
+
`import { VoiceAgentSession, type VoicePlugin } from "@kuralle-syrinx/core";`,
|
|
434
|
+
`import { ReasoningBridge } from "@kuralle-syrinx/aisdk";`,
|
|
435
|
+
importLine(stt.className, stt.importFrom ?? stt.packageName),
|
|
436
|
+
importLine(tts.className, tts.importFrom ?? tts.packageName),
|
|
437
|
+
...vad !== void 0 ? [importLine(vad.className, vad.packageName)] : [],
|
|
438
|
+
...eos !== void 0 ? [importLine(eos.className, eos.packageName)] : [],
|
|
439
|
+
...reasoner.importLines(nodeEnvRef)
|
|
440
|
+
];
|
|
441
|
+
const pluginConfigLines = [
|
|
442
|
+
` stt: {`,
|
|
443
|
+
...stt.configFields(nodeEnvRef).map((f) => ` ${f},`),
|
|
444
|
+
` },`
|
|
445
|
+
];
|
|
446
|
+
if (vad !== void 0) pluginConfigLines.push(` vad: {},`);
|
|
447
|
+
if (eos !== void 0) pluginConfigLines.push(` eos: {},`);
|
|
448
|
+
pluginConfigLines.push(
|
|
449
|
+
` bridge: {},`,
|
|
450
|
+
` tts: {`,
|
|
451
|
+
...tts.configFields(nodeEnvRef).map((f) => ` ${f},`),
|
|
452
|
+
` },`
|
|
453
|
+
);
|
|
454
|
+
const pluginInstanceLines = [` stt: new ${stt.className}(),`];
|
|
455
|
+
if (vad !== void 0) pluginInstanceLines.push(` vad: new ${vad.className}(),`);
|
|
456
|
+
if (eos !== void 0) pluginInstanceLines.push(` eos: new ${eos.className}(),`);
|
|
457
|
+
pluginInstanceLines.push(
|
|
458
|
+
` bridge: new ReasoningBridge(${reasoner.reasonerExpr}),`,
|
|
459
|
+
` tts: new ${tts.className}(),`
|
|
460
|
+
);
|
|
461
|
+
return `// SPDX-License-Identifier: MIT
|
|
462
|
+
//
|
|
463
|
+
// Cascade voice agent: ${opts.stt} STT -> ${opts.reasoner} reasoner -> ${opts.tts} TTS.
|
|
464
|
+
// Generated by create-syrinx-agent. Wired to the local dev server and to
|
|
465
|
+
// \`syrinx turn\`/\`syrinx text\` through the --agent <module>#<export> seam
|
|
466
|
+
// (examples/02-hello-voice-headless/scripts/dev-server.ts).
|
|
467
|
+
|
|
468
|
+
${mergeImports(imports).join("\n")}
|
|
469
|
+
|
|
470
|
+
const ${SYSTEM_PROMPT_CONST} = ${JSON.stringify(SYSTEM_PROMPT)};
|
|
471
|
+
|
|
472
|
+
export function createAgent(): VoiceAgentSession {
|
|
473
|
+
${reasoner.preludeLines(nodeEnvRef).map((l) => ` ${l}`).join("\n")}
|
|
474
|
+
|
|
475
|
+
const session = new VoiceAgentSession({
|
|
476
|
+
plugins: {
|
|
477
|
+
${pluginConfigLines.join("\n")}
|
|
478
|
+
},
|
|
479
|
+
endpointingOwner: "${endpointingOwner}",
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const plugins: Record<string, VoicePlugin> = {
|
|
483
|
+
${pluginInstanceLines.join("\n")}
|
|
484
|
+
};
|
|
485
|
+
for (const [name, plugin] of Object.entries(plugins)) {
|
|
486
|
+
session.registerPlugin(name, plugin);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return session;
|
|
490
|
+
}
|
|
491
|
+
`;
|
|
492
|
+
}
|
|
493
|
+
function buildRealtimeAgentModule(opts) {
|
|
494
|
+
const realtime = REALTIME_PROVIDERS2[opts.realtime];
|
|
495
|
+
if (realtime === void 0) {
|
|
496
|
+
throw new CliError(EXIT_CODES.USAGE, `--realtime ${opts.realtime} is not yet implemented by this generator`);
|
|
497
|
+
}
|
|
498
|
+
const imports = [
|
|
499
|
+
`import { VoiceAgentSession } from "@kuralle-syrinx/core";`,
|
|
500
|
+
`import { RealtimeBridge } from "@kuralle-syrinx/realtime";`,
|
|
501
|
+
`import { createNodeWsSocket } from "@kuralle-syrinx/ws/node";`,
|
|
502
|
+
...realtime.importLines
|
|
503
|
+
];
|
|
504
|
+
return `// SPDX-License-Identifier: MIT
|
|
505
|
+
//
|
|
506
|
+
// Speech-to-speech voice agent: ${opts.realtime} realtime. Generated by
|
|
507
|
+
// create-syrinx-agent. Wired to the local dev server and to
|
|
508
|
+
// \`syrinx turn\`/\`syrinx text\` through the --agent <module>#<export> seam
|
|
509
|
+
// (examples/02-hello-voice-headless/scripts/dev-server.ts).
|
|
510
|
+
|
|
511
|
+
${mergeImports(imports).join("\n")}
|
|
512
|
+
|
|
513
|
+
export function createAgent(): VoiceAgentSession {
|
|
514
|
+
const adapter = ${realtime.adapterExpr(nodeEnvRef, "createNodeWsSocket")};
|
|
515
|
+
const bridge = new RealtimeBridge(adapter);
|
|
516
|
+
|
|
517
|
+
const session = new VoiceAgentSession({
|
|
518
|
+
plugins: { realtime: {} },
|
|
519
|
+
endpointingOwner: "timer",
|
|
520
|
+
});
|
|
521
|
+
session.registerPlugin("realtime", bridge);
|
|
522
|
+
|
|
523
|
+
return session;
|
|
524
|
+
}
|
|
525
|
+
`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/templates/dev-server.ts
|
|
529
|
+
var TRANSPORT_SERVERS = {
|
|
530
|
+
browser: { factoryName: "createVoiceWebSocketServer", extraOptions: [`path: "/ws"`], urlPath: "/ws" },
|
|
531
|
+
twilio: { factoryName: "createTwilioMediaStreamServer", extraOptions: [], urlPath: "/" },
|
|
532
|
+
telnyx: { factoryName: "createTelnyxMediaStreamServer", extraOptions: [], urlPath: "/" },
|
|
533
|
+
smartpbx: { factoryName: "createSmartPbxMediaStreamServer", extraOptions: [], urlPath: "/" }
|
|
534
|
+
};
|
|
535
|
+
function buildDevServer(transport) {
|
|
536
|
+
const binding = TRANSPORT_SERVERS[transport];
|
|
537
|
+
const optionsLines = [` port,`, ` host,`, ...binding.extraOptions.map((l) => ` ${l},`), ` createSession: () => factory(),`];
|
|
538
|
+
return `// SPDX-License-Identifier: MIT
|
|
539
|
+
//
|
|
540
|
+
// Local dev server \u2014 wires YOUR agent module to a ${transport} transport host.
|
|
541
|
+
//
|
|
542
|
+
// pnpm dev -- --agent ./src/agent.ts#createAgent
|
|
543
|
+
//
|
|
544
|
+
// --agent is <module>[#namedExport]; the export is a zero-arg factory
|
|
545
|
+
// returning a VoiceAgentSession. Same seam as
|
|
546
|
+
// examples/02-hello-voice-headless/scripts/dev-server.ts.
|
|
547
|
+
|
|
548
|
+
import { isAbsolute, resolve } from "node:path";
|
|
549
|
+
import { pathToFileURL } from "node:url";
|
|
550
|
+
|
|
551
|
+
import { ${binding.factoryName}, installGracefulShutdown } from "@kuralle-syrinx/server-websocket";
|
|
552
|
+
import type { VoiceAgentSession } from "@kuralle-syrinx/core";
|
|
553
|
+
|
|
554
|
+
export type SessionFactory = () => VoiceAgentSession | Promise<VoiceAgentSession>;
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Resolve \`--agent <module>[#export]\` to a factory. Fails loudly and
|
|
558
|
+
* specifically \u2014 a dev server that silently falls back to a different agent
|
|
559
|
+
* than the one you asked for is worse than one that refuses to start.
|
|
560
|
+
*/
|
|
561
|
+
async function resolveAgentFactory(spec: string | undefined): Promise<{ factory: SessionFactory; label: string }> {
|
|
562
|
+
if (!spec) throw new Error("--agent <module>[#export] is required, e.g. --agent ./src/agent.ts#createAgent");
|
|
563
|
+
const [modulePath, exportName] = spec.split("#");
|
|
564
|
+
if (!modulePath) throw new Error(\`--agent needs a module path, got: \${spec}\`);
|
|
565
|
+
const abs = isAbsolute(modulePath) ? modulePath : resolve(process.cwd(), modulePath);
|
|
566
|
+
const mod = (await import(pathToFileURL(abs).href)) as Record<string, unknown>;
|
|
567
|
+
|
|
568
|
+
const picked = exportName ? mod[exportName] : (mod["default"] ?? mod["createAgent"] ?? mod["createSession"]);
|
|
569
|
+
if (typeof picked !== "function") {
|
|
570
|
+
const available = Object.keys(mod).filter((k) => typeof mod[k] === "function");
|
|
571
|
+
throw new Error(
|
|
572
|
+
\`\${abs} has no callable export \${exportName ? \`"\${exportName}"\` : "(default, createAgent, or createSession)"}. \` +
|
|
573
|
+
\`Callable exports: \${available.length > 0 ? available.join(", ") : "(none)"}\`,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
return { factory: picked as SessionFactory, label: \`\${modulePath}\${exportName ? \`#\${exportName}\` : ""}\` };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function flag(argv: readonly string[], name: string): string | undefined {
|
|
580
|
+
const i = argv.indexOf(name);
|
|
581
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function readPort(): number {
|
|
585
|
+
const raw = process.env["SYRINX_DEV_PORT"]?.trim();
|
|
586
|
+
if (!raw) return 4173;
|
|
587
|
+
const port = Number.parseInt(raw, 10);
|
|
588
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535) throw new Error(\`invalid SYRINX_DEV_PORT: \${raw}\`);
|
|
589
|
+
return port;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function main(): Promise<void> {
|
|
593
|
+
const { factory, label } = await resolveAgentFactory(flag(process.argv.slice(2), "--agent"));
|
|
594
|
+
const port = readPort();
|
|
595
|
+
const host = process.env["SYRINX_DEV_HOST"]?.trim() || "127.0.0.1";
|
|
596
|
+
|
|
597
|
+
const server = await ${binding.factoryName}({
|
|
598
|
+
${optionsLines.join("\n")}
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const address = server.address();
|
|
602
|
+
if (!address || typeof address === "string") throw new Error("expected TCP server address");
|
|
603
|
+
console.log(\`Syrinx dev server (${transport}): ws://\${host}:\${String(address.port)}${binding.urlPath}\`);
|
|
604
|
+
console.log(\`Agent: \${label}\`);
|
|
605
|
+
|
|
606
|
+
installGracefulShutdown(server, { drainDeadlineMs: 10_000, onClosed: () => process.exit(0) });
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
void main().catch((err: unknown) => {
|
|
610
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
611
|
+
process.exit(1);
|
|
612
|
+
});
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/templates/package-json.ts
|
|
617
|
+
var CORE_VERSION = "^4.3.0";
|
|
618
|
+
function transportPackage() {
|
|
619
|
+
return { "@kuralle-syrinx/server-websocket": CORE_VERSION, ws: "^8.21.0" };
|
|
620
|
+
}
|
|
621
|
+
function collectDependencies(opts) {
|
|
622
|
+
const deps = {
|
|
623
|
+
"@kuralle-syrinx/core": CORE_VERSION,
|
|
624
|
+
...transportPackage()
|
|
625
|
+
};
|
|
626
|
+
if (opts.mode === "cascade") {
|
|
627
|
+
const stt = STT_STAGE_PROVIDERS[opts.stt];
|
|
628
|
+
const tts = TTS_STAGE_PROVIDERS[opts.tts];
|
|
629
|
+
const reasoner = REASONER_PROVIDERS2[opts.reasoner];
|
|
630
|
+
if (stt) deps[stt.packageName] = stt.packageVersion;
|
|
631
|
+
if (tts) deps[tts.packageName] = tts.packageVersion;
|
|
632
|
+
deps["@kuralle-syrinx/aisdk"] = CORE_VERSION;
|
|
633
|
+
if (reasoner) {
|
|
634
|
+
deps[reasoner.packageName] = reasoner.packageVersion;
|
|
635
|
+
Object.assign(deps, reasoner.extraPackages);
|
|
636
|
+
}
|
|
637
|
+
const vad = opts.vad !== void 0 ? VAD_SIDECAR_PROVIDERS[opts.vad] : void 0;
|
|
638
|
+
if (vad) deps[vad.packageName] = vad.packageVersion;
|
|
639
|
+
const eos = opts.endpointing !== void 0 ? ENDPOINTING_SIDECAR_PROVIDERS[opts.endpointing] : void 0;
|
|
640
|
+
if (eos) deps[eos.packageName] = eos.packageVersion;
|
|
641
|
+
} else {
|
|
642
|
+
const realtime = REALTIME_PROVIDERS2[opts.realtime];
|
|
643
|
+
deps["@kuralle-syrinx/realtime"] = CORE_VERSION;
|
|
644
|
+
deps["@kuralle-syrinx/ws"] = CORE_VERSION;
|
|
645
|
+
if (realtime) deps[realtime.packageName] = realtime.packageVersion;
|
|
646
|
+
}
|
|
647
|
+
return deps;
|
|
648
|
+
}
|
|
649
|
+
function buildPackageJson(opts) {
|
|
650
|
+
const dependencies = collectDependencies(opts);
|
|
651
|
+
const agentSpec = "./src/agent.ts#createAgent";
|
|
652
|
+
const scripts = {
|
|
653
|
+
dev: `tsx scripts/dev-server.ts --agent ${agentSpec}`,
|
|
654
|
+
"check:typecheck": "tsc --noEmit",
|
|
655
|
+
"check:turn": `syrinx turn --in test/fixtures/smoke.wav --agent ${agentSpec} --json`
|
|
656
|
+
};
|
|
657
|
+
const devDependencies = {
|
|
658
|
+
"@types/node": "^22.0.0",
|
|
659
|
+
"@types/ws": "^8.18.1",
|
|
660
|
+
tsx: "^4.20.0",
|
|
661
|
+
typescript: "^5.7.0"
|
|
662
|
+
};
|
|
663
|
+
if (opts.runtime === "cloudflare") {
|
|
664
|
+
dependencies["@kuralle-syrinx/cf-agents"] = CORE_VERSION;
|
|
665
|
+
dependencies["agents"] = "0.14.0";
|
|
666
|
+
dependencies["ai"] = "^6.0.0";
|
|
667
|
+
devDependencies["@cloudflare/workers-types"] = "^4.20260601.1";
|
|
668
|
+
devDependencies["wrangler"] = "4.97.0";
|
|
669
|
+
scripts["check:wrangler-dry-run"] = "wrangler deploy --dry-run";
|
|
670
|
+
}
|
|
671
|
+
const pkg = {
|
|
672
|
+
name: opts.name,
|
|
673
|
+
version: "0.0.1",
|
|
674
|
+
private: true,
|
|
675
|
+
type: "module",
|
|
676
|
+
license: "MIT",
|
|
677
|
+
scripts,
|
|
678
|
+
dependencies: sortKeys({ ...dependencies, "@kuralle-syrinx/cli": CORE_VERSION }),
|
|
679
|
+
devDependencies: sortKeys(devDependencies)
|
|
680
|
+
};
|
|
681
|
+
return `${JSON.stringify(pkg, null, 2)}
|
|
682
|
+
`;
|
|
683
|
+
}
|
|
684
|
+
function sortKeys(obj) {
|
|
685
|
+
const out = {};
|
|
686
|
+
for (const key of Object.keys(obj).sort()) out[key] = obj[key];
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/templates/env-example.ts
|
|
691
|
+
function collectEnvKeys(opts) {
|
|
692
|
+
const keys = [];
|
|
693
|
+
const seen = /* @__PURE__ */ new Set();
|
|
694
|
+
const add = (specs) => {
|
|
695
|
+
for (const spec of specs) {
|
|
696
|
+
if (seen.has(spec.key)) continue;
|
|
697
|
+
seen.add(spec.key);
|
|
698
|
+
keys.push(spec);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
if (opts.mode === "cascade") {
|
|
702
|
+
const stt = STT_STAGE_PROVIDERS[opts.stt];
|
|
703
|
+
const tts = TTS_STAGE_PROVIDERS[opts.tts];
|
|
704
|
+
const reasoner = REASONER_PROVIDERS2[opts.reasoner];
|
|
705
|
+
if (stt) add(stt.envKeys);
|
|
706
|
+
if (tts) add(tts.envKeys);
|
|
707
|
+
if (reasoner) add(reasoner.envKeys);
|
|
708
|
+
const vad = opts.vad !== void 0 ? VAD_SIDECAR_PROVIDERS[opts.vad] : void 0;
|
|
709
|
+
if (vad) add(vad.envKeys);
|
|
710
|
+
const eos = opts.endpointing !== void 0 ? ENDPOINTING_SIDECAR_PROVIDERS[opts.endpointing] : void 0;
|
|
711
|
+
if (eos) add(eos.envKeys);
|
|
712
|
+
} else {
|
|
713
|
+
const realtime = REALTIME_PROVIDERS2[opts.realtime];
|
|
714
|
+
if (realtime) add(realtime.envKeys);
|
|
715
|
+
}
|
|
716
|
+
return keys;
|
|
717
|
+
}
|
|
718
|
+
function buildEnvExample(opts) {
|
|
719
|
+
const keys = collectEnvKeys(opts);
|
|
720
|
+
const lines = [
|
|
721
|
+
"# Generated by create-syrinx-agent \u2014 exactly the keys this combination needs.",
|
|
722
|
+
"# Copy to .env and fill in.",
|
|
723
|
+
"",
|
|
724
|
+
...keys.map((k) => `${k.key}=`),
|
|
725
|
+
"",
|
|
726
|
+
"# Optional \u2014 local dev server bind (defaults: 127.0.0.1:4173)",
|
|
727
|
+
"SYRINX_DEV_PORT=",
|
|
728
|
+
"SYRINX_DEV_HOST=",
|
|
729
|
+
""
|
|
730
|
+
];
|
|
731
|
+
return `${lines.join("\n")}
|
|
732
|
+
`;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/templates/agents-md.ts
|
|
736
|
+
function buildAgentsMd(opts) {
|
|
737
|
+
const pipelineLine = opts.mode === "cascade" ? `${opts.stt} STT -> ${opts.reasoner} reasoner -> ${opts.tts} TTS (cascade)` : `${opts.realtime} realtime (speech-to-speech, no separate STT/TTS stages)`;
|
|
738
|
+
return `# AGENTS.md
|
|
739
|
+
|
|
740
|
+
This project was generated by \`create-syrinx-agent\`.
|
|
741
|
+
|
|
742
|
+
Pipeline: ${pipelineLine}
|
|
743
|
+
Transport: ${opts.transport} \xB7 Runtime: ${opts.runtime}
|
|
744
|
+
|
|
745
|
+
## What a coding agent CAN verify here
|
|
746
|
+
|
|
747
|
+
- \`pnpm check:typecheck\` (\`tsc --noEmit\`) \u2014 the agent module and dev server compile.
|
|
748
|
+
- \`pnpm check:turn\` \u2014 runs \`syrinx turn --in test/fixtures/smoke.wav --agent ./src/agent.ts#createAgent --json\`.
|
|
749
|
+
Given a fixture **with** a recorded expected transcript (a \`.json\` sidecar produced by the
|
|
750
|
+
Studio's "Save as fixture"), this asserts the replayed transcript matches and **exits non-zero
|
|
751
|
+
on drift** \u2014 a real regression check, not just a smoke test. The bundled \`test/fixtures/smoke.wav\`
|
|
752
|
+
ships with no sidecar, so as generated this only proves the pipeline runs end-to-end and produces
|
|
753
|
+
*some* transcript/reply/audio; record your own fixture with the Studio for an assertion that
|
|
754
|
+
actually catches regressions.
|
|
755
|
+
- Exit codes and JSON output from \`syrinx turn\` / \`syrinx text\` \u2014 parseable, deterministic,
|
|
756
|
+
scriptable.
|
|
757
|
+
|
|
758
|
+
Both checks need real provider credentials in \`.env\` (see \`.env.example\`) \u2014 without them
|
|
759
|
+
\`check:turn\` fails at the provider call, not at a code defect.
|
|
760
|
+
|
|
761
|
+
## What a coding agent CANNOT verify here
|
|
762
|
+
|
|
763
|
+
A coding agent can assert transcripts, exit codes, and latency numbers. It **cannot** judge:
|
|
764
|
+
|
|
765
|
+
- Whether barge-in feels natural (the timing a human perceives as responsive vs. jarring).
|
|
766
|
+
- Whether a pause reads as "thinking" or as a hang \u2014 same latency number, different human read.
|
|
767
|
+
- Whether a synthesized voice sounds warm, robotic, or off \u2014 TTS quality is a perceptual judgment.
|
|
768
|
+
|
|
769
|
+
Those need a human in the Syrinx Studio, listening. Do not treat a green \`check:turn\` as proof the
|
|
770
|
+
agent is pleasant to talk to \u2014 it only proves the transcript matched (or that the pipeline ran, for
|
|
771
|
+
the unsigned bundled fixture).
|
|
772
|
+
|
|
773
|
+
## \`tsx\` vs plain \`node\`
|
|
774
|
+
|
|
775
|
+
\`--agent\` pointed at a \`.ts\` module (as in the scripts above) needs \`tsx\` \u2014 Node's built-in type
|
|
776
|
+
stripping does not do TypeScript's \`.js\`-import-resolves-to-\`.ts\` remapping that this project's
|
|
777
|
+
imports rely on. Built JS (after a bundler step) runs fine under plain \`node\`. Every script this
|
|
778
|
+
generator wrote already invokes \`tsx\`; if you write your own, do the same for a \`.ts\` --agent target.
|
|
779
|
+
|
|
780
|
+
## Layout
|
|
781
|
+
|
|
782
|
+
- \`src/agent.ts\` \u2014 \`createAgent()\`, the \`--agent\` seam's factory export.
|
|
783
|
+
- \`scripts/dev-server.ts\` \u2014 local dev server (\`pnpm dev\`), wraps \`createAgent()\` in a
|
|
784
|
+
${opts.transport} transport host.
|
|
785
|
+
- \`test/fixtures/smoke.wav\` \u2014 bundled silence fixture for \`check:turn\`.
|
|
786
|
+
- \`.env.example\` \u2014 exactly the provider keys this combination needs.
|
|
787
|
+
`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/templates/tsconfig.ts
|
|
791
|
+
function buildTsconfig(runtime) {
|
|
792
|
+
const types = runtime === "cloudflare" ? ["node", "@cloudflare/workers-types"] : ["node"];
|
|
793
|
+
const tsconfig = {
|
|
794
|
+
compilerOptions: {
|
|
795
|
+
target: "ES2022",
|
|
796
|
+
module: "NodeNext",
|
|
797
|
+
moduleResolution: "NodeNext",
|
|
798
|
+
strict: true,
|
|
799
|
+
skipLibCheck: true,
|
|
800
|
+
noEmit: true,
|
|
801
|
+
types
|
|
802
|
+
},
|
|
803
|
+
include: ["src/**/*.ts", "scripts/**/*.ts"]
|
|
804
|
+
};
|
|
805
|
+
return `${JSON.stringify(tsconfig, null, 2)}
|
|
806
|
+
`;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/templates/fixture.ts
|
|
810
|
+
var SAMPLE_RATE = 16e3;
|
|
811
|
+
var DURATION_SECONDS = 0.5;
|
|
812
|
+
var CHANNELS = 1;
|
|
813
|
+
var BITS_PER_SAMPLE = 16;
|
|
814
|
+
function buildSmokeWav() {
|
|
815
|
+
const numSamples = Math.floor(SAMPLE_RATE * DURATION_SECONDS);
|
|
816
|
+
const dataBytes = numSamples * CHANNELS * (BITS_PER_SAMPLE / 8);
|
|
817
|
+
const buffer = Buffer.alloc(44 + dataBytes);
|
|
818
|
+
buffer.write("RIFF", 0, "ascii");
|
|
819
|
+
buffer.writeUInt32LE(36 + dataBytes, 4);
|
|
820
|
+
buffer.write("WAVE", 8, "ascii");
|
|
821
|
+
buffer.write("fmt ", 12, "ascii");
|
|
822
|
+
buffer.writeUInt32LE(16, 16);
|
|
823
|
+
buffer.writeUInt16LE(1, 20);
|
|
824
|
+
buffer.writeUInt16LE(CHANNELS, 22);
|
|
825
|
+
buffer.writeUInt32LE(SAMPLE_RATE, 24);
|
|
826
|
+
buffer.writeUInt32LE(SAMPLE_RATE * CHANNELS * (BITS_PER_SAMPLE / 8), 28);
|
|
827
|
+
buffer.writeUInt16LE(CHANNELS * (BITS_PER_SAMPLE / 8), 32);
|
|
828
|
+
buffer.writeUInt16LE(BITS_PER_SAMPLE, 34);
|
|
829
|
+
buffer.write("data", 36, "ascii");
|
|
830
|
+
buffer.writeUInt32LE(dataBytes, 40);
|
|
831
|
+
return buffer;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/templates/cloudflare-worker.ts
|
|
835
|
+
var SYSTEM_PROMPT2 = "You are a helpful voice assistant. Keep your replies short.";
|
|
836
|
+
function envRef(key) {
|
|
837
|
+
return `env.${key}`;
|
|
838
|
+
}
|
|
839
|
+
function importLine2(className, from) {
|
|
840
|
+
return `import { ${className} } from "${from}";`;
|
|
841
|
+
}
|
|
842
|
+
function requireStage2(provider, flag, value) {
|
|
843
|
+
if (provider === void 0) {
|
|
844
|
+
throw new CliError(EXIT_CODES.USAGE, `--${flag} ${value} is not yet implemented by this generator`);
|
|
845
|
+
}
|
|
846
|
+
return provider;
|
|
847
|
+
}
|
|
848
|
+
function cfTransport(transport) {
|
|
849
|
+
if (transport === "browser") return "edge";
|
|
850
|
+
if (transport === "twilio" || transport === "telnyx") return transport;
|
|
851
|
+
throw new CliError(
|
|
852
|
+
EXIT_CODES.USAGE,
|
|
853
|
+
`--runtime cloudflare has no ${transport} transport binding in @kuralle-syrinx/cf-agents (only edge/twilio/telnyx) \u2014 use --transport browser, twilio, or telnyx`
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
function envInterface(keys) {
|
|
857
|
+
const lines = keys.map((k) => ` readonly ${k.key}: string;`);
|
|
858
|
+
return `interface Env extends Record<string, unknown> {
|
|
859
|
+
${lines.join("\n")}
|
|
860
|
+
}`;
|
|
861
|
+
}
|
|
862
|
+
function buildCloudflareWorkerSource(opts) {
|
|
863
|
+
const transport = cfTransport(opts.transport);
|
|
864
|
+
const envKeys = collectEnvKeys(opts);
|
|
865
|
+
if (opts.mode === "cascade") {
|
|
866
|
+
if (opts.vad !== void 0 || opts.endpointing !== void 0) {
|
|
867
|
+
throw new CliError(
|
|
868
|
+
EXIT_CODES.USAGE,
|
|
869
|
+
"--vad/--endpointing are not yet implemented for --runtime cloudflare by this generator"
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
const stt = requireStage2(STT_STAGE_PROVIDERS[opts.stt], "stt", opts.stt);
|
|
873
|
+
const tts = requireStage2(TTS_STAGE_PROVIDERS[opts.tts], "tts", opts.tts);
|
|
874
|
+
const reasoner = REASONER_PROVIDERS2[opts.reasoner];
|
|
875
|
+
if (reasoner === void 0) {
|
|
876
|
+
throw new CliError(EXIT_CODES.USAGE, `--reasoner ${opts.reasoner} is not yet implemented by this generator`);
|
|
877
|
+
}
|
|
878
|
+
const sttNew = stt.usesSocketFactory ? `new ${stt.className}(createWorkersSocket)` : `new ${stt.className}()`;
|
|
879
|
+
const ttsNew = tts.usesSocketFactory ? `new ${tts.className}(createWorkersSocket)` : `new ${tts.className}()`;
|
|
880
|
+
const needsWorkersSocket = stt.usesSocketFactory || tts.usesSocketFactory;
|
|
881
|
+
const imports2 = [
|
|
882
|
+
`import { Agent, routeAgentRequest } from "agents";`,
|
|
883
|
+
`import { withVoice } from "@kuralle-syrinx/cf-agents";`,
|
|
884
|
+
importLine2(stt.className, stt.importFrom ?? stt.packageName),
|
|
885
|
+
importLine2(tts.className, tts.importFrom ?? tts.packageName),
|
|
886
|
+
...needsWorkersSocket ? [`import { createWorkersSocket } from "@kuralle-syrinx/ws/workers";`] : [],
|
|
887
|
+
...reasoner.importLines(envRef)
|
|
888
|
+
];
|
|
889
|
+
return `// SPDX-License-Identifier: MIT
|
|
890
|
+
//
|
|
891
|
+
// Cascade voice agent on Cloudflare Workers: ${opts.stt} STT -> ${opts.reasoner} reasoner -> ${opts.tts} TTS.
|
|
892
|
+
// Generated by create-syrinx-agent. Mirrors examples/03-cf-agent-voice and
|
|
893
|
+
// packages/cf-agents/README.md.
|
|
894
|
+
|
|
895
|
+
${imports2.join("\n")}
|
|
896
|
+
|
|
897
|
+
${envInterface(envKeys)}
|
|
898
|
+
|
|
899
|
+
const ${SYSTEM_PROMPT_CONST} = ${JSON.stringify(SYSTEM_PROMPT2)};
|
|
900
|
+
|
|
901
|
+
export class VoiceAgent extends withVoice<Env, typeof Agent<Env>>(Agent<Env>, {
|
|
902
|
+
transport: "${transport}",
|
|
903
|
+
pipeline: {
|
|
904
|
+
kind: "cascaded",
|
|
905
|
+
stt: (env) => ({
|
|
906
|
+
plugin: ${sttNew},
|
|
907
|
+
config: {
|
|
908
|
+
${stt.configFields(envRef).map((f) => ` ${f},`).join("\n")}
|
|
909
|
+
},
|
|
910
|
+
}),
|
|
911
|
+
tts: (env) => ({
|
|
912
|
+
plugin: ${ttsNew},
|
|
913
|
+
config: {
|
|
914
|
+
${tts.configFields(envRef).map((f) => ` ${f},`).join("\n")}
|
|
915
|
+
},
|
|
916
|
+
}),
|
|
917
|
+
},
|
|
918
|
+
reasoner: (env) => {
|
|
919
|
+
${reasoner.preludeLines(envRef).map((l) => ` ${l}`).join("\n")}
|
|
920
|
+
return ${reasoner.reasonerExpr};
|
|
921
|
+
},
|
|
922
|
+
}) {}
|
|
923
|
+
|
|
924
|
+
export default {
|
|
925
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
926
|
+
return (await routeAgentRequest(request, env)) ?? new Response("Not found", { status: 404 });
|
|
927
|
+
},
|
|
928
|
+
};
|
|
929
|
+
`;
|
|
930
|
+
}
|
|
931
|
+
const realtime = REALTIME_PROVIDERS2[opts.realtime];
|
|
932
|
+
if (realtime === void 0) {
|
|
933
|
+
throw new CliError(EXIT_CODES.USAGE, `--realtime ${opts.realtime} is not yet implemented by this generator`);
|
|
934
|
+
}
|
|
935
|
+
const imports = [
|
|
936
|
+
`import { Agent, routeAgentRequest } from "agents";`,
|
|
937
|
+
`import { withVoice } from "@kuralle-syrinx/cf-agents";`,
|
|
938
|
+
`import { createWorkersSocket } from "@kuralle-syrinx/ws/workers";`,
|
|
939
|
+
...realtime.importLines
|
|
940
|
+
];
|
|
941
|
+
return `// SPDX-License-Identifier: MIT
|
|
942
|
+
//
|
|
943
|
+
// Speech-to-speech voice agent on Cloudflare Workers: ${opts.realtime} realtime.
|
|
944
|
+
// Generated by create-syrinx-agent. Mirrors examples/03-cf-agent-voice and
|
|
945
|
+
// packages/cf-agents/README.md.
|
|
946
|
+
|
|
947
|
+
${imports.join("\n")}
|
|
948
|
+
|
|
949
|
+
${envInterface(envKeys)}
|
|
950
|
+
|
|
951
|
+
export class VoiceAgent extends withVoice<Env, typeof Agent<Env>>(Agent<Env>, {
|
|
952
|
+
transport: "${transport}",
|
|
953
|
+
pipeline: {
|
|
954
|
+
kind: "realtime",
|
|
955
|
+
front: (env) => ${realtime.adapterExpr(envRef, "createWorkersSocket")},
|
|
956
|
+
},
|
|
957
|
+
}) {}
|
|
958
|
+
|
|
959
|
+
export default {
|
|
960
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
961
|
+
return (await routeAgentRequest(request, env)) ?? new Response("Not found", { status: 404 });
|
|
962
|
+
},
|
|
963
|
+
};
|
|
964
|
+
`;
|
|
965
|
+
}
|
|
966
|
+
function buildWranglerJsonc(opts) {
|
|
967
|
+
const wrangler = {
|
|
968
|
+
$schema: "node_modules/wrangler/config-schema.json",
|
|
969
|
+
name: opts.name,
|
|
970
|
+
main: "src/index.ts",
|
|
971
|
+
compatibility_date: "2026-06-01",
|
|
972
|
+
compatibility_flags: ["nodejs_compat"],
|
|
973
|
+
durable_objects: { bindings: [{ name: "VoiceAgent", class_name: "VoiceAgent" }] },
|
|
974
|
+
migrations: [{ tag: "v1", new_sqlite_classes: ["VoiceAgent"] }]
|
|
975
|
+
};
|
|
976
|
+
return `${JSON.stringify(wrangler, null, 2)}
|
|
977
|
+
`;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/write-project.ts
|
|
981
|
+
function buildFileMap(opts) {
|
|
982
|
+
const files = [
|
|
983
|
+
{ relPath: "package.json", content: buildPackageJson(opts) },
|
|
984
|
+
{ relPath: "tsconfig.json", content: buildTsconfig(opts.runtime) },
|
|
985
|
+
{ relPath: ".env.example", content: buildEnvExample(opts) },
|
|
986
|
+
{ relPath: "AGENTS.md", content: buildAgentsMd(opts) },
|
|
987
|
+
{ relPath: "src/agent.ts", content: buildAgentModule(opts) },
|
|
988
|
+
{ relPath: "scripts/dev-server.ts", content: buildDevServer(opts.transport) },
|
|
989
|
+
{ relPath: "test/fixtures/smoke.wav", content: buildSmokeWav() }
|
|
990
|
+
];
|
|
991
|
+
if (opts.runtime === "cloudflare") {
|
|
992
|
+
files.push(
|
|
993
|
+
{ relPath: "src/index.ts", content: buildCloudflareWorkerSource(opts) },
|
|
994
|
+
{ relPath: "wrangler.jsonc", content: buildWranglerJsonc(opts) }
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
return files;
|
|
998
|
+
}
|
|
999
|
+
async function writeProject(targetDir, files) {
|
|
1000
|
+
for (const file of files) {
|
|
1001
|
+
const abs = join(targetDir, file.relPath);
|
|
1002
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
1003
|
+
await writeFile(abs, file.content);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function runNpmInstall(targetDir) {
|
|
1007
|
+
const result = spawnSync("npm", ["install"], { cwd: targetDir, stdio: "inherit" });
|
|
1008
|
+
if (result.error) throw result.error;
|
|
1009
|
+
if (result.status !== 0) throw new Error(`npm install exited with code ${String(result.status)}`);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/generate.ts
|
|
1013
|
+
async function runGenerate(values, positionals, io) {
|
|
1014
|
+
const opts = resolveOptions(values, positionals);
|
|
1015
|
+
for (const warning of warningsFor(opts)) {
|
|
1016
|
+
io.stderr(`warning: ${warning}`);
|
|
1017
|
+
}
|
|
1018
|
+
const files = buildFileMap(opts);
|
|
1019
|
+
if (opts.dryRun) {
|
|
1020
|
+
io.stdout(`Would write ${String(files.length)} file(s) to ${opts.targetDir}:`);
|
|
1021
|
+
for (const file of files) io.stdout(` ${file.relPath}`);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
await writeProject(opts.targetDir, files);
|
|
1025
|
+
io.stdout(`Wrote ${String(files.length)} file(s) to ${opts.targetDir}`);
|
|
1026
|
+
if (opts.skipInstall) {
|
|
1027
|
+
io.stdout("Skipped install (--no-install). Run `npm install` inside the project next.");
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
io.stdout(`Installing dependencies in ${opts.name}/ ...`);
|
|
1031
|
+
runNpmInstall(opts.targetDir);
|
|
1032
|
+
io.stdout("Done.");
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/cli.ts
|
|
1036
|
+
var defaultIO = {
|
|
1037
|
+
stdout: (line) => {
|
|
1038
|
+
process.stdout.write(`${line}
|
|
1039
|
+
`);
|
|
1040
|
+
},
|
|
1041
|
+
stderr: (line) => {
|
|
1042
|
+
process.stderr.write(`${line}
|
|
1043
|
+
`);
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
var PARSE_OPTIONS = {
|
|
1047
|
+
"help": { type: "boolean", short: "h" },
|
|
1048
|
+
"version": { type: "boolean", short: "v" },
|
|
1049
|
+
"stt": { type: "string" },
|
|
1050
|
+
"tts": { type: "string" },
|
|
1051
|
+
"realtime": { type: "string" },
|
|
1052
|
+
"reasoner": { type: "string" },
|
|
1053
|
+
"vad": { type: "string" },
|
|
1054
|
+
"endpointing": { type: "string" },
|
|
1055
|
+
"transport": { type: "string" },
|
|
1056
|
+
"runtime": { type: "string" },
|
|
1057
|
+
"preset": { type: "string" },
|
|
1058
|
+
"name": { type: "string" },
|
|
1059
|
+
"yes": { type: "boolean" },
|
|
1060
|
+
"no-install": { type: "boolean" },
|
|
1061
|
+
"skip-install": { type: "boolean" },
|
|
1062
|
+
"dry-run": { type: "boolean" }
|
|
1063
|
+
};
|
|
1064
|
+
async function main(argv, io = defaultIO) {
|
|
1065
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1066
|
+
io.stdout(HELP_TEXT);
|
|
1067
|
+
return EXIT_CODES.SUCCESS;
|
|
1068
|
+
}
|
|
1069
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
1070
|
+
io.stdout(GENERATOR_VERSION);
|
|
1071
|
+
return EXIT_CODES.SUCCESS;
|
|
1072
|
+
}
|
|
1073
|
+
let parsed;
|
|
1074
|
+
try {
|
|
1075
|
+
parsed = parseArgs({ args: [...argv], options: PARSE_OPTIONS, allowPositionals: true });
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
io.stderr(err instanceof Error ? err.message : String(err));
|
|
1078
|
+
return EXIT_CODES.USAGE;
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
await runGenerate(parsed.values, parsed.positionals, io);
|
|
1082
|
+
return EXIT_CODES.SUCCESS;
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
if (err instanceof CliError) {
|
|
1085
|
+
io.stderr(err.message);
|
|
1086
|
+
return err.exitCode;
|
|
1087
|
+
}
|
|
1088
|
+
io.stderr(err instanceof Error ? err.stack ?? err.message : String(err));
|
|
1089
|
+
return EXIT_CODES.INTERNAL;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/index.ts
|
|
1094
|
+
main(process.argv.slice(2)).then((code) => {
|
|
1095
|
+
process.exitCode = code;
|
|
1096
|
+
}).catch((err) => {
|
|
1097
|
+
console.error(err instanceof Error ? err.stack ?? err.message : String(err));
|
|
1098
|
+
process.exitCode = 1;
|
|
1099
|
+
});
|
|
1100
|
+
//# sourceMappingURL=index.js.map
|