pantheon-opencode 1.6.0-beta.2 → 1.6.0-beta.3
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/CHANGELOG.md +90 -0
- package/README.md +28 -8
- package/README.pt-BR.md +26 -6
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/src/pantheon/delegation-enforce.ts +10 -1
- package/src/pantheon/v2-tools.ts +426 -0
- package/src/plugin-v2.ts +344 -134
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2 tool surface — the Pantheon tools that are self-sufficient on the
|
|
3
|
+
* OpenCode V2 host, declared for `ctx.tool.transform`'s `draft.add`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why only three tools
|
|
6
|
+
*
|
|
7
|
+
* A V2 tool can only be served here when it needs no V1 infrastructure. The
|
|
8
|
+
* three goal tools (`pantheon_goal_create`, `pantheon_goal_get`,
|
|
9
|
+
* `pantheon_goal_update`) are NOT: `GoalLoopDeps` requires a `GoalStore`, a
|
|
10
|
+
* `GoalLoopClient` and a `BackgroundJobBoard`, none of which exist on the V2
|
|
11
|
+
* `PluginContext`, and `getV2BridgeFromContext` resolves to `null` outside V1.
|
|
12
|
+
* They are therefore absent from this surface rather than registered as
|
|
13
|
+
* placeholders that resolve to a message — a placeholder still fails the host's
|
|
14
|
+
* output contract on every call, so it buys nothing. See
|
|
15
|
+
* `V2_UNSUPPORTED_FEATURES` in `../plugin-v2.ts` (marker `goal-tools`).
|
|
16
|
+
*
|
|
17
|
+
* `hashline_edit` (pure filesystem), `pantheon_cost` (reads `opencode.db`) and
|
|
18
|
+
* `pantheon_model` (reads/writes `active-preset.json`) need none of that, so
|
|
19
|
+
* they are wired to their real V1 implementations.
|
|
20
|
+
*
|
|
21
|
+
* ## The output declaration is mandatory
|
|
22
|
+
*
|
|
23
|
+
* The host enforces a biconditional between a tool's declared `output` and the
|
|
24
|
+
* value its `execute` resolves to (opencode 2.0.22):
|
|
25
|
+
*
|
|
26
|
+
* `def.output === undefined` and `'output' in result` -> throws
|
|
27
|
+
* "Tool result declared output without an output schema"
|
|
28
|
+
* `def.output !== undefined` and `!('output' in result)` -> throws
|
|
29
|
+
* "Tool did not return its declared output"
|
|
30
|
+
*
|
|
31
|
+
* Declaring `output` and returning it are therefore one decision, not two. The
|
|
32
|
+
* first branch is the historical Pantheon defect; the second is why the two
|
|
33
|
+
* must move together. `V2ToolDef` makes the field required so TypeScript
|
|
34
|
+
* rejects a tool that forgets it.
|
|
35
|
+
*
|
|
36
|
+
* @module pantheon/v2-tools
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { createCostCommand } from './cost-command.ts'
|
|
40
|
+
import { createHashlineEditTool, type HashlineToolResult } from './hashline/tool.ts'
|
|
41
|
+
import { createModelCommand } from './model-command.ts'
|
|
42
|
+
import type { ToolContextLike } from './tool-context.ts'
|
|
43
|
+
|
|
44
|
+
// ─── Contract types ─────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* V2 tool result shape.
|
|
48
|
+
*
|
|
49
|
+
* The V1 SDK types `ToolResult` as `string | { output: string, ... }`, but the
|
|
50
|
+
* V2 beta host reads a field off the resolved value and throws when a tool
|
|
51
|
+
* resolves to a bare string. Always resolve to the object shape — it satisfies
|
|
52
|
+
* both the SDK union and the beta host.
|
|
53
|
+
*
|
|
54
|
+
* `title` and `metadata` are part of the shape because `hashline_edit` returns
|
|
55
|
+
* them on success (see `HashlineToolResult`); the host spreads `metadata`
|
|
56
|
+
* through when present, so they are carried rather than dropped.
|
|
57
|
+
*/
|
|
58
|
+
export interface V2ToolResult {
|
|
59
|
+
output: string
|
|
60
|
+
title?: string
|
|
61
|
+
metadata?: Record<string, unknown>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A V2 tool definition ready for registration.
|
|
66
|
+
*
|
|
67
|
+
* `output` is REQUIRED, not optional: an omitted declaration is precisely the
|
|
68
|
+
* condition the host rejects at runtime, so making it optional would let the
|
|
69
|
+
* defect return silently.
|
|
70
|
+
*/
|
|
71
|
+
export interface V2ToolDef {
|
|
72
|
+
name: string
|
|
73
|
+
description: string
|
|
74
|
+
input: Record<string, unknown>
|
|
75
|
+
output: Record<string, unknown>
|
|
76
|
+
execute: (input: Record<string, unknown>, context: unknown) => Promise<V2ToolResult>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Context adapter ────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Project the host's V2 tool context onto the structural `ToolContextLike`.
|
|
83
|
+
*
|
|
84
|
+
* The host may omit `directory`/`worktree`; `hashline_edit` then falls back to
|
|
85
|
+
* `process.cwd()` through its own `resolveFile` containment guard, so an absent
|
|
86
|
+
* field degrades safely rather than throwing.
|
|
87
|
+
*/
|
|
88
|
+
function toolContext(context: unknown): ToolContextLike {
|
|
89
|
+
const ctx = (context ?? {}) as Partial<ToolContextLike>
|
|
90
|
+
return {
|
|
91
|
+
sessionID: typeof ctx.sessionID === 'string' ? ctx.sessionID : 'v2-unknown',
|
|
92
|
+
// Spread rather than assign `undefined`: the project compiles with
|
|
93
|
+
// exactOptionalPropertyTypes, so an explicit undefined is not assignable.
|
|
94
|
+
...(typeof ctx.directory === 'string' ? { directory: ctx.directory } : {}),
|
|
95
|
+
...(typeof ctx.worktree === 'string' ? { worktree: ctx.worktree } : {}),
|
|
96
|
+
...(typeof ctx.agent === 'string' ? { agent: ctx.agent } : {}),
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Error text for an unexpected throw, without leaking a stack trace. */
|
|
101
|
+
function errorText(prefix: string, err: unknown): string {
|
|
102
|
+
return `${prefix}: ${err instanceof Error ? err.message : String(err)}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── hashline_edit ──────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/** JSON Schema for `hashline_edit`, mirroring the V1 zod args exactly. */
|
|
108
|
+
const HASHLINE_EDIT_INPUT: Record<string, unknown> = {
|
|
109
|
+
type: 'object',
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
required: ['file', 'edits'],
|
|
112
|
+
properties: {
|
|
113
|
+
file: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
minLength: 1,
|
|
116
|
+
description: 'File to edit. Absolute, or relative to the worktree.',
|
|
117
|
+
},
|
|
118
|
+
edits: {
|
|
119
|
+
type: 'array',
|
|
120
|
+
minItems: 1,
|
|
121
|
+
description:
|
|
122
|
+
'Edits to apply. All refs are validated against the ORIGINAL file before anything is written.',
|
|
123
|
+
items: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
additionalProperties: false,
|
|
126
|
+
required: ['op', 'ref'],
|
|
127
|
+
properties: {
|
|
128
|
+
op: { type: 'string', enum: ['replace', 'append', 'prepend', 'delete'] },
|
|
129
|
+
ref: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
minLength: 1,
|
|
132
|
+
description: 'Anchor ref "LINE#TAG" against the ORIGINAL snapshot.',
|
|
133
|
+
},
|
|
134
|
+
endRef: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
minLength: 1,
|
|
137
|
+
description: 'Optional range end for replace/delete, as "LINE#TAG".',
|
|
138
|
+
},
|
|
139
|
+
lines: {
|
|
140
|
+
type: 'array',
|
|
141
|
+
items: { type: 'string' },
|
|
142
|
+
description: 'Replacement lines (replace only).',
|
|
143
|
+
},
|
|
144
|
+
content: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
description: 'Text to insert (append/prepend); split on newlines.',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Coerced `hashline_edit` args, structurally equal to the V1 zod inference. */
|
|
155
|
+
interface HashlineEditArgs {
|
|
156
|
+
file: string
|
|
157
|
+
edits: Array<{
|
|
158
|
+
op: 'replace' | 'append' | 'prepend' | 'delete'
|
|
159
|
+
ref: string
|
|
160
|
+
endRef?: string
|
|
161
|
+
lines?: string[]
|
|
162
|
+
content?: string
|
|
163
|
+
}>
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const HASHLINE_OPS = new Set(['replace', 'append', 'prepend', 'delete'])
|
|
167
|
+
|
|
168
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
169
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Re-validate the host-provided input against the declared JSON Schema.
|
|
174
|
+
*
|
|
175
|
+
* The host validates `input` before `execute`, but this wrapper is also the
|
|
176
|
+
* direct programmatic entry point (tests, embedded callers), so it must not
|
|
177
|
+
* assume a pre-validated shape. Failures are returned as error-as-text, the
|
|
178
|
+
* same contract the V1 tool uses for user errors.
|
|
179
|
+
*/
|
|
180
|
+
function coerceHashlineEditArgs(input: Record<string, unknown>):
|
|
181
|
+
| {
|
|
182
|
+
ok: true
|
|
183
|
+
args: HashlineEditArgs
|
|
184
|
+
}
|
|
185
|
+
| { ok: false; error: string } {
|
|
186
|
+
const file = input.file
|
|
187
|
+
if (typeof file !== 'string' || file.length === 0) {
|
|
188
|
+
return { ok: false, error: 'hashline_edit: "file" must be a non-empty string' }
|
|
189
|
+
}
|
|
190
|
+
const rawEdits = input.edits
|
|
191
|
+
if (!Array.isArray(rawEdits) || rawEdits.length === 0) {
|
|
192
|
+
return { ok: false, error: 'hashline_edit: "edits" must be a non-empty array' }
|
|
193
|
+
}
|
|
194
|
+
const edits: HashlineEditArgs['edits'] = []
|
|
195
|
+
for (const [i, raw] of rawEdits.entries()) {
|
|
196
|
+
if (!isRecord(raw)) {
|
|
197
|
+
return { ok: false, error: `hashline_edit: edit (index ${i}) must be an object` }
|
|
198
|
+
}
|
|
199
|
+
const op = raw.op
|
|
200
|
+
if (typeof op !== 'string' || !HASHLINE_OPS.has(op)) {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: `hashline_edit: edit (index ${i}) "op" must be one of replace, append, prepend, delete`,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const ref = raw.ref
|
|
207
|
+
if (typeof ref !== 'string' || ref.length === 0) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
error: `hashline_edit: edit (index ${i}) "ref" must be a non-empty string`,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let lines: string[] | undefined
|
|
214
|
+
if (raw.lines !== undefined) {
|
|
215
|
+
if (!Array.isArray(raw.lines) || raw.lines.some((l) => typeof l !== 'string')) {
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
error: `hashline_edit: edit (index ${i}) "lines" must be a string array`,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
lines = raw.lines as string[]
|
|
222
|
+
}
|
|
223
|
+
if (raw.content !== undefined && typeof raw.content !== 'string') {
|
|
224
|
+
return { ok: false, error: `hashline_edit: edit (index ${i}) "content" must be a string` }
|
|
225
|
+
}
|
|
226
|
+
if (raw.endRef !== undefined && typeof raw.endRef !== 'string') {
|
|
227
|
+
return { ok: false, error: `hashline_edit: edit (index ${i}) "endRef" must be a string` }
|
|
228
|
+
}
|
|
229
|
+
edits.push({
|
|
230
|
+
op: op as HashlineEditArgs['edits'][number]['op'],
|
|
231
|
+
ref,
|
|
232
|
+
// Conditional spread: exactOptionalPropertyTypes forbids `prop: undefined`.
|
|
233
|
+
...(typeof raw.endRef === 'string' ? { endRef: raw.endRef } : {}),
|
|
234
|
+
...(lines === undefined ? {} : { lines }),
|
|
235
|
+
...(typeof raw.content === 'string' ? { content: raw.content } : {}),
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
return { ok: true, args: { file, edits } }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const hashlineEdit = createHashlineEditTool()
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Wrap the V1 `hashline_edit` tool for the V2 surface.
|
|
245
|
+
*
|
|
246
|
+
* The V1 tool returns error-as-text for user errors and the success object
|
|
247
|
+
* `{ title, output, metadata }`. Both are carried: the `output` string always
|
|
248
|
+
* satisfies the host's declaration, and `title`/`metadata` survive because the
|
|
249
|
+
* host spreads metadata through.
|
|
250
|
+
*/
|
|
251
|
+
async function executeHashlineEdit(
|
|
252
|
+
input: Record<string, unknown>,
|
|
253
|
+
context: unknown,
|
|
254
|
+
): Promise<V2ToolResult> {
|
|
255
|
+
const coerced = coerceHashlineEditArgs(input)
|
|
256
|
+
if (!coerced.ok) return { output: coerced.error }
|
|
257
|
+
let result: HashlineToolResult
|
|
258
|
+
try {
|
|
259
|
+
result = await hashlineEdit.execute(coerced.args, toolContext(context))
|
|
260
|
+
} catch (err: unknown) {
|
|
261
|
+
// The V1 tool does not throw for user errors; this is defence in depth so
|
|
262
|
+
// an unexpected throw surfaces as text instead of an absent tool result.
|
|
263
|
+
return { output: errorText('hashline_edit failed', err) }
|
|
264
|
+
}
|
|
265
|
+
if (typeof result === 'string') return { output: result }
|
|
266
|
+
return {
|
|
267
|
+
output: result.output,
|
|
268
|
+
...(result.title === undefined ? {} : { title: result.title }),
|
|
269
|
+
...(result.metadata === undefined ? {} : { metadata: result.metadata }),
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── pantheon_cost ──────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
/** JSON Schema for `pantheon_cost`, mirroring the V1 zod args. */
|
|
276
|
+
const COST_INPUT: Record<string, unknown> = {
|
|
277
|
+
type: 'object',
|
|
278
|
+
additionalProperties: false,
|
|
279
|
+
properties: {
|
|
280
|
+
days: {
|
|
281
|
+
type: 'integer',
|
|
282
|
+
minimum: 1,
|
|
283
|
+
maximum: 365,
|
|
284
|
+
description: 'Reporting window in days (default 7).',
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Validate `days` exactly as the V1 zod schema does; absent means default. */
|
|
290
|
+
function coerceDays(value: unknown): { ok: true; days?: number } | { ok: false; error: string } {
|
|
291
|
+
if (value === undefined) return { ok: true }
|
|
292
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 365) {
|
|
293
|
+
return { ok: false, error: 'pantheon_cost: "days" must be an integer between 1 and 365' }
|
|
294
|
+
}
|
|
295
|
+
return { ok: true, days: value }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const costCommand = createCostCommand()
|
|
299
|
+
|
|
300
|
+
async function executeCost(
|
|
301
|
+
input: Record<string, unknown>,
|
|
302
|
+
context: unknown,
|
|
303
|
+
): Promise<V2ToolResult> {
|
|
304
|
+
const days = coerceDays(input.days)
|
|
305
|
+
if (!days.ok) return { output: days.error }
|
|
306
|
+
// The V1 tool already wraps its whole body and reports failures as text.
|
|
307
|
+
const output = await costCommand.pantheon_cost.execute({ days: days.days }, toolContext(context))
|
|
308
|
+
return { output }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── pantheon_model ─────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
/** JSON Schema for `pantheon_model`, mirroring the V1 zod args. */
|
|
314
|
+
const MODEL_INPUT: Record<string, unknown> = {
|
|
315
|
+
type: 'object',
|
|
316
|
+
additionalProperties: false,
|
|
317
|
+
properties: {
|
|
318
|
+
action: {
|
|
319
|
+
type: 'string',
|
|
320
|
+
enum: ['status', 'show', 'set', 'reset'],
|
|
321
|
+
description: 'Operation to perform. Defaults to status when agent/model are absent.',
|
|
322
|
+
},
|
|
323
|
+
agent: { type: 'string', description: 'Agent name (one of the 14 canonical agents).' },
|
|
324
|
+
model: { type: 'string', description: 'Model in provider/model-id format; used by set.' },
|
|
325
|
+
effort: {
|
|
326
|
+
type: 'string',
|
|
327
|
+
enum: ['low', 'medium', 'high'],
|
|
328
|
+
description: 'Reasoning effort (variant), clamped to model capability.',
|
|
329
|
+
},
|
|
330
|
+
scope: {
|
|
331
|
+
type: 'string',
|
|
332
|
+
enum: ['project', 'global'],
|
|
333
|
+
description: 'Scope for active-preset.json (default project).',
|
|
334
|
+
},
|
|
335
|
+
confirm: {
|
|
336
|
+
type: 'boolean',
|
|
337
|
+
description: 'Explicit confirmation for a mutating operation; required for global changes.',
|
|
338
|
+
},
|
|
339
|
+
authorize_global: {
|
|
340
|
+
type: 'boolean',
|
|
341
|
+
description: 'Separate authorization for global configuration; not implied by confirm.',
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const WIZARD_UNAVAILABLE =
|
|
347
|
+
'the interactive wizard has no user in the V2 tool surface — pass action="status" to read ' +
|
|
348
|
+
'the current overrides, or pass agent and model explicitly for action="set"'
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The V1 command falls back to an interactive readline prompt when invoked
|
|
352
|
+
* with no arguments. A tool call has no user at the terminal, so that fallback
|
|
353
|
+
* would block the call until it timed out. `ask` is injected to refuse
|
|
354
|
+
* deterministically instead, which routes through the command's own
|
|
355
|
+
* `wizard canceled` error path rather than forking its logic.
|
|
356
|
+
*/
|
|
357
|
+
const modelCommand = createModelCommand({
|
|
358
|
+
ask: () => Promise.reject(new Error(WIZARD_UNAVAILABLE)),
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
async function executeModel(
|
|
362
|
+
input: Record<string, unknown>,
|
|
363
|
+
context: unknown,
|
|
364
|
+
): Promise<V2ToolResult> {
|
|
365
|
+
// The V1 tool validates its own args (zod) and reports failures as text.
|
|
366
|
+
const output = await modelCommand.pantheon_model.execute(
|
|
367
|
+
input as Parameters<typeof modelCommand.pantheon_model.execute>[0],
|
|
368
|
+
toolContext(context),
|
|
369
|
+
)
|
|
370
|
+
return { output }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── Tool set ───────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Build the V2 tool definitions.
|
|
377
|
+
*
|
|
378
|
+
* Every entry declares `output`. An empty object is the host's own marker for
|
|
379
|
+
* "declared, unconstrained" (its MCP bridge uses `output: g.outputSchema ??
|
|
380
|
+
* {}`), so it satisfies the declaration without adding a second way for a tool
|
|
381
|
+
* to fail validation. The protection against forgetting the field is the
|
|
382
|
+
* required `output` on `V2ToolDef`, not the emptiness of this schema.
|
|
383
|
+
*
|
|
384
|
+
* The three tool instances (`hashlineEdit`, `costCommand`, `modelCommand`) are
|
|
385
|
+
* module-level consts created once at import, not per call. What matters for
|
|
386
|
+
* this function is only that it allocates a fresh descriptor array each call,
|
|
387
|
+
* so a caller cannot mutate another's definitions. None of the three factories
|
|
388
|
+
* performs filesystem or database work at construction, so importing this
|
|
389
|
+
* module stays side-effect free.
|
|
390
|
+
*/
|
|
391
|
+
export function createV2ToolDefinitions(): V2ToolDef[] {
|
|
392
|
+
return [
|
|
393
|
+
{
|
|
394
|
+
name: 'hashline_edit',
|
|
395
|
+
description:
|
|
396
|
+
'Edit a file anchored by hashline refs (LINE#TAG) instead of raw line numbers. ' +
|
|
397
|
+
'Ops: replace (ref..endRef or single ref → lines), append/prepend (anchor ref → content), ' +
|
|
398
|
+
'delete (ref..endRef). ALL refs must be validated against the ORIGINAL file before ' +
|
|
399
|
+
'anything is written; on mismatch the tool returns an error with a re-tagged excerpt ' +
|
|
400
|
+
'and a Did-you-mean suggestion, and nothing is modified.',
|
|
401
|
+
input: HASHLINE_EDIT_INPUT,
|
|
402
|
+
output: {},
|
|
403
|
+
execute: executeHashlineEdit,
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'pantheon_cost',
|
|
407
|
+
description:
|
|
408
|
+
'Report input, output, and total token usage by agent and phase over the last N days, ' +
|
|
409
|
+
'read from opencode.db (read-only, no monetary values).',
|
|
410
|
+
input: COST_INPUT,
|
|
411
|
+
output: {},
|
|
412
|
+
execute: executeCost,
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: 'pantheon_model',
|
|
416
|
+
description:
|
|
417
|
+
'Show, set, or reset per-agent model overrides in active-preset.json (project or global). ' +
|
|
418
|
+
'Overrides win over preset; env preset wins over file. Never writes .env or top-level ' +
|
|
419
|
+
'model/small_model. Pass action="status" to read the current overrides; unlike the V1 ' +
|
|
420
|
+
'command there is no interactive wizard on this surface.',
|
|
421
|
+
input: MODEL_INPUT,
|
|
422
|
+
output: {},
|
|
423
|
+
execute: executeModel,
|
|
424
|
+
},
|
|
425
|
+
]
|
|
426
|
+
}
|