@zhihaohong52/sonata 0.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 +21 -0
- package/README.md +437 -0
- package/dist/adapters/claude.js +83 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/codex.js +382 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/opencode.js +104 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/adapters/pi.js +152 -0
- package/dist/adapters/pi.js.map +1 -0
- package/dist/adapters/reasonix.js +358 -0
- package/dist/adapters/reasonix.js.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/agent-markers.js +3 -0
- package/dist/agent-markers.js.map +1 -0
- package/dist/aipricing.js +109 -0
- package/dist/aipricing.js.map +1 -0
- package/dist/catalog.js +274 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cli.js +766 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/approve.js +23 -0
- package/dist/commands/approve.js.map +1 -0
- package/dist/commands/auth.js +41 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/catalog.js +172 -0
- package/dist/commands/catalog.js.map +1 -0
- package/dist/commands/code.js +113 -0
- package/dist/commands/code.js.map +1 -0
- package/dist/commands/dispatch.js +148 -0
- package/dist/commands/dispatch.js.map +1 -0
- package/dist/commands/doctor.js +545 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/gc.js +25 -0
- package/dist/commands/gc.js.map +1 -0
- package/dist/commands/init-log.js +83 -0
- package/dist/commands/init-log.js.map +1 -0
- package/dist/commands/init.js +157 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/log.js +24 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/route.js +660 -0
- package/dist/commands/route.js.map +1 -0
- package/dist/commands/run.js +199 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/runs.js +37 -0
- package/dist/commands/runs.js.map +1 -0
- package/dist/commands/serve.js +818 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/status.js +14 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/sync.js +258 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/tail.js +229 -0
- package/dist/commands/tail.js.map +1 -0
- package/dist/commands/usage.js +85 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/commands/verify.js +69 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/wait.js +38 -0
- package/dist/commands/wait.js.map +1 -0
- package/dist/config.js +588 -0
- package/dist/config.js.map +1 -0
- package/dist/detect.js +445 -0
- package/dist/detect.js.map +1 -0
- package/dist/filelock.js +83 -0
- package/dist/filelock.js.map +1 -0
- package/dist/init/apply.js +96 -0
- package/dist/init/apply.js.map +1 -0
- package/dist/init/candidates.js +115 -0
- package/dist/init/candidates.js.map +1 -0
- package/dist/init/discover.js +216 -0
- package/dist/init/discover.js.map +1 -0
- package/dist/init/helpers.js +429 -0
- package/dist/init/helpers.js.map +1 -0
- package/dist/init/interactive-state.js +128 -0
- package/dist/init/interactive-state.js.map +1 -0
- package/dist/init/plan.js +225 -0
- package/dist/init/plan.js.map +1 -0
- package/dist/init/scripted-state.js +130 -0
- package/dist/init/scripted-state.js.map +1 -0
- package/dist/init/toml.js +83 -0
- package/dist/init/toml.js.map +1 -0
- package/dist/init/validate.js +149 -0
- package/dist/init/validate.js.map +1 -0
- package/dist/ledger.js +123 -0
- package/dist/ledger.js.map +1 -0
- package/dist/mcp/protocol.js +64 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/server.js +96 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.js +210 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mode.js +64 -0
- package/dist/mode.js.map +1 -0
- package/dist/native/claude-shim.js +228 -0
- package/dist/native/claude-shim.js.map +1 -0
- package/dist/native/codex-auth.js +177 -0
- package/dist/native/codex-auth.js.map +1 -0
- package/dist/native/copilot-auth.js +105 -0
- package/dist/native/copilot-auth.js.map +1 -0
- package/dist/native/credentials.js +83 -0
- package/dist/native/credentials.js.map +1 -0
- package/dist/native/hosts.js +60 -0
- package/dist/native/hosts.js.map +1 -0
- package/dist/native/litellm.js +103 -0
- package/dist/native/litellm.js.map +1 -0
- package/dist/native/models.js +96 -0
- package/dist/native/models.js.map +1 -0
- package/dist/native/oauth-login.js +112 -0
- package/dist/native/oauth-login.js.map +1 -0
- package/dist/native/router.js +469 -0
- package/dist/native/router.js.map +1 -0
- package/dist/native/tls.js +73 -0
- package/dist/native/tls.js.map +1 -0
- package/dist/native/usage.js +119 -0
- package/dist/native/usage.js.map +1 -0
- package/dist/normalize.js +98 -0
- package/dist/normalize.js.map +1 -0
- package/dist/pricing.js +86 -0
- package/dist/pricing.js.map +1 -0
- package/dist/roles.js +49 -0
- package/dist/roles.js.map +1 -0
- package/dist/sessions.js +62 -0
- package/dist/sessions.js.map +1 -0
- package/dist/settings.js +131 -0
- package/dist/settings.js.map +1 -0
- package/dist/store.js +70 -0
- package/dist/store.js.map +1 -0
- package/dist/tmux.js +89 -0
- package/dist/tmux.js.map +1 -0
- package/dist/tui-ink/app-state.js +340 -0
- package/dist/tui-ink/app-state.js.map +1 -0
- package/dist/tui-ink/app.js +156 -0
- package/dist/tui-ink/app.js.map +1 -0
- package/dist/tui-ink/components/byok-step.js +102 -0
- package/dist/tui-ink/components/byok-step.js.map +1 -0
- package/dist/tui-ink/components/login-screen.js +80 -0
- package/dist/tui-ink/components/login-screen.js.map +1 -0
- package/dist/tui-ink/components/models-step.js +92 -0
- package/dist/tui-ink/components/models-step.js.map +1 -0
- package/dist/tui-ink/components/multi-select-state.js +56 -0
- package/dist/tui-ink/components/multi-select-state.js.map +1 -0
- package/dist/tui-ink/components/multi-select.js +69 -0
- package/dist/tui-ink/components/multi-select.js.map +1 -0
- package/dist/tui-ink/components/providers-step.js +285 -0
- package/dist/tui-ink/components/providers-step.js.map +1 -0
- package/dist/tui-ink/components/ranked-select-state.js +41 -0
- package/dist/tui-ink/components/ranked-select-state.js.map +1 -0
- package/dist/tui-ink/components/ranked-select.js +61 -0
- package/dist/tui-ink/components/ranked-select.js.map +1 -0
- package/dist/tui-ink/components/search-select.js +47 -0
- package/dist/tui-ink/components/search-select.js.map +1 -0
- package/dist/tui-ink/components/text-input-state.js +42 -0
- package/dist/tui-ink/components/text-input-state.js.map +1 -0
- package/dist/tui-ink/components/text-input.js +48 -0
- package/dist/tui-ink/components/text-input.js.map +1 -0
- package/dist/tui-ink/run.js +38 -0
- package/dist/tui-ink/run.js.map +1 -0
- package/dist/tui-ink/test-compile.js +4 -0
- package/dist/tui-ink/test-compile.js.map +1 -0
- package/dist/tui-ink/types.js +2 -0
- package/dist/tui-ink/types.js.map +1 -0
- package/dist/tui.js +330 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/watchdog.js +85 -0
- package/dist/watchdog.js.map +1 -0
- package/hooks/capture-mode.mjs +56 -0
- package/hooks/ensure-serve.mjs +140 -0
- package/hooks/hooks.json +12 -0
- package/hooks/route-session.mjs +47 -0
- package/hooks/route-subagent.mjs +54 -0
- package/package.json +64 -0
- package/roles/code.md +13 -0
- package/roles/explore.md +9 -0
- package/roles/plan.md +10 -0
- package/roles/review.md +11 -0
- package/skills/loop/SKILL.md +43 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { cmdRun } from './commands/run.js';
|
|
5
|
+
import { cmdDispatch } from './commands/dispatch.js';
|
|
6
|
+
import { cmdTail } from './commands/tail.js';
|
|
7
|
+
import { cmdWait } from './commands/wait.js';
|
|
8
|
+
import { loadConfig } from './config.js';
|
|
9
|
+
import { cmdApprove } from './commands/approve.js';
|
|
10
|
+
import { cmdSync } from './commands/sync.js';
|
|
11
|
+
import { cmdDoctor } from './commands/doctor.js';
|
|
12
|
+
import { cmdGc } from './commands/gc.js';
|
|
13
|
+
import { cmdInit, isCancellation } from './commands/init.js';
|
|
14
|
+
import { initLogDir } from './commands/init-log.js';
|
|
15
|
+
import { banner, isInteractive, confirm } from './tui.js';
|
|
16
|
+
import { pruneAgents } from './detect.js';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
20
|
+
import { cmdAuthAdd, cmdAuthList, cmdAuthLogin, cmdAuthRemove } from './commands/auth.js';
|
|
21
|
+
import { cmdServe, cmdRestart, startServeDaemon, isSonataRouter } from './commands/serve.js';
|
|
22
|
+
import { cmdCode } from './commands/code.js';
|
|
23
|
+
import { recentRoutes } from './commands/status.js';
|
|
24
|
+
import { summarizeRuns } from './commands/runs.js';
|
|
25
|
+
import { cmdRoute, cmdRouteSession, cmdRouteSubagent } from './commands/route.js';
|
|
26
|
+
import { cmdCatalogUpdate } from './commands/catalog.js';
|
|
27
|
+
import { AA_ATTRIBUTION, aaCatalogPath, loadAaCatalog } from './catalog.js';
|
|
28
|
+
import { AI_PRICING_ATTRIBUTION } from './aipricing.js';
|
|
29
|
+
import { cmdUsage } from './commands/usage.js';
|
|
30
|
+
import { readRows } from './ledger.js';
|
|
31
|
+
const USAGE = `sonata — foreign-model subagents for Claude Code
|
|
32
|
+
|
|
33
|
+
sonata init set up sonata in this project (interactive)
|
|
34
|
+
sonata doctor check tmux, harnesses, auth and versions
|
|
35
|
+
sonata sync regenerate agent files from sonata.toml
|
|
36
|
+
sonata run launch a harness run, print its id
|
|
37
|
+
sonata dispatch run a ranked tier with harness fallback
|
|
38
|
+
sonata tail poll a run for progress
|
|
39
|
+
sonata wait resume a RUNNING or approved run and block for its next state
|
|
40
|
+
sonata approve answer a pending approval (requires --yes or --no)
|
|
41
|
+
sonata gc kill finished tmux sessions
|
|
42
|
+
sonata log print a run's whole transcript (tail returns only new lines)
|
|
43
|
+
sonata verify confirm a dispatch actually happened
|
|
44
|
+
sonata serve start the native routing proxy (router + litellm)
|
|
45
|
+
sonata restart kill any router holding the port and start a fresh daemon
|
|
46
|
+
sonata code launch a claude session routed through sonata serve
|
|
47
|
+
sonata route on|off|status — route plain claude sessions through sonata serve
|
|
48
|
+
auto|manual — route each session for its lifetime, keeping Remote Control
|
|
49
|
+
sonata auth manage gateway credentials (list/add/remove/login)
|
|
50
|
+
sonata catalog show or refresh the Artificial Analysis model catalog
|
|
51
|
+
sonata usage report native-path token and cost usage from the ledger
|
|
52
|
+
sonata status router health and the last hour of routes
|
|
53
|
+
sonata runs list every run, with state and whether it wrote a report
|
|
54
|
+
|
|
55
|
+
init flags (skip the prompts):
|
|
56
|
+
--yes accept defaults, no prompts
|
|
57
|
+
--providers opencode/openrouter,pi/opencode-go providers to draw models from
|
|
58
|
+
--models a,b models to enable (config keys)
|
|
59
|
+
--roles code,review roles to generate
|
|
60
|
+
--credential-source gateway=sonata|codex|opencode record a gateway credential source
|
|
61
|
+
--config-scope project|global where the config and its agents go
|
|
62
|
+
--scope project|global|skip where to install the permission hook
|
|
63
|
+
--prune delete stale sonata agent files
|
|
64
|
+
`;
|
|
65
|
+
/** `--wait` wins; otherwise use the configured tail window. */
|
|
66
|
+
export function tailWaitSeconds(flag, configured) {
|
|
67
|
+
if (flag === undefined)
|
|
68
|
+
return configured;
|
|
69
|
+
const parsed = Number.parseInt(flag, 10);
|
|
70
|
+
return Number.isFinite(parsed) ? parsed : configured;
|
|
71
|
+
}
|
|
72
|
+
/** Repository root, one level above the compiled dist/ or src/ directory. */
|
|
73
|
+
function packageRoot() {
|
|
74
|
+
return join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
|
75
|
+
}
|
|
76
|
+
export async function main(argv) {
|
|
77
|
+
const [command, ...rest] = argv;
|
|
78
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
79
|
+
console.log(USAGE);
|
|
80
|
+
return command ? 0 : 2;
|
|
81
|
+
}
|
|
82
|
+
if (command === 'init') {
|
|
83
|
+
const { values } = parseArgs({
|
|
84
|
+
args: rest,
|
|
85
|
+
options: {
|
|
86
|
+
yes: { type: 'boolean', default: false },
|
|
87
|
+
providers: { type: 'string' },
|
|
88
|
+
models: { type: 'string' },
|
|
89
|
+
roles: { type: 'string' },
|
|
90
|
+
'credential-source': { type: 'string', multiple: true },
|
|
91
|
+
'config-scope': { type: 'string' },
|
|
92
|
+
scope: { type: 'string' },
|
|
93
|
+
routing: { type: 'string' },
|
|
94
|
+
// No default: `undefined` means "unanswered", which lets cmdInit fall
|
|
95
|
+
// through to the interactive prompt. `false` would suppress it.
|
|
96
|
+
prune: { type: 'boolean' },
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
const scope = values.scope;
|
|
100
|
+
if (scope && !['project', 'global', 'skip'].includes(scope)) {
|
|
101
|
+
throw new Error(`sonata init: --scope must be project, global or skip (got "${scope}")`);
|
|
102
|
+
}
|
|
103
|
+
const routing = values.routing;
|
|
104
|
+
if (routing && !['project', 'global', 'skip'].includes(routing)) {
|
|
105
|
+
throw new Error(`sonata init: --routing must be project, global or skip (got "${routing}")`);
|
|
106
|
+
}
|
|
107
|
+
const configScope = values['config-scope'];
|
|
108
|
+
if (configScope && !['project', 'global'].includes(configScope)) {
|
|
109
|
+
throw new Error(`sonata init: --config-scope must be project or global (got "${configScope}")`);
|
|
110
|
+
}
|
|
111
|
+
const split = (v) => v === undefined ? undefined : v.split(',').map((s) => s.trim()).filter(Boolean);
|
|
112
|
+
try {
|
|
113
|
+
const res = await cmdInit({
|
|
114
|
+
cwd: process.cwd(),
|
|
115
|
+
home: homedir(),
|
|
116
|
+
packageRoot: packageRoot(),
|
|
117
|
+
yes: values.yes,
|
|
118
|
+
providers: split(values.providers),
|
|
119
|
+
models: split(values.models),
|
|
120
|
+
roles: split(values.roles),
|
|
121
|
+
credentialSource: values['credential-source'],
|
|
122
|
+
scope,
|
|
123
|
+
routing,
|
|
124
|
+
configScope,
|
|
125
|
+
prune: values.prune,
|
|
126
|
+
});
|
|
127
|
+
if (res.cancelled)
|
|
128
|
+
return 1;
|
|
129
|
+
return res.problems.some((p) => p.severity === 'error') ? 1 : 0;
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// The wizard owns the screen, so a failure's own output may already be
|
|
133
|
+
// gone with the alternate buffer. Point at the log that survived it.
|
|
134
|
+
const logDir = initLogDir(homedir());
|
|
135
|
+
if (isCancellation(err)) {
|
|
136
|
+
console.log('\n Cancelled. Nothing written.');
|
|
137
|
+
console.log(` Log: ${logDir}`);
|
|
138
|
+
return 130;
|
|
139
|
+
}
|
|
140
|
+
console.error(`\n sonata init failed. Log: ${logDir}`);
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (command === 'run') {
|
|
145
|
+
const { values } = parseArgs({
|
|
146
|
+
args: rest,
|
|
147
|
+
options: {
|
|
148
|
+
role: { type: 'string' },
|
|
149
|
+
model: { type: 'string' },
|
|
150
|
+
'task-file': { type: 'string' },
|
|
151
|
+
'roles-dir': { type: 'string' },
|
|
152
|
+
json: { type: 'boolean', default: false },
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
if (!values.role || !values.model || !values['task-file']) {
|
|
156
|
+
throw new Error('sonata run requires --role, --model and --task-file');
|
|
157
|
+
}
|
|
158
|
+
const res = await cmdRun({
|
|
159
|
+
cwd: process.cwd(),
|
|
160
|
+
role: values.role,
|
|
161
|
+
model: values.model,
|
|
162
|
+
taskFile: values['task-file'],
|
|
163
|
+
// fileURLToPath, not .pathname — the latter percent-encodes spaces and
|
|
164
|
+
// would break for anyone whose install path contains one.
|
|
165
|
+
rolesDir: values['roles-dir'] ?? join(packageRoot(), 'roles'),
|
|
166
|
+
sessionId: process.env.CLAUDE_CODE_SESSION_ID,
|
|
167
|
+
});
|
|
168
|
+
// The attach command is printed in full, because watching a foreign model
|
|
169
|
+
// work is the one thing sonata cannot stream into a Claude Code
|
|
170
|
+
// conversation — and reconstructing the session name by hand is the step
|
|
171
|
+
// that stops people from looking.
|
|
172
|
+
console.log(values.json
|
|
173
|
+
? JSON.stringify(res)
|
|
174
|
+
: `run ${res.id} (tmux ${res.session})\n watch: tmux attach -r -t ${res.session}`);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
if (command === 'dispatch') {
|
|
178
|
+
const { values, positionals } = parseArgs({
|
|
179
|
+
args: rest,
|
|
180
|
+
allowPositionals: true,
|
|
181
|
+
options: {
|
|
182
|
+
tier: { type: 'string' },
|
|
183
|
+
model: { type: 'string' },
|
|
184
|
+
role: { type: 'string' },
|
|
185
|
+
'task-file': { type: 'string' },
|
|
186
|
+
'task-stdin': { type: 'boolean' },
|
|
187
|
+
'roles-dir': { type: 'string' },
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
if ((values.tier === undefined) === (values.model === undefined)) {
|
|
191
|
+
throw new Error('sonata dispatch requires exactly one of --tier or --model');
|
|
192
|
+
}
|
|
193
|
+
if (values.role !== undefined && values.tier !== undefined) {
|
|
194
|
+
throw new Error('sonata dispatch: --role only applies to --model; --tier\'s alias already names a role');
|
|
195
|
+
}
|
|
196
|
+
if (values['task-file'] !== undefined && values['task-stdin']) {
|
|
197
|
+
throw new Error('sonata dispatch: --task-file and --task-stdin are mutually exclusive');
|
|
198
|
+
}
|
|
199
|
+
if ((values['task-file'] !== undefined || values['task-stdin']) && positionals.length > 0) {
|
|
200
|
+
throw new Error('sonata dispatch accepts positional task text only without --task-file/--task-stdin');
|
|
201
|
+
}
|
|
202
|
+
const task = values['task-stdin']
|
|
203
|
+
? readFileSync(0, 'utf8')
|
|
204
|
+
: values['task-file'] !== undefined
|
|
205
|
+
? readFileSync(values['task-file'], 'utf8')
|
|
206
|
+
: positionals.join(' ');
|
|
207
|
+
if (!task.trim())
|
|
208
|
+
throw new Error('sonata dispatch requires task text, --task-file, or --task-stdin');
|
|
209
|
+
const res = await cmdDispatch({
|
|
210
|
+
cwd: process.cwd(),
|
|
211
|
+
home: homedir(),
|
|
212
|
+
tier: values.tier,
|
|
213
|
+
model: values.model,
|
|
214
|
+
role: values.role,
|
|
215
|
+
task,
|
|
216
|
+
rolesDir: values['roles-dir'] ?? join(packageRoot(), 'roles'),
|
|
217
|
+
sessionId: process.env.CLAUDE_CODE_SESSION_ID,
|
|
218
|
+
});
|
|
219
|
+
console.log(`${res.state} model=${res.modelKey} id=${res.id}`);
|
|
220
|
+
if (res.report)
|
|
221
|
+
console.log(`\n${res.report}`);
|
|
222
|
+
if (res.prompt) {
|
|
223
|
+
console.log(`\nPROMPT: ${res.prompt}`);
|
|
224
|
+
console.log(`sonata approve ${res.id} --yes (or --no)`);
|
|
225
|
+
}
|
|
226
|
+
else if (res.state === 'RUNNING') {
|
|
227
|
+
console.log(`sonata wait ${res.id}`);
|
|
228
|
+
}
|
|
229
|
+
if (res.state === 'FAILED') {
|
|
230
|
+
for (const attempt of res.attempts) {
|
|
231
|
+
console.log(` ${attempt.modelKey}: ${attempt.state}${attempt.degraded ? ' (degraded)' : ''}${attempt.error ? ` — ${attempt.error}` : ''}`);
|
|
232
|
+
}
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
if (res.state === 'STALLED')
|
|
236
|
+
return 3;
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
if (command === 'tail') {
|
|
240
|
+
const { values, positionals } = parseArgs({
|
|
241
|
+
args: rest,
|
|
242
|
+
allowPositionals: true,
|
|
243
|
+
options: { wait: { type: 'string' }, json: { type: 'boolean', default: false } },
|
|
244
|
+
});
|
|
245
|
+
const id = positionals[0];
|
|
246
|
+
if (!id)
|
|
247
|
+
throw new Error('sonata tail requires a run id');
|
|
248
|
+
const res = await cmdTail({
|
|
249
|
+
cwd: process.cwd(),
|
|
250
|
+
id,
|
|
251
|
+
waitSeconds: tailWaitSeconds(values.wait, loadConfig(process.cwd()).run.tailWindowSeconds),
|
|
252
|
+
});
|
|
253
|
+
if (values.json) {
|
|
254
|
+
console.log(JSON.stringify(res));
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.log(res.state);
|
|
258
|
+
for (const l of res.lines)
|
|
259
|
+
console.log(` ${l}`);
|
|
260
|
+
if (res.prompt)
|
|
261
|
+
console.log(` PROMPT: ${res.prompt}`);
|
|
262
|
+
if (res.report)
|
|
263
|
+
console.log(`\n${res.report}`);
|
|
264
|
+
}
|
|
265
|
+
return res.state === 'STALLED' ? 3 : 0;
|
|
266
|
+
}
|
|
267
|
+
if (command === 'wait') {
|
|
268
|
+
const { values, positionals } = parseArgs({
|
|
269
|
+
args: rest,
|
|
270
|
+
allowPositionals: true,
|
|
271
|
+
options: { json: { type: 'boolean', default: false } },
|
|
272
|
+
});
|
|
273
|
+
const id = positionals[0];
|
|
274
|
+
if (!id)
|
|
275
|
+
throw new Error('sonata wait requires a run id');
|
|
276
|
+
const res = await cmdWait({ cwd: process.cwd(), id });
|
|
277
|
+
if (values.json) {
|
|
278
|
+
console.log(JSON.stringify(res));
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
console.log(res.state);
|
|
282
|
+
for (const l of res.lines)
|
|
283
|
+
console.log(` ${l}`);
|
|
284
|
+
if (res.prompt) {
|
|
285
|
+
console.log(` PROMPT: ${res.prompt}`);
|
|
286
|
+
console.log(`sonata approve ${id} --yes (or --no)`);
|
|
287
|
+
}
|
|
288
|
+
else if (res.state === 'RUNNING') {
|
|
289
|
+
console.log(`sonata wait ${id}`);
|
|
290
|
+
}
|
|
291
|
+
if (res.report)
|
|
292
|
+
console.log(`\n${res.report}`);
|
|
293
|
+
}
|
|
294
|
+
return res.state === 'STALLED' ? 3 : 0;
|
|
295
|
+
}
|
|
296
|
+
if (command === 'approve') {
|
|
297
|
+
const { values, positionals } = parseArgs({
|
|
298
|
+
args: rest,
|
|
299
|
+
allowPositionals: true,
|
|
300
|
+
options: { yes: { type: 'boolean', default: false }, no: { type: 'boolean', default: false } },
|
|
301
|
+
});
|
|
302
|
+
const id = positionals[0];
|
|
303
|
+
if (!id)
|
|
304
|
+
throw new Error('sonata approve requires a run id');
|
|
305
|
+
if (values.yes === values.no)
|
|
306
|
+
throw new Error('sonata approve requires exactly one of --yes or --no');
|
|
307
|
+
await cmdApprove({ cwd: process.cwd(), id, yes: values.yes });
|
|
308
|
+
console.log(`approved=${values.yes} run=${id}`);
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
if (command === 'sync') {
|
|
312
|
+
const { values } = parseArgs({
|
|
313
|
+
args: rest,
|
|
314
|
+
options: { prune: { type: 'boolean', default: false } },
|
|
315
|
+
});
|
|
316
|
+
const agentsDir = join(process.cwd(), '.claude', 'agents');
|
|
317
|
+
const sync = cmdSync({
|
|
318
|
+
cwd: process.cwd(),
|
|
319
|
+
home: homedir(),
|
|
320
|
+
agentsDir,
|
|
321
|
+
});
|
|
322
|
+
for (const p of sync.written)
|
|
323
|
+
console.log(`wrote ${p}`);
|
|
324
|
+
if (sync.skipped.length > 0) {
|
|
325
|
+
for (const f of sync.skipped.slice(0, 5))
|
|
326
|
+
console.log(`skipped ${f} (exists, not sonata-owned)`);
|
|
327
|
+
if (sync.skipped.length > 5)
|
|
328
|
+
console.log(`skipped ... and ${sync.skipped.length - 5} more`);
|
|
329
|
+
}
|
|
330
|
+
if (sync.stale.length > 0) {
|
|
331
|
+
for (const f of sync.stale.slice(0, 5))
|
|
332
|
+
console.log(`stale ${f}`);
|
|
333
|
+
if (sync.stale.length > 5)
|
|
334
|
+
console.log(`stale ... and ${sync.stale.length - 5} more`);
|
|
335
|
+
const remove = values.prune || (isInteractive() && await confirm('Delete them?', true));
|
|
336
|
+
if (remove) {
|
|
337
|
+
const pruned = pruneAgents(agentsDir, sync.stale);
|
|
338
|
+
console.log(`removed ${pruned.length} stale agent file(s)`);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
console.log('delete them by hand, or re-run with --prune');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
if (command === 'doctor') {
|
|
347
|
+
// doctor is read, not piped — but only decorate a real terminal, so
|
|
348
|
+
// `sonata doctor > report.txt` stays plain.
|
|
349
|
+
if (isInteractive())
|
|
350
|
+
console.log(`\n${banner()}\n`);
|
|
351
|
+
const { ok, checks } = await cmdDoctor({
|
|
352
|
+
cwd: process.cwd(),
|
|
353
|
+
home: homedir(),
|
|
354
|
+
packageRoot: packageRoot(),
|
|
355
|
+
});
|
|
356
|
+
for (const c of checks)
|
|
357
|
+
console.log(`${c.ok ? 'ok ' : 'FAIL'} ${c.name}: ${c.detail}`);
|
|
358
|
+
return ok ? 0 : 1;
|
|
359
|
+
}
|
|
360
|
+
if (command === 'catalog') {
|
|
361
|
+
const action = rest[0];
|
|
362
|
+
if (action !== undefined && action !== 'update') {
|
|
363
|
+
throw new Error('sonata catalog requires update or no subcommand');
|
|
364
|
+
}
|
|
365
|
+
if (action === 'update') {
|
|
366
|
+
const result = await cmdCatalogUpdate(homedir());
|
|
367
|
+
if ('error' in result.aa) {
|
|
368
|
+
console.error(`Artificial Analysis catalog not updated: ${result.aa.error.message}`);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
console.log(`catalog updated: ${result.aa.models} models`);
|
|
372
|
+
console.log(` path: ${result.aa.path}`);
|
|
373
|
+
console.log(` fetched: ${result.aa.fetchedAt}`);
|
|
374
|
+
console.log(AA_ATTRIBUTION);
|
|
375
|
+
}
|
|
376
|
+
if ('error' in result.aiPricing) {
|
|
377
|
+
console.error(`ai-pricing catalog not updated: ${result.aiPricing.error.message}`);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
console.log(`ai-pricing catalog updated: ${result.aiPricing.models} models`);
|
|
381
|
+
console.log(` path: ${result.aiPricing.path}`);
|
|
382
|
+
console.log(` fetched: ${result.aiPricing.fetchedAt}`);
|
|
383
|
+
console.log(AI_PRICING_ATTRIBUTION);
|
|
384
|
+
}
|
|
385
|
+
return 'error' in result.aa || 'error' in result.aiPricing ? 1 : 0;
|
|
386
|
+
}
|
|
387
|
+
const path = aaCatalogPath(homedir());
|
|
388
|
+
const catalog = loadAaCatalog(homedir());
|
|
389
|
+
if (!catalog || !existsSync(path)) {
|
|
390
|
+
console.log('no catalog — run sonata catalog update');
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
const age = Math.max(0, Date.now() - Date.parse(catalog.fetchedAt));
|
|
394
|
+
console.log(`catalog: ${catalog.models ? Object.keys(catalog.models).length : 0} models; age ${Math.floor(age / 1000)}s`);
|
|
395
|
+
console.log(` path: ${path}`);
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
if (command === 'usage') {
|
|
399
|
+
// parseArgs is strict by default: an unrecognized flag throws rather than
|
|
400
|
+
// being silently ignored — for a cost report, quietly falling back to the
|
|
401
|
+
// default window on a misspelled `--since` would be materially misleading.
|
|
402
|
+
const { values } = parseArgs({
|
|
403
|
+
args: rest,
|
|
404
|
+
options: {
|
|
405
|
+
by: { type: 'string' },
|
|
406
|
+
since: { type: 'string' },
|
|
407
|
+
session: { type: 'string' },
|
|
408
|
+
json: { type: 'boolean', default: false },
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
const by = (values.by ?? 'model');
|
|
412
|
+
if (!['model', 'role', 'tier', 'gateway', 'session', 'project'].includes(by)) {
|
|
413
|
+
throw new Error('sonata usage --by must be one of: model | role | tier | gateway | session | project');
|
|
414
|
+
}
|
|
415
|
+
const report = await cmdUsage({
|
|
416
|
+
home: homedir(),
|
|
417
|
+
since: values.since ?? '7d',
|
|
418
|
+
by,
|
|
419
|
+
session: values.session,
|
|
420
|
+
json: values.json,
|
|
421
|
+
});
|
|
422
|
+
if (values.json) {
|
|
423
|
+
console.log(JSON.stringify(report, null, 2));
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
426
|
+
// Widen to the longest label rather than a fixed 30: a model key like
|
|
427
|
+
// `openrouter-nemotron-3.5-lightning` overruns that, and every number on
|
|
428
|
+
// its row then shifts out of its column.
|
|
429
|
+
const labelWidth = Math.max(0, ...report.buckets.map((bucket) => bucket.label.length));
|
|
430
|
+
for (const bucket of report.buckets) {
|
|
431
|
+
console.log(`${bucket.label.padEnd(labelWidth)} ${String(bucket.requests).padStart(8)} ${String(bucket.input).padStart(12)} ${String(bucket.output).padStart(10)} ${bucket.costUsd === 0 && bucket.unpricedRequests === bucket.requests ? '—' : `$${bucket.costUsd.toFixed(4)}`}`);
|
|
432
|
+
}
|
|
433
|
+
console.log(`\npriced total $${report.pricedTotalUsd.toFixed(4)}`);
|
|
434
|
+
if (report.unpriced.requests > 0) {
|
|
435
|
+
console.log(`unpriced ${report.unpriced.requests} requests · ${report.unpriced.input} in · ${report.unpriced.output} out`);
|
|
436
|
+
}
|
|
437
|
+
console.log('native path only — `sonata dispatch` runs bypass the router and cannot be measured');
|
|
438
|
+
if (report.priceCacheAgeMs !== undefined) {
|
|
439
|
+
console.log(`prices: ai-pricing.fyi cache ${Math.floor(report.priceCacheAgeMs / 86_400_000)}d old`);
|
|
440
|
+
}
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
if (command === 'status') {
|
|
444
|
+
// parseArgs is strict by default: an unknown flag throws rather than being
|
|
445
|
+
// silently ignored — a misspelled `--session` must not silently fall back
|
|
446
|
+
// to the most recent session's rows.
|
|
447
|
+
const { values } = parseArgs({
|
|
448
|
+
args: rest,
|
|
449
|
+
options: {
|
|
450
|
+
session: { type: 'string' },
|
|
451
|
+
all: { type: 'boolean', default: false },
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
if (values.session !== undefined && values.all) {
|
|
455
|
+
throw new Error('sonata status: --session and --all are mutually exclusive');
|
|
456
|
+
}
|
|
457
|
+
const home = homedir();
|
|
458
|
+
let port;
|
|
459
|
+
try {
|
|
460
|
+
port = loadConfig(process.cwd(), home).native?.ports.router;
|
|
461
|
+
}
|
|
462
|
+
catch { /* no config here — ledger rows below still report */ }
|
|
463
|
+
const up = port === undefined ? false : await isSonataRouter(port);
|
|
464
|
+
console.log(up ? `router: up on localhost:${port}` : port === undefined ? 'router: unknown (no sonata.toml here)' : 'router: down');
|
|
465
|
+
// The last hour of routes by default, narrowed to the most recent session
|
|
466
|
+
// — unless --all asks for every session or --session names one specifically.
|
|
467
|
+
let rows = readRows(home, Date.now() - 3_600_000);
|
|
468
|
+
if (values.all) {
|
|
469
|
+
// keep every session's rows
|
|
470
|
+
}
|
|
471
|
+
else if (values.session !== undefined) {
|
|
472
|
+
rows = rows.filter((row) => row.session === values.session);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
// The most recent session is the one whose newest row is newest.
|
|
476
|
+
const sessions = new Map();
|
|
477
|
+
for (const row of rows) {
|
|
478
|
+
if (row.session === undefined)
|
|
479
|
+
continue;
|
|
480
|
+
const ts = Date.parse(row.ts);
|
|
481
|
+
const prev = sessions.get(row.session);
|
|
482
|
+
if (prev === undefined || ts > prev)
|
|
483
|
+
sessions.set(row.session, ts);
|
|
484
|
+
}
|
|
485
|
+
let newest;
|
|
486
|
+
for (const [id, ts] of sessions) {
|
|
487
|
+
if (newest === undefined || ts > newest.ts)
|
|
488
|
+
newest = { id, ts };
|
|
489
|
+
}
|
|
490
|
+
rows = newest === undefined ? [] : rows.filter((row) => row.session === newest.id);
|
|
491
|
+
}
|
|
492
|
+
const recent = recentRoutes(rows, 10);
|
|
493
|
+
if (recent.length === 0) {
|
|
494
|
+
console.log('no routes in the last hour');
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
for (const line of recent) {
|
|
498
|
+
const served = line.served ?? '(none — all candidates failed)';
|
|
499
|
+
console.log(`${line.status} ${line.alias.padEnd(24)} -> ${served.padEnd(20)} ${line.input} in / ${line.output} out`);
|
|
500
|
+
if (line.attempts.length > 0) {
|
|
501
|
+
for (const a of line.attempts)
|
|
502
|
+
console.log(` attempt ${a.key}: ${a.status}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
console.log('reach and routing state live in `sonata route status`');
|
|
507
|
+
return 0;
|
|
508
|
+
}
|
|
509
|
+
if (command === 'runs') {
|
|
510
|
+
const { values } = parseArgs({
|
|
511
|
+
args: rest,
|
|
512
|
+
options: { json: { type: 'boolean', default: false } },
|
|
513
|
+
});
|
|
514
|
+
const runs = summarizeRuns(process.cwd());
|
|
515
|
+
if (values.json) {
|
|
516
|
+
console.log(JSON.stringify(runs, null, 2));
|
|
517
|
+
return 0;
|
|
518
|
+
}
|
|
519
|
+
if (runs.length === 0) {
|
|
520
|
+
console.log('no runs — `sonata run` or `sonata dispatch` to launch one');
|
|
521
|
+
return 0;
|
|
522
|
+
}
|
|
523
|
+
for (const r of runs) {
|
|
524
|
+
const flags = `${r.state}${r.degraded ? ' degraded' : ''}${r.report ? ' report' : ''}`;
|
|
525
|
+
console.log(`${r.id.padEnd(8)} ${flags.padEnd(24)} ${(r.role ?? '—').padEnd(8)} ${r.model ?? '—'} ${r.started ?? ''}`);
|
|
526
|
+
}
|
|
527
|
+
return 0;
|
|
528
|
+
}
|
|
529
|
+
if (command === 'auth') {
|
|
530
|
+
const { positionals } = parseArgs({ args: rest, allowPositionals: true, options: {} });
|
|
531
|
+
const [action, gateway] = positionals;
|
|
532
|
+
let gateways = [];
|
|
533
|
+
try {
|
|
534
|
+
gateways = Object.keys(loadConfig(process.cwd(), homedir()).native?.gateways ?? {});
|
|
535
|
+
}
|
|
536
|
+
catch { /* no config yet — add/remove/login do not need one */ }
|
|
537
|
+
if (action === 'list') {
|
|
538
|
+
console.log(cmdAuthList({ home: homedir(), gateways }).text);
|
|
539
|
+
return 0;
|
|
540
|
+
}
|
|
541
|
+
if (action === 'login') {
|
|
542
|
+
if (!gateway)
|
|
543
|
+
throw new Error('sonata auth login requires a gateway');
|
|
544
|
+
await cmdAuthLogin({ home: homedir(), cwd: process.cwd(), gateway, out: (l) => console.log(l) });
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
if (action === 'remove') {
|
|
548
|
+
if (!gateway)
|
|
549
|
+
throw new Error('sonata auth remove requires a gateway');
|
|
550
|
+
cmdAuthRemove({ home: homedir(), gateway });
|
|
551
|
+
return 0;
|
|
552
|
+
}
|
|
553
|
+
if (action === 'add') {
|
|
554
|
+
if (!gateway)
|
|
555
|
+
throw new Error('sonata auth add requires a gateway');
|
|
556
|
+
const key = await readAuthKey();
|
|
557
|
+
if (!key)
|
|
558
|
+
throw new Error('sonata auth add requires a non-empty key');
|
|
559
|
+
cmdAuthAdd({ home: homedir(), gateway, key });
|
|
560
|
+
return 0;
|
|
561
|
+
}
|
|
562
|
+
throw new Error('sonata auth requires list, add <gateway>, remove <gateway> or login <gateway>');
|
|
563
|
+
}
|
|
564
|
+
if (command === 'serve') {
|
|
565
|
+
const { values } = parseArgs({
|
|
566
|
+
args: rest,
|
|
567
|
+
options: { daemon: { type: 'boolean', default: false } },
|
|
568
|
+
});
|
|
569
|
+
if (values.daemon) {
|
|
570
|
+
// Re-exec this same CLI in the foreground, detached. The flag used to be
|
|
571
|
+
// parsed, handed to cmdServe and ignored, so `--daemon` blocked exactly
|
|
572
|
+
// like the foreground command.
|
|
573
|
+
const self = fileURLToPath(import.meta.url);
|
|
574
|
+
const daemon = await startServeDaemon(homedir(), [process.execPath, self, 'serve']);
|
|
575
|
+
console.log(`sonata serve running in the background (pid ${daemon.pid}) on port ${daemon.port}`);
|
|
576
|
+
console.log(` log: ${daemon.logPath}`);
|
|
577
|
+
console.log(` stop: kill ${daemon.pid}`);
|
|
578
|
+
return 0;
|
|
579
|
+
}
|
|
580
|
+
const handle = await cmdServe({ cwd: process.cwd(), home: homedir(), daemon: values.daemon });
|
|
581
|
+
console.log(`router listening on ${handle.routerPort}; litellm listening on ${handle.litellmPort}`);
|
|
582
|
+
// serve runs until it is killed, so the signal handlers ARE its normal exit
|
|
583
|
+
// path — without them `stop()` never runs and the run's temp directory
|
|
584
|
+
// survives. That directory holds the generated master key, and for a
|
|
585
|
+
// codex-oauth gateway the ChatGPT credential too; one was found in the
|
|
586
|
+
// system temp directory after a Ctrl-C.
|
|
587
|
+
let stopping = false;
|
|
588
|
+
const shutdown = (signal) => {
|
|
589
|
+
if (stopping)
|
|
590
|
+
return;
|
|
591
|
+
stopping = true;
|
|
592
|
+
void handle.stop().finally(() => process.kill(process.pid, signal));
|
|
593
|
+
};
|
|
594
|
+
process.once('SIGINT', shutdown);
|
|
595
|
+
process.once('SIGTERM', shutdown);
|
|
596
|
+
process.once('SIGHUP', shutdown);
|
|
597
|
+
await new Promise(() => { });
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
if (command === 'code') {
|
|
601
|
+
const passthrough = rest[0] === '--' ? rest.slice(1) : rest;
|
|
602
|
+
await cmdCode({ cwd: process.cwd(), home: homedir(), passthrough });
|
|
603
|
+
return 0;
|
|
604
|
+
}
|
|
605
|
+
if (command === 'route') {
|
|
606
|
+
const action = rest[0];
|
|
607
|
+
const global = rest.includes('--global');
|
|
608
|
+
const scope = global ? 'global' : 'project';
|
|
609
|
+
const opts = { cwd: process.cwd(), home: homedir(), packageRoot: packageRoot(), scope };
|
|
610
|
+
// The two session-<phase> actions are the body of the auto-mode hooks, not
|
|
611
|
+
// a surface for people: they take the session id the hook read from stdin.
|
|
612
|
+
if (action === 'session-start' || action === 'session-end') {
|
|
613
|
+
const id = rest[rest.indexOf('--id') + 1];
|
|
614
|
+
if (!rest.includes('--id') || !id)
|
|
615
|
+
throw new Error(`sonata route ${action} requires --id <session-id>`);
|
|
616
|
+
const self = fileURLToPath(import.meta.url);
|
|
617
|
+
const res = await cmdRouteSession(action === 'session-start' ? 'start' : 'end', id, { ...opts, serveArgv: [process.execPath, self, 'serve'] });
|
|
618
|
+
console.log(`routing ${res.routing}; ${res.sessions} session(s) routed`);
|
|
619
|
+
return 0;
|
|
620
|
+
}
|
|
621
|
+
// Also the body of a hook, not a surface for people: routing follows the
|
|
622
|
+
// foreign-model subagents that need it, turned on at SubagentStart and off
|
|
623
|
+
// when the last one stops.
|
|
624
|
+
if (action === 'subagent-start' || action === 'subagent-stop') {
|
|
625
|
+
const id = rest[rest.indexOf('--id') + 1];
|
|
626
|
+
if (!rest.includes('--id') || !id)
|
|
627
|
+
throw new Error(`sonata route ${action} requires --id <agent-id>`);
|
|
628
|
+
const res = await cmdRouteSubagent(action === 'subagent-start' ? 'start' : 'stop', id, opts);
|
|
629
|
+
console.log(`routing ${res.routing}; ${res.subagents} subagent(s) running`);
|
|
630
|
+
return 0;
|
|
631
|
+
}
|
|
632
|
+
if (!action || !['on', 'off', 'status', 'auto', 'manual'].includes(action)) {
|
|
633
|
+
throw new Error('sonata route requires one of: on | off | status | auto | manual');
|
|
634
|
+
}
|
|
635
|
+
const status = await cmdRoute(action, opts);
|
|
636
|
+
if (status) {
|
|
637
|
+
const scopeLabel = global ? 'globally' : 'in this project';
|
|
638
|
+
console.log(status.on
|
|
639
|
+
? `claude sessions ${scopeLabel} route through sonata serve`
|
|
640
|
+
: `claude sessions ${scopeLabel} use their default API endpoint`);
|
|
641
|
+
if (status.on)
|
|
642
|
+
console.log(` router: http://localhost:${status.port}`);
|
|
643
|
+
if (action === 'status') {
|
|
644
|
+
const active = ['project', 'global'].filter((name) => status.scopes[name].on || status.scopes[name].auto);
|
|
645
|
+
console.log(` scopes: ${active.length > 0 ? active.join(', ') : 'none'}`);
|
|
646
|
+
console.log(' see `sonata status` for what the router has actually served recently');
|
|
647
|
+
}
|
|
648
|
+
if (status.auto) {
|
|
649
|
+
console.log(' auto: on — routing turns on only while a foreign-model subagent runs');
|
|
650
|
+
console.log(' (every session keeps Remote Control: none launches into a routed file)');
|
|
651
|
+
if (status.sessions > 0)
|
|
652
|
+
console.log(` live: ${status.sessions} routed session(s)`);
|
|
653
|
+
}
|
|
654
|
+
else if (action === 'manual') {
|
|
655
|
+
console.log(' auto: off — routing is whatever `sonata route on|off` last set');
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
if (command === 'restart') {
|
|
661
|
+
// Kills whatever sonata router currently holds the configured port (a
|
|
662
|
+
// stale daemon or an in-process native router) using
|
|
663
|
+
// only the pid `cmdServe` recorded for itself, then starts a fresh
|
|
664
|
+
// daemon. Plain `sonata serve --daemon` cannot do this: it just times
|
|
665
|
+
// out against `EADDRINUSE` with "the daemon did not answer", which reads
|
|
666
|
+
// as a startup failure rather than "something else is already there".
|
|
667
|
+
const self = fileURLToPath(import.meta.url);
|
|
668
|
+
const restarted = await cmdRestart(homedir(), [process.execPath, self, 'serve'], { cwd: process.cwd() });
|
|
669
|
+
console.log(`sonata serve restarted (pid ${restarted.pid}) on port ${restarted.port}`);
|
|
670
|
+
console.log(` log: ${restarted.logPath}`);
|
|
671
|
+
console.log(` stop: kill ${restarted.pid}`);
|
|
672
|
+
return 0;
|
|
673
|
+
}
|
|
674
|
+
if (command === 'gc') {
|
|
675
|
+
const killed = await cmdGc({ cwd: process.cwd() });
|
|
676
|
+
console.log(killed.length ? `killed ${killed.join(', ')}` : 'nothing to clean up');
|
|
677
|
+
return 0;
|
|
678
|
+
}
|
|
679
|
+
if (command === 'log') {
|
|
680
|
+
const { positionals } = parseArgs({ args: rest, allowPositionals: true, options: {} });
|
|
681
|
+
const id = positionals[0];
|
|
682
|
+
if (!id)
|
|
683
|
+
throw new Error('sonata log requires a run id');
|
|
684
|
+
const { cmdLog } = await import('./commands/log.js');
|
|
685
|
+
const res = cmdLog({ cwd: process.cwd(), id });
|
|
686
|
+
console.log(res.text);
|
|
687
|
+
return res.ok ? 0 : 1;
|
|
688
|
+
}
|
|
689
|
+
if (command === 'verify') {
|
|
690
|
+
const { values, positionals } = parseArgs({
|
|
691
|
+
args: rest, allowPositionals: true, options: { model: { type: 'string' } },
|
|
692
|
+
});
|
|
693
|
+
const id = positionals[0];
|
|
694
|
+
if (!id)
|
|
695
|
+
throw new Error('sonata verify requires a run id');
|
|
696
|
+
const { cmdVerify } = await import('./commands/verify.js');
|
|
697
|
+
const res = cmdVerify({ cwd: process.cwd(), id, model: values.model });
|
|
698
|
+
console.log(`${res.ok ? 'ok ' : 'FAIL'} ${res.detail}`);
|
|
699
|
+
return res.ok ? 0 : 1;
|
|
700
|
+
}
|
|
701
|
+
console.error(`sonata: unknown command "${command ?? ''}"`);
|
|
702
|
+
return 2;
|
|
703
|
+
}
|
|
704
|
+
async function readAuthKey() {
|
|
705
|
+
if (!process.stdin.isTTY) {
|
|
706
|
+
return readFileSync(0, 'utf8').split(/\r?\n/, 1)[0].trim();
|
|
707
|
+
}
|
|
708
|
+
const stdin = process.stdin;
|
|
709
|
+
const stdout = process.stdout;
|
|
710
|
+
stdout.write('Key: ');
|
|
711
|
+
stdin.setRawMode?.(true);
|
|
712
|
+
stdin.resume();
|
|
713
|
+
let value = '';
|
|
714
|
+
try {
|
|
715
|
+
for await (const chunk of stdin.iterator({ destroyOnReturn: false })) {
|
|
716
|
+
const text = String(chunk);
|
|
717
|
+
for (const char of text) {
|
|
718
|
+
if (char === '\n' || char === '\r')
|
|
719
|
+
return value.trim();
|
|
720
|
+
if (char === '\u0003')
|
|
721
|
+
throw new Error('sonata auth add cancelled');
|
|
722
|
+
if (char === '\u007f' || char === '\b')
|
|
723
|
+
value = value.slice(0, -1);
|
|
724
|
+
else
|
|
725
|
+
value += char;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
finally {
|
|
730
|
+
stdin.setRawMode?.(false);
|
|
731
|
+
stdin.pause();
|
|
732
|
+
stdout.write('\n');
|
|
733
|
+
}
|
|
734
|
+
return value.trim();
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* True when this module was executed as the program, rather than imported.
|
|
738
|
+
*
|
|
739
|
+
* Compares REAL paths. `sonata` on PATH is a symlink into node_modules, so
|
|
740
|
+
* node sets argv[1] to the symlink while import.meta.url resolves to the file
|
|
741
|
+
* it points at — a raw string comparison never matches, and every command
|
|
742
|
+
* exits 0 having done nothing. That shipped: the guard exists so a test can
|
|
743
|
+
* import this module without running the CLI, and the first version of it
|
|
744
|
+
* killed the CLI instead. Nothing caught it, because every test imports.
|
|
745
|
+
*/
|
|
746
|
+
export function invokedAsProgram(argv1, moduleUrl) {
|
|
747
|
+
if (!argv1)
|
|
748
|
+
return false;
|
|
749
|
+
try {
|
|
750
|
+
return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
// One of the paths does not resolve, so this is not the program. Staying
|
|
754
|
+
// silent beats running twice.
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (invokedAsProgram(process.argv[1], import.meta.url)) {
|
|
759
|
+
main(process.argv.slice(2))
|
|
760
|
+
.then((code) => process.exit(code))
|
|
761
|
+
.catch((err) => {
|
|
762
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
763
|
+
process.exit(1);
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
//# sourceMappingURL=cli.js.map
|