imprint-mcp 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/package.json +1 -1
- package/prompts/audit-agent.md +6 -5
- package/prompts/build-planning.md +2 -2
- package/prompts/compile-agent.md +6 -4
- package/prompts/live-verifier-agent.md +24 -11
- package/src/cli.ts +41 -6
- package/src/imprint/agent.ts +7 -2
- package/src/imprint/audit.ts +255 -42
- package/src/imprint/auth-compile-tools.ts +64 -13
- package/src/imprint/claude-cli-compile.ts +43 -7
- package/src/imprint/codex-cli-compile.ts +205 -129
- package/src/imprint/compile-agent-types.ts +25 -2
- package/src/imprint/compile-agent.ts +197 -40
- package/src/imprint/compile-tools.ts +164 -22
- package/src/imprint/inject-listener.ts +31 -3
- package/src/imprint/live-verifier.ts +167 -6
- package/src/imprint/llm.ts +7 -3
- package/src/imprint/mcp-compile-server.ts +188 -83
- package/src/imprint/mcp-server.ts +16 -1
- package/src/imprint/probe-backends.ts +60 -25
- package/src/imprint/react-flight-limits.ts +31 -0
- package/src/imprint/record.ts +657 -46
- package/src/imprint/redact.ts +240 -30
- package/src/imprint/response-body-intent.ts +73 -0
- package/src/imprint/response-body-lifecycle.ts +32 -0
- package/src/imprint/response-body-stream.ts +704 -0
- package/src/imprint/teach-state.ts +52 -2
- package/src/imprint/teach.ts +94 -69
- package/src/imprint/tracing.ts +60 -238
|
@@ -5,13 +5,20 @@
|
|
|
5
5
|
* `[IMPRINT]` is exact-match — keep stable.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export function createInjectedListenerSource(eventToken: string): string {
|
|
9
|
+
return `
|
|
9
10
|
(function imprintInjector() {
|
|
10
11
|
if (window.__imprint_injected__) return;
|
|
11
12
|
window.__imprint_injected__ = true;
|
|
12
13
|
|
|
13
14
|
const SENTINEL = '[IMPRINT]';
|
|
15
|
+
const EVENT_TOKEN = ${JSON.stringify(eventToken)};
|
|
16
|
+
const emitLog = console.log.bind(console);
|
|
17
|
+
const stringify = JSON.stringify.bind(JSON);
|
|
14
18
|
const MAX_VAL = 200;
|
|
19
|
+
let lastIntentTarget = null;
|
|
20
|
+
let lastIntentAt = 0;
|
|
21
|
+
let lastIntentKind = null;
|
|
15
22
|
|
|
16
23
|
function safeStr(v) {
|
|
17
24
|
try {
|
|
@@ -53,6 +60,7 @@ export const INJECTED_LISTENER_SOURCE = `
|
|
|
53
60
|
text: safeStr((el.textContent || '').trim()),
|
|
54
61
|
ariaLabel: el.getAttribute && el.getAttribute('aria-label') || null,
|
|
55
62
|
href: el.tagName === 'A' ? el.getAttribute('href') : null,
|
|
63
|
+
absoluteHref: el.tagName === 'A' ? el.href : null,
|
|
56
64
|
selector: selectorFor(el),
|
|
57
65
|
};
|
|
58
66
|
} catch (e) { return {}; }
|
|
@@ -60,17 +68,33 @@ export const INJECTED_LISTENER_SOURCE = `
|
|
|
60
68
|
|
|
61
69
|
function emit(type, payload) {
|
|
62
70
|
try {
|
|
63
|
-
|
|
71
|
+
emitLog(SENTINEL, EVENT_TOKEN, type, stringify(payload));
|
|
64
72
|
} catch (e) { /* ignore */ }
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
function onClick(ev) {
|
|
68
76
|
try {
|
|
69
|
-
|
|
77
|
+
onIntent(ev, 'click');
|
|
78
|
+
const tgt = ev.target && ev.target.closest ? ev.target.closest('a,button,input,form') || ev.target : ev.target;
|
|
70
79
|
emit('click', describe(tgt));
|
|
71
80
|
} catch (e) { /* ignore */ }
|
|
72
81
|
}
|
|
73
82
|
|
|
83
|
+
function onIntent(ev, intentKind) {
|
|
84
|
+
try {
|
|
85
|
+
if (!ev.isTrusted) return;
|
|
86
|
+
const tgt = ev.target && ev.target.closest ? ev.target.closest('a,button,input,form') : ev.target;
|
|
87
|
+
if (!tgt) return;
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const activation = intentKind === 'pointerdown' || intentKind === 'click';
|
|
90
|
+
if (!activation && tgt === lastIntentTarget && intentKind === lastIntentKind && now - lastIntentAt < 250) return;
|
|
91
|
+
lastIntentTarget = tgt;
|
|
92
|
+
lastIntentAt = now;
|
|
93
|
+
lastIntentKind = intentKind;
|
|
94
|
+
emit('intent', { ...describe(tgt), intentAt: now, intentKind: intentKind });
|
|
95
|
+
} catch (e) { /* ignore */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
function onInput(ev) {
|
|
75
99
|
try {
|
|
76
100
|
const tgt = ev.target;
|
|
@@ -128,10 +152,14 @@ export const INJECTED_LISTENER_SOURCE = `
|
|
|
128
152
|
// Capture phase = true so we see the event before the site has a chance to
|
|
129
153
|
// stopPropagation it.
|
|
130
154
|
document.addEventListener('click', onClick, true);
|
|
155
|
+
document.addEventListener('pointerover', (ev) => onIntent(ev, 'pointerover'), true);
|
|
156
|
+
document.addEventListener('pointerdown', (ev) => onIntent(ev, 'pointerdown'), true);
|
|
157
|
+
document.addEventListener('focusin', (ev) => onIntent(ev, 'focusin'), true);
|
|
131
158
|
document.addEventListener('input', onInput, true);
|
|
132
159
|
document.addEventListener('change', onChange, true);
|
|
133
160
|
document.addEventListener('submit', onSubmit, true);
|
|
134
161
|
})();
|
|
135
162
|
`;
|
|
163
|
+
}
|
|
136
164
|
|
|
137
165
|
export const IMPRINT_SENTINEL = '[IMPRINT]';
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type probeResolvedTool,
|
|
33
33
|
rebindBackendsCacheToWorkflow,
|
|
34
34
|
} from './probe-backends.ts';
|
|
35
|
+
import { ensureImprintRuntimeLink } from './runtime-link.ts';
|
|
35
36
|
import { loadCredentialStore } from './runtime.ts';
|
|
36
37
|
import { isSensitiveKey } from './sensitive-keys.ts';
|
|
37
38
|
import { buildZodValidator } from './tool-loader.ts';
|
|
@@ -43,9 +44,18 @@ const SYSTEM_PROMPT_PATH = pathJoin(REPO_ROOT, 'prompts', 'live-verifier-agent.m
|
|
|
43
44
|
const MCP_SERVER_NAME = 'imprint-live-verifier';
|
|
44
45
|
export const LIVE_VERIFICATION_EVIDENCE_FILE = '.live-verification-evidence.json';
|
|
45
46
|
const LIVE_VERIFIER_LOG_FILE = '.live-verifier-log.jsonl';
|
|
46
|
-
|
|
47
|
+
// Cheap probes normally finish well inside the original two-minute window and
|
|
48
|
+
// skip browser transports. The larger ceiling is for the fallback path: a cold
|
|
49
|
+
// CDP request must finish before fetch-bootstrap gets its own browser attempt.
|
|
50
|
+
// Comprehensive preparation is sequential on purpose: concurrent browser
|
|
51
|
+
// transports can trip shared anti-bot/session defenses. Reserve enough time
|
|
52
|
+
// for the normal fetch/stealth/playbook candidates plus CDP and fetch-bootstrap
|
|
53
|
+
// fallbacks; the former 240s cap could kill CDP after a 150s playbook attempt.
|
|
54
|
+
const BACKEND_PREPARATION_BUDGET_MS = 8 * 60_000;
|
|
47
55
|
const MAX_CREDITED_BACKEND_PREPARATIONS = 2;
|
|
48
56
|
const BACKEND_PROBE_SHUTDOWN_GRACE_MS = 5_000;
|
|
57
|
+
const MAX_PROMPT_EVIDENCE_RECORDS = 48;
|
|
58
|
+
const MAX_PROMPT_EVIDENCE_RECORD_CHARS = 8_000;
|
|
49
59
|
let evidenceTempSequence = 0;
|
|
50
60
|
|
|
51
61
|
const BaselineSchema = z
|
|
@@ -158,6 +168,17 @@ export const LiveVerificationReportSchema = z
|
|
|
158
168
|
|
|
159
169
|
type LiveVerificationReport = z.infer<typeof LiveVerificationReportSchema>;
|
|
160
170
|
|
|
171
|
+
export function isInfrastructureOnlyInconclusiveReport(report: LiveVerificationReport): boolean {
|
|
172
|
+
return (
|
|
173
|
+
report.status === 'inconclusive' &&
|
|
174
|
+
report.baseline.verdict === 'infrastructure' &&
|
|
175
|
+
report.issues.length === 0 &&
|
|
176
|
+
report.parameters.every(
|
|
177
|
+
(parameter) => parameter.verdict === 'works' || parameter.verdict === 'untestable',
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
161
182
|
interface LiveIntegrationSuiteResult {
|
|
162
183
|
exitCode: number;
|
|
163
184
|
timedOut: boolean;
|
|
@@ -251,6 +272,11 @@ export async function runLiveIntegrationSuite(opts: {
|
|
|
251
272
|
reason?: string;
|
|
252
273
|
sessionLabel?: string;
|
|
253
274
|
}): Promise<LiveIntegrationSuiteResult> {
|
|
275
|
+
// The suite is launched directly from the generated tool directory, bypassing
|
|
276
|
+
// normal tool discovery. Repair a stale global/worktree runtime link here so
|
|
277
|
+
// imports such as `imprint/compile-verification` resolve against this running
|
|
278
|
+
// Imprint installation.
|
|
279
|
+
ensureImprintRuntimeLink(dirname(dirname(opts.toolDir)));
|
|
254
280
|
const integrationPath = pathJoin(opts.toolDir, 'integration.test.ts');
|
|
255
281
|
if (!existsSync(integrationPath)) throw new Error('integration.test.ts is missing');
|
|
256
282
|
const workflowPath = pathJoin(opts.toolDir, 'workflow.json');
|
|
@@ -461,7 +487,11 @@ export function readPersistedLiveVerificationEvidence(
|
|
|
461
487
|
|
|
462
488
|
export function hasSuiteReceiptForSession(path: string, sessionLabel: string): boolean {
|
|
463
489
|
return readPersistedLiveVerificationEvidence(path).some(
|
|
464
|
-
(item) =>
|
|
490
|
+
(item) =>
|
|
491
|
+
item.kind === 'suite' &&
|
|
492
|
+
item.verifierSession === sessionLabel &&
|
|
493
|
+
item.status !== 'running' &&
|
|
494
|
+
typeof item.finishedAt === 'string',
|
|
465
495
|
);
|
|
466
496
|
}
|
|
467
497
|
|
|
@@ -628,6 +658,11 @@ async function runBackendProbeSubprocess(opts: {
|
|
|
628
658
|
attempt?: number;
|
|
629
659
|
label: string;
|
|
630
660
|
}): Promise<Awaited<ReturnType<typeof probeResolvedTool>>> {
|
|
661
|
+
const previousCacheContents = existsSync(opts.outPath)
|
|
662
|
+
? readFileSync(opts.outPath, 'utf8')
|
|
663
|
+
: undefined;
|
|
664
|
+
const tool = resolveWorkflowTool(opts.workflowPath);
|
|
665
|
+
const assetRoot = pathJoin(tool.dir, '..', '..');
|
|
631
666
|
const child = spawn(
|
|
632
667
|
process.execPath,
|
|
633
668
|
[
|
|
@@ -674,6 +709,30 @@ async function runBackendProbeSubprocess(opts: {
|
|
|
674
709
|
unregisterParentCleanup();
|
|
675
710
|
}
|
|
676
711
|
if (outcome.timedOut) {
|
|
712
|
+
// probeResolvedTool checkpoints every successful candidate. If a later
|
|
713
|
+
// optional rung consumed the aggregate deadline, use the new valid cache
|
|
714
|
+
// instead of discarding an already-proven backend. Do not reuse an unchanged
|
|
715
|
+
// pre-probe cache: forceReprobe may have been requested because it failed.
|
|
716
|
+
const currentCacheContents = existsSync(opts.outPath)
|
|
717
|
+
? readFileSync(opts.outPath, 'utf8')
|
|
718
|
+
: undefined;
|
|
719
|
+
const cacheStatus = loadBackendsCacheStatus(tool.site, assetRoot, tool.dir, {
|
|
720
|
+
warn: false,
|
|
721
|
+
toolName: tool.workflow.toolName,
|
|
722
|
+
});
|
|
723
|
+
if (
|
|
724
|
+
currentCacheContents !== undefined &&
|
|
725
|
+
currentCacheContents !== previousCacheContents &&
|
|
726
|
+
cacheStatus.status === 'ok'
|
|
727
|
+
) {
|
|
728
|
+
appendLiveVerifierLog(opts.logPath, {
|
|
729
|
+
type: 'backend.probe.partial-cache-recovered',
|
|
730
|
+
attempt: opts.attempt,
|
|
731
|
+
label: opts.label,
|
|
732
|
+
preferredOrder: cacheStatus.cache.preferredOrder,
|
|
733
|
+
});
|
|
734
|
+
return { cache: cacheStatus.cache, outPath: opts.outPath };
|
|
735
|
+
}
|
|
677
736
|
throw new Error(
|
|
678
737
|
`backend probe exceeded its separate ${Math.round(BACKEND_PREPARATION_BUDGET_MS / 1_000)}s budget and was terminated`,
|
|
679
738
|
);
|
|
@@ -684,8 +743,6 @@ async function runBackendProbeSubprocess(opts: {
|
|
|
684
743
|
);
|
|
685
744
|
}
|
|
686
745
|
|
|
687
|
-
const tool = resolveWorkflowTool(opts.workflowPath);
|
|
688
|
-
const assetRoot = pathJoin(tool.dir, '..', '..');
|
|
689
746
|
const cacheStatus = loadBackendsCacheStatus(tool.site, assetRoot, tool.dir, {
|
|
690
747
|
warn: false,
|
|
691
748
|
toolName: tool.workflow.toolName,
|
|
@@ -701,6 +758,10 @@ interface LiveSemanticVerificationResult {
|
|
|
701
758
|
provider: ProviderName;
|
|
702
759
|
model: string;
|
|
703
760
|
attempts: number;
|
|
761
|
+
/** True only when an independent verifier returned a schema-valid report.
|
|
762
|
+
* Provider startup/input failures synthesize an inconclusive report for
|
|
763
|
+
* compiler feedback but do not consume a semantic-review cycle. */
|
|
764
|
+
completedReview: boolean;
|
|
704
765
|
}
|
|
705
766
|
|
|
706
767
|
export async function runLiveSemanticVerification(opts: {
|
|
@@ -828,6 +889,7 @@ export async function runLiveSemanticVerification(opts: {
|
|
|
828
889
|
provider: opts.provider,
|
|
829
890
|
model,
|
|
830
891
|
attempts: attempt,
|
|
892
|
+
completedReview: true,
|
|
831
893
|
};
|
|
832
894
|
} catch (error) {
|
|
833
895
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -890,7 +952,13 @@ export async function runLiveSemanticVerification(opts: {
|
|
|
890
952
|
status: 'inconclusive',
|
|
891
953
|
attempts: 2,
|
|
892
954
|
});
|
|
893
|
-
return {
|
|
955
|
+
return {
|
|
956
|
+
report: inconclusive,
|
|
957
|
+
provider: opts.provider,
|
|
958
|
+
model,
|
|
959
|
+
attempts: 2,
|
|
960
|
+
completedReview: false,
|
|
961
|
+
};
|
|
894
962
|
}
|
|
895
963
|
|
|
896
964
|
function mergePersistedRecords(
|
|
@@ -955,13 +1023,106 @@ function buildInitialMessage(
|
|
|
955
1023
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
956
1024
|
},
|
|
957
1025
|
artifactContext: buildVerifierArtifactContext(toolDir, workflow.toolName),
|
|
958
|
-
baselineEvidence: evidence,
|
|
1026
|
+
baselineEvidence: compactVerifierEvidenceContext(evidence),
|
|
959
1027
|
},
|
|
960
1028
|
null,
|
|
961
1029
|
2,
|
|
962
1030
|
)}`;
|
|
963
1031
|
}
|
|
964
1032
|
|
|
1033
|
+
/** Keep the verifier prompt bounded without weakening the durable evidence
|
|
1034
|
+
* artifact. Full, sanitized receipts remain on disk; the model gets recent
|
|
1035
|
+
* records with representative payload previews and navigation metadata. */
|
|
1036
|
+
export function compactVerifierEvidenceContext(
|
|
1037
|
+
evidence: Array<LiveIntegrationEvidence | PersistedLiveVerificationRecord>,
|
|
1038
|
+
): unknown[] {
|
|
1039
|
+
const omittedRecords = Math.max(0, evidence.length - MAX_PROMPT_EVIDENCE_RECORDS);
|
|
1040
|
+
const selected = evidence.slice(-MAX_PROMPT_EVIDENCE_RECORDS);
|
|
1041
|
+
const compacted = selected.map((record) => compactVerifierEvidenceRecord(record));
|
|
1042
|
+
return omittedRecords > 0
|
|
1043
|
+
? [
|
|
1044
|
+
{
|
|
1045
|
+
kind: 'prompt-compaction',
|
|
1046
|
+
omittedRecords,
|
|
1047
|
+
note: `Full receipts remain in ${LIVE_VERIFICATION_EVIDENCE_FILE}.`,
|
|
1048
|
+
},
|
|
1049
|
+
...compacted,
|
|
1050
|
+
]
|
|
1051
|
+
: compacted;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function compactVerifierEvidenceRecord(
|
|
1055
|
+
record: LiveIntegrationEvidence | PersistedLiveVerificationRecord,
|
|
1056
|
+
): unknown {
|
|
1057
|
+
const compacted = compactVerifierValue(record, 0);
|
|
1058
|
+
const serialized = JSON.stringify(compacted);
|
|
1059
|
+
if (serialized.length <= MAX_PROMPT_EVIDENCE_RECORD_CHARS) return compacted;
|
|
1060
|
+
|
|
1061
|
+
const navigationKeys = [
|
|
1062
|
+
'schemaVersion',
|
|
1063
|
+
'kind',
|
|
1064
|
+
'label',
|
|
1065
|
+
'caseName',
|
|
1066
|
+
'toolName',
|
|
1067
|
+
'status',
|
|
1068
|
+
'verifierSession',
|
|
1069
|
+
'startedAt',
|
|
1070
|
+
'finishedAt',
|
|
1071
|
+
'requestedParams',
|
|
1072
|
+
'effectiveParams',
|
|
1073
|
+
'preferredBackend',
|
|
1074
|
+
'usedBackend',
|
|
1075
|
+
'attempts',
|
|
1076
|
+
'durationMs',
|
|
1077
|
+
'exitCode',
|
|
1078
|
+
'timedOut',
|
|
1079
|
+
'passedTests',
|
|
1080
|
+
'failedTests',
|
|
1081
|
+
'completedCallLabels',
|
|
1082
|
+
'reason',
|
|
1083
|
+
'error',
|
|
1084
|
+
] as const;
|
|
1085
|
+
const source = record as Record<string, unknown>;
|
|
1086
|
+
const navigation: Record<string, unknown> = {};
|
|
1087
|
+
for (const key of navigationKeys) {
|
|
1088
|
+
if (source[key] !== undefined) navigation[key] = compactVerifierValue(source[key], 0);
|
|
1089
|
+
}
|
|
1090
|
+
return {
|
|
1091
|
+
...navigation,
|
|
1092
|
+
payloadPreview: clipVerifierString(serialized, MAX_PROMPT_EVIDENCE_RECORD_CHARS),
|
|
1093
|
+
promptCompacted: true,
|
|
1094
|
+
note: `Full receipt remains in ${LIVE_VERIFICATION_EVIDENCE_FILE}.`,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function compactVerifierValue(value: unknown, depth: number): unknown {
|
|
1099
|
+
if (typeof value === 'string') return clipVerifierString(value, 2_000);
|
|
1100
|
+
if (value === null || typeof value !== 'object') return value;
|
|
1101
|
+
if (depth >= 6) return clipVerifierString(JSON.stringify(value), 2_000);
|
|
1102
|
+
if (Array.isArray(value)) {
|
|
1103
|
+
if (value.length <= 4) return value.map((item) => compactVerifierValue(item, depth + 1));
|
|
1104
|
+
return [
|
|
1105
|
+
...value.slice(0, 3).map((item) => compactVerifierValue(item, depth + 1)),
|
|
1106
|
+
{ promptCompactedItems: value.length - 4 },
|
|
1107
|
+
compactVerifierValue(value.at(-1), depth + 1),
|
|
1108
|
+
];
|
|
1109
|
+
}
|
|
1110
|
+
return Object.fromEntries(
|
|
1111
|
+
Object.entries(value as Record<string, unknown>).map(([key, item]) => [
|
|
1112
|
+
key,
|
|
1113
|
+
compactVerifierValue(item, depth + 1),
|
|
1114
|
+
]),
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function clipVerifierString(value: string, maxChars: number): string {
|
|
1119
|
+
const redacted = redactFreeformText(value).redacted;
|
|
1120
|
+
if (redacted.length <= maxChars) return redacted;
|
|
1121
|
+
const headChars = Math.ceil(maxChars * 0.6);
|
|
1122
|
+
const tailChars = Math.floor(maxChars * 0.4);
|
|
1123
|
+
return `${redacted.slice(0, headChars)}\n...[prompt preview truncated ${redacted.length - maxChars} chars]...\n${redacted.slice(-tailChars)}`;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
965
1126
|
function creditBackendPreparation(
|
|
966
1127
|
budget: { creditedCount: number; creditMs: number },
|
|
967
1128
|
durationMs: number,
|
package/src/imprint/llm.ts
CHANGED
|
@@ -627,9 +627,9 @@ async function traceAnalyze(
|
|
|
627
627
|
throw err;
|
|
628
628
|
}
|
|
629
629
|
// Providers report `inputTokens` as the *uncached* input only; the cached
|
|
630
|
-
// portion lives in the cache fields.
|
|
631
|
-
//
|
|
632
|
-
//
|
|
630
|
+
// portion lives in the cache fields. Phoenix expects the TOTAL prompt plus
|
|
631
|
+
// the cache breakdown so it can calculate cost server-side, so sum them
|
|
632
|
+
// here. A real total is also large enough to clear the
|
|
633
633
|
// resolveTraceTokenCount sanity check, so cache-hit calls stop falling back
|
|
634
634
|
// to the chars/4 estimate.
|
|
635
635
|
const cacheReadTokens = result.cacheReadInputTokens ?? undefined;
|
|
@@ -1254,6 +1254,10 @@ export function preferredVerificationModel(provider: ProviderName): string {
|
|
|
1254
1254
|
throw new Error(`Provider ${provider} does not support live semantic verification`);
|
|
1255
1255
|
}
|
|
1256
1256
|
|
|
1257
|
+
/** The semantic reviewer intentionally uses a provider independent from the
|
|
1258
|
+
* compiler. Claude/Sonnet compilation therefore still receives a Terra review. */
|
|
1259
|
+
export const DEFAULT_VERIFICATION_PROVIDER: ProviderName = 'codex-cli';
|
|
1260
|
+
|
|
1257
1261
|
interface ModelOption {
|
|
1258
1262
|
model: string;
|
|
1259
1263
|
isDefault: boolean;
|