typeclaw 0.36.8 → 0.37.1
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 +3 -3
- package/package.json +3 -2
- package/src/agent/index.ts +31 -11
- package/src/agent/live-sessions.ts +12 -0
- package/src/agent/model-fallback.ts +17 -15
- package/src/agent/model-overrides.ts +2 -2
- package/src/agent/session-meta.ts +10 -0
- package/src/agent/subagents.ts +30 -3
- package/src/agent/system-prompt.ts +9 -3
- package/src/agent/todo/continuation-policy.ts +6 -3
- package/src/agent/todo/continuation-wiring.ts +4 -2
- package/src/agent/todo/continuation.ts +3 -3
- package/src/agent/tools/todo/index.ts +27 -4
- package/src/bundled-plugins/agent-browser/index.ts +33 -108
- package/src/bundled-plugins/agent-browser/shim.ts +3 -94
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +8 -33
- package/src/bundled-plugins/doc-render/skills/typeclaw-render-pdf/SKILL.md +2 -2
- package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +7 -1
- package/src/bundled-plugins/memory/README.md +80 -23
- package/src/bundled-plugins/memory/append-tool.ts +74 -53
- package/src/bundled-plugins/memory/citation-superset.ts +4 -0
- package/src/bundled-plugins/memory/citations.ts +54 -0
- package/src/bundled-plugins/memory/dreaming-metrics.ts +30 -0
- package/src/bundled-plugins/memory/dreaming.ts +444 -21
- package/src/bundled-plugins/memory/index.ts +544 -400
- package/src/bundled-plugins/memory/load-memory.ts +87 -10
- package/src/bundled-plugins/memory/load-shards.ts +48 -22
- package/src/bundled-plugins/memory/memory-logger.ts +95 -106
- package/src/bundled-plugins/memory/memory-retrieval.ts +3 -3
- package/src/bundled-plugins/memory/parent-link.ts +33 -0
- package/src/bundled-plugins/memory/paths.ts +12 -0
- package/src/bundled-plugins/memory/references/frontmatter.ts +197 -0
- package/src/bundled-plugins/memory/references/load-references.ts +212 -0
- package/src/bundled-plugins/memory/references/store-reference-tool.ts +59 -0
- package/src/bundled-plugins/memory/search-tool.ts +282 -45
- package/src/bundled-plugins/memory/stream-events.ts +1 -0
- package/src/bundled-plugins/memory/stream-io.ts +28 -3
- package/src/bundled-plugins/memory/turn-dedup.ts +40 -0
- package/src/bundled-plugins/memory/vector/cache-write.ts +19 -0
- package/src/bundled-plugins/memory/vector/config.ts +28 -0
- package/src/bundled-plugins/memory/vector/doctor.ts +124 -0
- package/src/bundled-plugins/memory/vector/embedder.ts +246 -0
- package/src/bundled-plugins/memory/vector/hybrid.ts +439 -0
- package/src/bundled-plugins/memory/vector/index-on-write.ts +34 -0
- package/src/bundled-plugins/memory/vector/inspect.ts +111 -0
- package/src/bundled-plugins/memory/vector/passages.ts +125 -0
- package/src/bundled-plugins/memory/vector/reference-index-on-write.ts +50 -0
- package/src/bundled-plugins/memory/vector/relevance-gate.ts +93 -0
- package/src/bundled-plugins/memory/vector/startup.ts +71 -0
- package/src/bundled-plugins/memory/vector/store.ts +203 -0
- package/src/bundled-plugins/memory/vector/truncation.ts +124 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +2 -0
- package/src/channels/router.ts +239 -40
- package/src/cli/incomplete-init.ts +57 -0
- package/src/cli/init.ts +166 -18
- package/src/cli/inspect.ts +11 -5
- package/src/cli/model.ts +115 -36
- package/src/cli/provider.ts +5 -3
- package/src/cli/restart.ts +24 -0
- package/src/cli/start.ts +24 -0
- package/src/cli/tunnel.ts +53 -8
- package/src/config/config.ts +110 -19
- package/src/config/index.ts +5 -1
- package/src/config/models-mutation.ts +29 -11
- package/src/config/providers-mutation.ts +2 -2
- package/src/config/providers.ts +146 -12
- package/src/container/shared.ts +9 -0
- package/src/container/start.ts +87 -4
- package/src/cron/consumer.ts +13 -7
- package/src/hostd/models.ts +64 -0
- package/src/hostd/paths.ts +6 -0
- package/src/hostd/portbroker-manager.ts +2 -2
- package/src/init/checkpoint.ts +201 -0
- package/src/init/dockerfile.ts +121 -34
- package/src/init/gitignore.ts +7 -7
- package/src/init/index.ts +41 -9
- package/src/init/models-dev.ts +96 -21
- package/src/init/oauth-login.ts +3 -3
- package/src/init/progress.ts +29 -0
- package/src/init/validate-api-key.ts +4 -0
- package/src/inspect/index.ts +13 -6
- package/src/inspect/item-list.ts +11 -2
- package/src/inspect/live-list.ts +65 -0
- package/src/inspect/open-item.ts +22 -1
- package/src/inspect/session-list.ts +29 -0
- package/src/models/embedding-model.ts +114 -0
- package/src/models/transformers-version.ts +55 -0
- package/src/plugin/types.ts +3 -0
- package/src/portbroker/container-server.ts +23 -0
- package/src/portbroker/forward-request-bus.ts +35 -0
- package/src/portbroker/forward-result-bus.ts +2 -3
- package/src/portbroker/hostd-client.ts +182 -36
- package/src/portbroker/index.ts +6 -1
- package/src/portbroker/protocol.ts +9 -2
- package/src/run/channel-session-factory.ts +11 -1
- package/src/run/index.ts +65 -8
- package/src/server/command-runner.ts +24 -1
- package/src/server/index.ts +42 -8
- package/src/shared/index.ts +2 -0
- package/src/shared/protocol.ts +31 -0
- package/src/skills/typeclaw-channels/SKILL.md +4 -4
- package/src/skills/typeclaw-config/SKILL.md +2 -2
- package/src/skills/typeclaw-memory/SKILL.md +3 -1
- package/src/skills/typeclaw-permissions/SKILL.md +3 -3
- package/src/skills/typeclaw-skills/SKILL.md +1 -1
- package/src/skills/typeclaw-tunnels/SKILL.md +22 -1
- package/src/tunnels/providers/cloudflare-quick.ts +65 -7
- package/src/tunnels/upstream-probe.ts +25 -0
- package/typeclaw.schema.json +156 -67
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +0 -170
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +0 -421
- package/src/portbroker/bind-with-forward.ts +0 -102
package/src/cli/init.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
autocomplete,
|
|
5
|
+
cancel,
|
|
6
|
+
confirm,
|
|
7
|
+
intro,
|
|
8
|
+
isCancel,
|
|
9
|
+
log,
|
|
10
|
+
note,
|
|
11
|
+
password,
|
|
12
|
+
select,
|
|
13
|
+
spinner,
|
|
14
|
+
text,
|
|
15
|
+
} from '@clack/prompts'
|
|
4
16
|
import { defineCommand } from 'citty'
|
|
5
17
|
|
|
6
18
|
import {
|
|
7
19
|
KNOWN_PROVIDER_VENDORS,
|
|
8
20
|
KNOWN_PROVIDERS,
|
|
21
|
+
isKnownModelRef,
|
|
9
22
|
listKnownProviderVendorIds,
|
|
10
23
|
providerIdsForVendor,
|
|
11
24
|
supportsApiKey as providerSupportsApiKey,
|
|
@@ -35,9 +48,17 @@ import {
|
|
|
35
48
|
type KakaotalkAuthResult,
|
|
36
49
|
type LLMAuth,
|
|
37
50
|
} from '@/init'
|
|
51
|
+
import {
|
|
52
|
+
checkpointFromSelections,
|
|
53
|
+
createLocalWizardCheckpointStore,
|
|
54
|
+
sanitizeCheckpointAgainstCatalog,
|
|
55
|
+
type WizardAnswerCheckpointV1,
|
|
56
|
+
type WizardCheckpointStore,
|
|
57
|
+
} from '@/init/checkpoint'
|
|
38
58
|
import { runKakaotalkBootstrap } from '@/init/kakaotalk-auth'
|
|
39
|
-
import { fetchModelOptions, type ModelOption } from '@/init/models-dev'
|
|
59
|
+
import { customModelMetaFromOption, fetchModelOptions, type ModelOption } from '@/init/models-dev'
|
|
40
60
|
import { makeOAuthLoginRunner, type OAuthLoginResult } from '@/init/oauth-login'
|
|
61
|
+
import { detectInitProgress } from '@/init/progress'
|
|
41
62
|
import {
|
|
42
63
|
API_KEY_DASHBOARD_URL,
|
|
43
64
|
MINIMAX_TOKEN_PLAN_DASHBOARD_URL,
|
|
@@ -159,9 +180,10 @@ export const init = defineCommand({
|
|
|
159
180
|
}
|
|
160
181
|
preflightSpinner.stop('Docker is reachable.')
|
|
161
182
|
|
|
183
|
+
const checkpointStore = createLocalWizardCheckpointStore()
|
|
162
184
|
let collected: CollectedInputs
|
|
163
185
|
try {
|
|
164
|
-
collected = await collectWizardInputs(cwd, defaultWizardPrompts, { reset })
|
|
186
|
+
collected = await collectWizardInputs(cwd, defaultWizardPrompts, { reset, checkpointStore })
|
|
165
187
|
} catch (error) {
|
|
166
188
|
if (error instanceof WizardAbortedError) {
|
|
167
189
|
if (error.oauthCredentialsSaved) {
|
|
@@ -189,6 +211,8 @@ export const init = defineCommand({
|
|
|
189
211
|
kakaotalkPassword,
|
|
190
212
|
github: githubCredentials,
|
|
191
213
|
} = channelSecrets
|
|
214
|
+
const modelMeta = customModelMetaFromOption(model)
|
|
215
|
+
const visionModelMeta = vision !== undefined ? customModelMetaFromOption(vision.model) : undefined
|
|
192
216
|
|
|
193
217
|
// TODO: add remaining wizard steps from TypeClaw.md once their runtime lands:
|
|
194
218
|
// - git backup (url + PAT) — Phase 10
|
|
@@ -213,7 +237,14 @@ export const init = defineCommand({
|
|
|
213
237
|
cwd,
|
|
214
238
|
llmAuth,
|
|
215
239
|
model: model.ref,
|
|
216
|
-
...(
|
|
240
|
+
...(modelMeta !== undefined ? { modelMeta } : {}),
|
|
241
|
+
...(vision !== undefined
|
|
242
|
+
? {
|
|
243
|
+
visionModel: vision.model.ref,
|
|
244
|
+
...(visionModelMeta !== undefined ? { visionModelMeta } : {}),
|
|
245
|
+
visionAuth: vision.llmAuth,
|
|
246
|
+
}
|
|
247
|
+
: {}),
|
|
217
248
|
cliEntry: process.argv[1],
|
|
218
249
|
...(discordBotToken !== undefined ? { discordBotToken } : {}),
|
|
219
250
|
...(slackBotToken !== undefined ? { slackBotToken, slackAppToken } : {}),
|
|
@@ -271,6 +302,10 @@ export const init = defineCommand({
|
|
|
271
302
|
}
|
|
272
303
|
|
|
273
304
|
if (hatchingOk) {
|
|
305
|
+
// Clear the resume checkpoint only on full success (hatching ok). A
|
|
306
|
+
// failed install/dockerfile/git/hatching leaves it in place so the next
|
|
307
|
+
// `typeclaw init` — or `typeclaw start` (PR2) — can resume.
|
|
308
|
+
await checkpointStore.clear(cwd).catch(() => {})
|
|
274
309
|
const claimableChannel =
|
|
275
310
|
channelChoice !== 'none' && channelChoice !== 'github' ? channelDisplayName(channelChoice) : null
|
|
276
311
|
const hints: Array<{ label: string; command: string }> = []
|
|
@@ -380,7 +415,7 @@ export interface WizardPrompts {
|
|
|
380
415
|
pickModel: (
|
|
381
416
|
options: ModelOption[],
|
|
382
417
|
providerId: KnownProviderId,
|
|
383
|
-
initial:
|
|
418
|
+
initial: string | undefined,
|
|
384
419
|
) => Promise<StepResult<ModelOption>>
|
|
385
420
|
pickAuthMethod: (
|
|
386
421
|
provider: (typeof KNOWN_PROVIDERS)[KnownProviderId],
|
|
@@ -400,7 +435,7 @@ export interface WizardPrompts {
|
|
|
400
435
|
pickVisionModel: (
|
|
401
436
|
options: ModelOption[],
|
|
402
437
|
providerId: KnownProviderId,
|
|
403
|
-
initial:
|
|
438
|
+
initial: string | undefined,
|
|
404
439
|
) => Promise<StepResult<ModelOption>>
|
|
405
440
|
pickChannel: (initial: ChannelChoice | undefined) => Promise<StepResult<ChannelChoice>>
|
|
406
441
|
hasExistingChannelSecrets: (cwd: string, channel: Exclude<ChannelChoice, 'none'>) => Promise<boolean>
|
|
@@ -408,17 +443,19 @@ export interface WizardPrompts {
|
|
|
408
443
|
runOAuthLogin: (
|
|
409
444
|
provider: (typeof KNOWN_PROVIDERS)[KnownProviderId],
|
|
410
445
|
cwd: string,
|
|
411
|
-
model:
|
|
446
|
+
model: string,
|
|
412
447
|
) => Promise<OAuthLoginResult>
|
|
413
448
|
askOAuthFailureRecovery: (
|
|
414
449
|
provider: (typeof KNOWN_PROVIDERS)[KnownProviderId],
|
|
415
450
|
reason: string,
|
|
416
451
|
apiKeyAvailable: boolean,
|
|
417
452
|
) => Promise<OAuthFailureRecovery>
|
|
453
|
+
confirmResumeCheckpoint: (checkpoint: WizardAnswerCheckpointV1) => Promise<'resume' | 'start-over'>
|
|
418
454
|
}
|
|
419
455
|
|
|
420
456
|
export type CollectWizardInputsOptions = {
|
|
421
457
|
reset?: boolean
|
|
458
|
+
checkpointStore?: WizardCheckpointStore
|
|
422
459
|
}
|
|
423
460
|
|
|
424
461
|
export type OAuthFailureRecovery = 'retry' | 'api-key' | 'abort'
|
|
@@ -448,6 +485,7 @@ export const defaultWizardPrompts: WizardPrompts = {
|
|
|
448
485
|
}
|
|
449
486
|
},
|
|
450
487
|
askOAuthFailureRecovery,
|
|
488
|
+
confirmResumeCheckpoint,
|
|
451
489
|
}
|
|
452
490
|
|
|
453
491
|
export async function collectWizardInputs(
|
|
@@ -456,12 +494,46 @@ export async function collectWizardInputs(
|
|
|
456
494
|
options: CollectWizardInputsOptions = {},
|
|
457
495
|
): Promise<CollectedInputs> {
|
|
458
496
|
const reset = options.reset === true
|
|
497
|
+
const checkpointStore = options.checkpointStore
|
|
459
498
|
const catalog = await prompts.loadCatalog()
|
|
460
499
|
const state: WizardState = { catalog }
|
|
461
500
|
let step: StepId = 'pick-vendor'
|
|
462
501
|
let pendingBackOrigin: StepId | null = null
|
|
463
502
|
let oauthCredentialsSaved = false
|
|
464
503
|
|
|
504
|
+
// Resume saved wizard answers from a prior unfinished init. `--reset` skips
|
|
505
|
+
// this entirely (the user asked to re-answer everything). Route through the
|
|
506
|
+
// shared detectInitProgress() predicate so init/start/restart classify a
|
|
507
|
+
// checkpoint identically: only an `incomplete` init (checkpoint + not
|
|
508
|
+
// hatched) offers resume; a `complete-stale-checkpoint` (checkpoint that
|
|
509
|
+
// outlived a hatched agent) is cleared and ignored, matching the contract.
|
|
510
|
+
if (!reset && checkpointStore !== undefined) {
|
|
511
|
+
const progress = await detectInitProgress({ cwd, checkpointStore })
|
|
512
|
+
if (progress.kind === 'complete-stale-checkpoint') {
|
|
513
|
+
await checkpointStore.clear(cwd).catch(() => {})
|
|
514
|
+
} else if (progress.kind === 'incomplete') {
|
|
515
|
+
// Sanitize against the freshly-loaded catalog BEFORE showing the resume
|
|
516
|
+
// prompt. `load` only validates version/cwd/updatedAt, so a checkpoint
|
|
517
|
+
// with a since-removed provider/model id reaches here intact; describing
|
|
518
|
+
// or seeding it raw would crash on `KNOWN_PROVIDERS[providerId]`. Pruning
|
|
519
|
+
// first means the prompt, the seed, and any partial answers all see only
|
|
520
|
+
// catalog-valid fields.
|
|
521
|
+
const catalogRefs = new Set(catalog.options.map((option) => option.ref))
|
|
522
|
+
const sanitized = sanitizeCheckpointAgainstCatalog(progress.checkpoint, catalogRefs)
|
|
523
|
+
const decision = await prompts.confirmResumeCheckpoint(sanitized)
|
|
524
|
+
if (decision === 'resume') {
|
|
525
|
+
seedWizardState(state, sanitized)
|
|
526
|
+
} else {
|
|
527
|
+
await checkpointStore.clear(cwd)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const persistCheckpoint = async (): Promise<void> => {
|
|
533
|
+
if (reset || checkpointStore === undefined) return
|
|
534
|
+
await checkpointStore.save(cwd, projectCheckpoint(cwd, state)).catch(() => {})
|
|
535
|
+
}
|
|
536
|
+
|
|
465
537
|
const abort = (): never => {
|
|
466
538
|
throw new WizardAbortedError({ oauthCredentialsSaved })
|
|
467
539
|
}
|
|
@@ -492,6 +564,11 @@ export async function collectWizardInputs(
|
|
|
492
564
|
}
|
|
493
565
|
|
|
494
566
|
while (true) {
|
|
567
|
+
// Persist the cumulative selections at the top of every iteration so a
|
|
568
|
+
// mid-wizard abort keeps everything answered so far. Running it here (not
|
|
569
|
+
// per-case) means back-navigation — which clears downstream fields before
|
|
570
|
+
// looping — overwrites the projection too, so stale answers never linger.
|
|
571
|
+
await persistCheckpoint()
|
|
495
572
|
switch (step) {
|
|
496
573
|
case 'pick-vendor': {
|
|
497
574
|
const result = onResult(step, await prompts.pickVendor(catalog.options, state.vendorId))
|
|
@@ -849,7 +926,7 @@ async function runOAuthLoginSafely(
|
|
|
849
926
|
prompts: WizardPrompts,
|
|
850
927
|
provider: (typeof KNOWN_PROVIDERS)[KnownProviderId],
|
|
851
928
|
cwd: string,
|
|
852
|
-
model:
|
|
929
|
+
model: string,
|
|
853
930
|
): Promise<OAuthLoginResult> {
|
|
854
931
|
try {
|
|
855
932
|
return await prompts.runOAuthLogin(provider, cwd, model)
|
|
@@ -871,6 +948,72 @@ function finalize(state: WizardState, channelSecrets: CollectedInputs['channelSe
|
|
|
871
948
|
}
|
|
872
949
|
}
|
|
873
950
|
|
|
951
|
+
async function confirmResumeCheckpoint(checkpoint: WizardAnswerCheckpointV1): Promise<'resume' | 'start-over'> {
|
|
952
|
+
const summary = describeCheckpoint(checkpoint)
|
|
953
|
+
const choice = await select({
|
|
954
|
+
message: 'Found answers from a previous, unfinished init. Resume from them?',
|
|
955
|
+
options: [
|
|
956
|
+
{ value: 'resume' as const, label: 'Resume', hint: summary },
|
|
957
|
+
{ value: 'start-over' as const, label: 'Start over', hint: 'discard saved answers and pick everything again' },
|
|
958
|
+
],
|
|
959
|
+
initialValue: 'resume' as const,
|
|
960
|
+
})
|
|
961
|
+
if (isCancel(choice)) return 'start-over'
|
|
962
|
+
return choice
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function describeCheckpoint(checkpoint: WizardAnswerCheckpointV1): string {
|
|
966
|
+
const parts: string[] = []
|
|
967
|
+
// Defense-in-depth: callers sanitize before describing, but a raw provider id
|
|
968
|
+
// missing from KNOWN_PROVIDERS must degrade to the id string, never throw.
|
|
969
|
+
if (checkpoint.modelRef !== undefined) parts.push(checkpoint.modelRef)
|
|
970
|
+
else if (checkpoint.providerId !== undefined) {
|
|
971
|
+
parts.push(KNOWN_PROVIDERS[checkpoint.providerId]?.name ?? checkpoint.providerId)
|
|
972
|
+
}
|
|
973
|
+
if (checkpoint.visionModelRef !== undefined) parts.push(`vision: ${checkpoint.visionModelRef}`)
|
|
974
|
+
if (checkpoint.channelChoice !== undefined && checkpoint.channelChoice !== 'none') {
|
|
975
|
+
parts.push(`channel: ${checkpoint.channelChoice}`)
|
|
976
|
+
}
|
|
977
|
+
return parts.length > 0 ? parts.join(', ') : 'partial selections'
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Pre-populate wizard state from a sanitized checkpoint so each prompt's
|
|
981
|
+
// `initial` argument fast-forwards the user to their prior choice. Only seeds
|
|
982
|
+
// fields whose upstream selections are all present and catalog-valid; the
|
|
983
|
+
// sanitize pass already dropped stale refs, so a missing field here just means
|
|
984
|
+
// that step prompts fresh.
|
|
985
|
+
function seedWizardState(state: WizardState, checkpoint: WizardAnswerCheckpointV1): void {
|
|
986
|
+
state.vendorId = checkpoint.vendorId
|
|
987
|
+
state.providerId = checkpoint.providerId
|
|
988
|
+
state.model = resolveModelOption(state.catalog, checkpoint.modelRef)
|
|
989
|
+
state.authMethod = checkpoint.authMethod
|
|
990
|
+
state.visionVendorId = checkpoint.visionVendorId
|
|
991
|
+
state.visionProviderId = checkpoint.visionProviderId
|
|
992
|
+
state.visionModel = resolveModelOption(state.catalog, checkpoint.visionModelRef)
|
|
993
|
+
state.visionAuthMethod = checkpoint.visionAuthMethod
|
|
994
|
+
state.channelChoice = checkpoint.channelChoice
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function resolveModelOption(catalog: WizardState['catalog'], ref: string | undefined): ModelOption | undefined {
|
|
998
|
+
if (catalog === undefined || ref === undefined) return undefined
|
|
999
|
+
return catalog.options.find((option) => option.ref === ref)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function projectCheckpoint(cwd: string, state: WizardState): WizardAnswerCheckpointV1 {
|
|
1003
|
+
return checkpointFromSelections({
|
|
1004
|
+
cwd,
|
|
1005
|
+
...(state.vendorId !== undefined ? { vendorId: state.vendorId } : {}),
|
|
1006
|
+
...(state.providerId !== undefined ? { providerId: state.providerId } : {}),
|
|
1007
|
+
...(state.model?.ref !== undefined ? { modelRef: state.model.ref } : {}),
|
|
1008
|
+
...(state.authMethod !== undefined ? { authMethod: state.authMethod } : {}),
|
|
1009
|
+
...(state.visionVendorId !== undefined ? { visionVendorId: state.visionVendorId } : {}),
|
|
1010
|
+
...(state.visionProviderId !== undefined ? { visionProviderId: state.visionProviderId } : {}),
|
|
1011
|
+
...(state.visionModel?.ref !== undefined ? { visionModelRef: state.visionModel.ref } : {}),
|
|
1012
|
+
...(state.visionAuthMethod !== undefined ? { visionAuthMethod: state.visionAuthMethod } : {}),
|
|
1013
|
+
...(state.channelChoice !== undefined ? { channelChoice: state.channelChoice } : {}),
|
|
1014
|
+
})
|
|
1015
|
+
}
|
|
1016
|
+
|
|
874
1017
|
function channelDisplayName(choice: Exclude<ChannelChoice, 'none'>): string {
|
|
875
1018
|
switch (choice) {
|
|
876
1019
|
case 'slack':
|
|
@@ -952,8 +1095,9 @@ async function pickVendor(
|
|
|
952
1095
|
initial: KnownProviderVendorId | undefined,
|
|
953
1096
|
): Promise<StepResult<KnownProviderVendorId>> {
|
|
954
1097
|
const vendors = uniqueVendors(options)
|
|
955
|
-
const choice = await
|
|
1098
|
+
const choice = await autocomplete({
|
|
956
1099
|
message: 'Pick an LLM provider',
|
|
1100
|
+
placeholder: 'Type to search…',
|
|
957
1101
|
options: vendors.map((id) => ({
|
|
958
1102
|
value: id,
|
|
959
1103
|
label: KNOWN_PROVIDER_VENDORS[id].name,
|
|
@@ -973,8 +1117,9 @@ async function pickProviderVariant(
|
|
|
973
1117
|
const variants = providersForVendorInCatalog(vendorId, options)
|
|
974
1118
|
if (variants.length === 0) throw new Error(`Internal error: vendor ${vendorId} has no providers in the catalog`)
|
|
975
1119
|
if (variants.length === 1) return autoValue(variants[0]!)
|
|
976
|
-
const choice = await
|
|
1120
|
+
const choice = await autocomplete<KnownProviderId>({
|
|
977
1121
|
message: `Pick a ${KNOWN_PROVIDER_VENDORS[vendorId].name} option`,
|
|
1122
|
+
placeholder: 'Type to search…',
|
|
978
1123
|
options: variants.map((id) => {
|
|
979
1124
|
const hint = variantHint(vendorId, id)
|
|
980
1125
|
return hint !== undefined
|
|
@@ -990,15 +1135,16 @@ async function pickProviderVariant(
|
|
|
990
1135
|
async function pickModelForProvider(
|
|
991
1136
|
options: ModelOption[],
|
|
992
1137
|
providerId: KnownProviderId,
|
|
993
|
-
initial:
|
|
1138
|
+
initial: string | undefined,
|
|
994
1139
|
): Promise<StepResult<ModelOption>> {
|
|
995
1140
|
const candidates = sortRecommendedFirst(options.filter((o) => o.providerId === providerId))
|
|
996
1141
|
// select<string>, not select<KnownModelRef>: clack's Option<Value> is a
|
|
997
1142
|
// distributive conditional type, so a large KnownModelRef union explodes into
|
|
998
1143
|
// a per-literal option union that no longer accepts `value: ref`. The runtime
|
|
999
1144
|
// value is the ref string and is re-narrowed via `candidates.find` below.
|
|
1000
|
-
const choice = await
|
|
1145
|
+
const choice = await autocomplete<string>({
|
|
1001
1146
|
message: `Pick a ${KNOWN_PROVIDERS[providerId].name} model`,
|
|
1147
|
+
placeholder: 'Type to search…',
|
|
1002
1148
|
options: candidates.map((o) => ({
|
|
1003
1149
|
value: o.ref,
|
|
1004
1150
|
label: formatModelLabel(o),
|
|
@@ -1042,8 +1188,9 @@ async function pickVisionVendor(
|
|
|
1042
1188
|
log.warn('No vision-capable models available; skipping vision profile.')
|
|
1043
1189
|
return autoValue('skip')
|
|
1044
1190
|
}
|
|
1045
|
-
const choice = await
|
|
1191
|
+
const choice = await autocomplete<KnownProviderVendorId | 'skip'>({
|
|
1046
1192
|
message: 'Your model is text-only. Pick a provider for the `vision` profile (used for image input)',
|
|
1193
|
+
placeholder: 'Type to search…',
|
|
1047
1194
|
options: [
|
|
1048
1195
|
...vendors.map((id) => ({
|
|
1049
1196
|
value: id as KnownProviderVendorId | 'skip',
|
|
@@ -1069,12 +1216,13 @@ async function pickVisionProviderVariant(
|
|
|
1069
1216
|
async function pickVisionModel(
|
|
1070
1217
|
options: ModelOption[],
|
|
1071
1218
|
providerId: KnownProviderId,
|
|
1072
|
-
initial:
|
|
1219
|
+
initial: string | undefined,
|
|
1073
1220
|
): Promise<StepResult<ModelOption>> {
|
|
1074
1221
|
const candidates = sortRecommendedFirst(options.filter((o) => o.providerId === providerId))
|
|
1075
1222
|
// select<string> for the same distributive-Option reason as pickModelForProvider.
|
|
1076
|
-
const choice = await
|
|
1223
|
+
const choice = await autocomplete<string>({
|
|
1077
1224
|
message: `Pick a vision-capable ${KNOWN_PROVIDERS[providerId].name} model`,
|
|
1225
|
+
placeholder: 'Type to search…',
|
|
1078
1226
|
options: candidates.map((o) => ({
|
|
1079
1227
|
value: o.ref,
|
|
1080
1228
|
label: formatModelLabel(o),
|
|
@@ -1697,12 +1845,12 @@ const RECOMMENDED_MODEL_REFS: ReadonlySet<KnownModelRef> = new Set<KnownModelRef
|
|
|
1697
1845
|
])
|
|
1698
1846
|
|
|
1699
1847
|
export function formatModelLabel(o: ModelOption): string {
|
|
1700
|
-
return RECOMMENDED_MODEL_REFS.has(o.ref) ? `${o.modelName} (Recommended)` : o.modelName
|
|
1848
|
+
return isKnownModelRef(o.ref) && RECOMMENDED_MODEL_REFS.has(o.ref) ? `${o.modelName} (Recommended)` : o.modelName
|
|
1701
1849
|
}
|
|
1702
1850
|
|
|
1703
1851
|
export function sortRecommendedFirst(options: ModelOption[]): ModelOption[] {
|
|
1704
|
-
const recommended = options.filter((o) => RECOMMENDED_MODEL_REFS.has(o.ref))
|
|
1705
|
-
const rest = options.filter((o) => !RECOMMENDED_MODEL_REFS.has(o.ref))
|
|
1852
|
+
const recommended = options.filter((o) => isKnownModelRef(o.ref) && RECOMMENDED_MODEL_REFS.has(o.ref))
|
|
1853
|
+
const rest = options.filter((o) => !isKnownModelRef(o.ref) || !RECOMMENDED_MODEL_REFS.has(o.ref))
|
|
1706
1854
|
return [...recommended, ...rest]
|
|
1707
1855
|
}
|
|
1708
1856
|
|
package/src/cli/inspect.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { defineCommand } from 'citty'
|
|
|
3
3
|
import { requireContainerRunning, resolveHostPort, resolveTuiToken } from '@/container'
|
|
4
4
|
import { findAgentDir } from '@/init'
|
|
5
5
|
import {
|
|
6
|
+
fetchLiveSessions,
|
|
6
7
|
listViewerItems,
|
|
7
8
|
openViewerItem,
|
|
8
9
|
parseDuration,
|
|
@@ -150,7 +151,8 @@ export async function runInspectViewer(opts: RunInspectViewerOptions): Promise<n
|
|
|
150
151
|
|
|
151
152
|
const interactive = Boolean(process.stdin.isTTY)
|
|
152
153
|
const liveHint = interactive ? escHintLine(color) : undefined
|
|
153
|
-
const
|
|
154
|
+
const inspectUrl = containerRunning ? await resolveInspectUrl(cwd) : undefined
|
|
155
|
+
const liveSource = inspectUrl !== undefined ? buildLiveSource(inspectUrl) : undefined
|
|
154
156
|
|
|
155
157
|
const stdout = (line: string): void => {
|
|
156
158
|
process.stdout.write(`${line}\n`)
|
|
@@ -186,6 +188,7 @@ export async function runInspectViewer(opts: RunInspectViewerOptions): Promise<n
|
|
|
186
188
|
onWarn: stderr,
|
|
187
189
|
}
|
|
188
190
|
if (sinceMs !== undefined) listOpts.sinceMs = sinceMs
|
|
191
|
+
if (inspectUrl !== undefined) listOpts.liveSessions = await fetchLiveSessions({ url: inspectUrl })
|
|
189
192
|
return (await listViewerItems(listOpts)).items
|
|
190
193
|
},
|
|
191
194
|
keyOf: (item) => (item.kind === 'logs' ? 'logs' : item.summary.sessionId),
|
|
@@ -223,12 +226,15 @@ async function resolveTuiUrl(cwd: string): Promise<string> {
|
|
|
223
226
|
return url.toString()
|
|
224
227
|
}
|
|
225
228
|
|
|
226
|
-
async function
|
|
229
|
+
async function resolveInspectUrl(cwd: string): Promise<string> {
|
|
227
230
|
const port = await resolveHostPort({ cwd })
|
|
228
231
|
const token = await resolveTuiToken({ cwd })
|
|
229
232
|
const baseUrl = new URL(`ws://127.0.0.1:${port}/inspect`)
|
|
230
233
|
if (token !== null) baseUrl.searchParams.set('token', token)
|
|
231
|
-
|
|
234
|
+
return baseUrl.toString()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildLiveSource(url: string): LiveSourceFactory {
|
|
232
238
|
return ({ sessionId, sinceMs, signal, onSubscribed }) =>
|
|
233
239
|
streamLive({
|
|
234
240
|
url,
|
|
@@ -325,8 +331,8 @@ function itemHint(item: ViewerItem): { hint: string } {
|
|
|
325
331
|
function sessionRowLabel(s: SessionSummary): string {
|
|
326
332
|
const id = shortSessionId(s.sessionId)
|
|
327
333
|
const label = s.origin === null ? '(unknown origin)' : originLabel(s.origin)
|
|
328
|
-
const when = formatRelative(s.mtimeMs)
|
|
329
|
-
return `${c.cyan(id)} ${label} ${
|
|
334
|
+
const when = s.live === true ? c.green('live · replying') : c.dim(formatRelative(s.mtimeMs))
|
|
335
|
+
return `${c.cyan(id)} ${label} ${when}`
|
|
330
336
|
}
|
|
331
337
|
|
|
332
338
|
function formatRelative(ms: number): string {
|
package/src/cli/model.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { cancel, intro, isCancel, log, select } from '@clack/prompts'
|
|
1
|
+
import { autocomplete, cancel, intro, isCancel, log, select } from '@clack/prompts'
|
|
2
2
|
import { defineCommand } from 'citty'
|
|
3
3
|
|
|
4
|
+
import type { CustomModelMeta } from '@/config'
|
|
4
5
|
import {
|
|
5
6
|
addProfile,
|
|
6
7
|
listModelProfiles,
|
|
@@ -9,19 +10,25 @@ import {
|
|
|
9
10
|
setProfile,
|
|
10
11
|
} from '@/config/models-mutation'
|
|
11
12
|
import {
|
|
13
|
+
isKnownModelRef,
|
|
12
14
|
KNOWN_PROVIDERS,
|
|
13
|
-
listKnownModelRefs,
|
|
14
15
|
providerForModelRef,
|
|
15
16
|
type KnownModelRef,
|
|
16
17
|
type KnownProviderId,
|
|
17
18
|
} from '@/config/providers'
|
|
18
19
|
import { findAgentDir, isInitialized } from '@/init'
|
|
20
|
+
import { customModelMetaFromOption, fetchModelOptions, type ModelOption } from '@/init/models-dev'
|
|
19
21
|
|
|
20
22
|
import { runProviderAddFlow } from './provider'
|
|
21
23
|
import { c, done, errorLine } from './ui'
|
|
22
24
|
|
|
23
25
|
const ADD_PROVIDER_SENTINEL = '__add-provider__'
|
|
24
26
|
|
|
27
|
+
type PickedModelRef = {
|
|
28
|
+
ref: string
|
|
29
|
+
meta?: CustomModelMeta
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
const setSub = defineCommand({
|
|
26
33
|
meta: {
|
|
27
34
|
name: 'set',
|
|
@@ -47,18 +54,21 @@ const setSub = defineCommand({
|
|
|
47
54
|
async run({ args }) {
|
|
48
55
|
const cwd = ensureAgentDir()
|
|
49
56
|
const profile = args.profile ?? (await pickProfileName())
|
|
50
|
-
const
|
|
57
|
+
const picked = args.ref !== undefined ? await resolveExplicitRef(args.ref) : await pickModelRef(cwd)
|
|
51
58
|
|
|
52
|
-
intro(`Setting model profile: ${profile} → ${ref}`)
|
|
59
|
+
intro(`Setting model profile: ${profile} → ${picked.ref}`)
|
|
53
60
|
|
|
54
|
-
const result = setProfile(cwd, profile, ref, {
|
|
61
|
+
const result = setProfile(cwd, profile, picked.ref, {
|
|
62
|
+
force: args.force === true,
|
|
63
|
+
...(picked.meta !== undefined ? { meta: picked.meta } : {}),
|
|
64
|
+
})
|
|
55
65
|
if (!result.ok) {
|
|
56
66
|
console.error(errorLine(result.reason))
|
|
57
67
|
process.exit(1)
|
|
58
68
|
}
|
|
59
69
|
done({
|
|
60
70
|
title: c.green(`Profile "${profile}" set.`),
|
|
61
|
-
details: `${profile} → ${ref}`,
|
|
71
|
+
details: `${profile} → ${picked.ref}`,
|
|
62
72
|
hints: [{ label: 'If the agent is running:', command: 'typeclaw reload' }],
|
|
63
73
|
})
|
|
64
74
|
},
|
|
@@ -88,18 +98,21 @@ const addSub = defineCommand({
|
|
|
88
98
|
},
|
|
89
99
|
async run({ args }) {
|
|
90
100
|
const cwd = ensureAgentDir()
|
|
91
|
-
const
|
|
101
|
+
const picked = args.ref !== undefined ? await resolveExplicitRef(args.ref) : await pickModelRef(cwd)
|
|
92
102
|
|
|
93
|
-
intro(`Adding model profile: ${args.profile} → ${ref}`)
|
|
103
|
+
intro(`Adding model profile: ${args.profile} → ${picked.ref}`)
|
|
94
104
|
|
|
95
|
-
const result = addProfile(cwd, args.profile, ref, {
|
|
105
|
+
const result = addProfile(cwd, args.profile, picked.ref, {
|
|
106
|
+
force: args.force === true,
|
|
107
|
+
...(picked.meta !== undefined ? { meta: picked.meta } : {}),
|
|
108
|
+
})
|
|
96
109
|
if (!result.ok) {
|
|
97
110
|
console.error(errorLine(result.reason))
|
|
98
111
|
process.exit(1)
|
|
99
112
|
}
|
|
100
113
|
done({
|
|
101
114
|
title: c.green(`Profile "${args.profile}" added.`),
|
|
102
|
-
details: `${args.profile} → ${ref}`,
|
|
115
|
+
details: `${args.profile} → ${picked.ref}`,
|
|
103
116
|
hints: [{ label: 'If the agent is running:', command: 'typeclaw reload' }],
|
|
104
117
|
})
|
|
105
118
|
},
|
|
@@ -146,7 +159,7 @@ const listSub = defineCommand({
|
|
|
146
159
|
},
|
|
147
160
|
async run({ args }) {
|
|
148
161
|
if (args.available === true) {
|
|
149
|
-
printAvailableRefs()
|
|
162
|
+
await printAvailableRefs()
|
|
150
163
|
return
|
|
151
164
|
}
|
|
152
165
|
const cwd = ensureAgentDir()
|
|
@@ -218,7 +231,7 @@ async function pickProfileName(): Promise<string> {
|
|
|
218
231
|
return choice
|
|
219
232
|
}
|
|
220
233
|
|
|
221
|
-
async function pickModelRef(cwd: string): Promise<
|
|
234
|
+
async function pickModelRef(cwd: string): Promise<PickedModelRef> {
|
|
222
235
|
while (true) {
|
|
223
236
|
const refs = listRegisteredModelRefs(cwd)
|
|
224
237
|
if (refs.length === 0) {
|
|
@@ -234,13 +247,15 @@ async function pickModelRef(cwd: string): Promise<string> {
|
|
|
234
247
|
// distributive conditional type and a large ref union breaks `value: ref`
|
|
235
248
|
// assignability. Values are ref strings (+ the sentinel) and stay correct
|
|
236
249
|
// at runtime — the sentinel check and `return choice` below are unaffected.
|
|
237
|
-
const
|
|
250
|
+
const modelOptions = await listCredentialedModelOptions(refs)
|
|
251
|
+
const choice = await autocomplete<string>({
|
|
238
252
|
message: 'Pick a model',
|
|
253
|
+
placeholder: 'Type to search…',
|
|
239
254
|
options: [
|
|
240
|
-
...
|
|
241
|
-
value: ref,
|
|
242
|
-
label: describeRef(ref),
|
|
243
|
-
hint: ref,
|
|
255
|
+
...modelOptions.map((option) => ({
|
|
256
|
+
value: option.ref,
|
|
257
|
+
label: describeRef(option.ref),
|
|
258
|
+
hint: option.ref,
|
|
244
259
|
})),
|
|
245
260
|
{
|
|
246
261
|
value: ADD_PROVIDER_SENTINEL,
|
|
@@ -248,13 +263,18 @@ async function pickModelRef(cwd: string): Promise<string> {
|
|
|
248
263
|
hint: 'configure a new provider',
|
|
249
264
|
},
|
|
250
265
|
],
|
|
251
|
-
initialValue: refs[0],
|
|
266
|
+
initialValue: modelOptions[0]?.ref ?? refs[0],
|
|
252
267
|
})
|
|
253
268
|
if (isCancel(choice)) {
|
|
254
269
|
cancel('Aborted.')
|
|
255
270
|
process.exit(0)
|
|
256
271
|
}
|
|
257
|
-
if (choice !== ADD_PROVIDER_SENTINEL)
|
|
272
|
+
if (choice !== ADD_PROVIDER_SENTINEL) {
|
|
273
|
+
const option = modelOptions.find((candidate) => candidate.ref === choice)
|
|
274
|
+
if (option === undefined) return { ref: choice }
|
|
275
|
+
const meta = customModelMetaFromOption(option)
|
|
276
|
+
return { ref: option.ref, ...(meta !== undefined ? { meta } : {}) }
|
|
277
|
+
}
|
|
258
278
|
const added = await runProviderAddFlow(cwd, {})
|
|
259
279
|
if (!added.ok) {
|
|
260
280
|
console.error(errorLine(added.reason))
|
|
@@ -263,29 +283,88 @@ async function pickModelRef(cwd: string): Promise<string> {
|
|
|
263
283
|
}
|
|
264
284
|
}
|
|
265
285
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
286
|
+
// Non-interactive `<ref>` path. Curated refs resolve from KNOWN_PROVIDERS, so
|
|
287
|
+
// they need no metadata. Non-curated refs are looked up in the live catalog so
|
|
288
|
+
// `customModels[ref]` carries the same metadata the interactive picker would
|
|
289
|
+
// persist; without it `resolveModel` silently falls back to defaults. A
|
|
290
|
+
// catalog miss (offline / unknown id) still writes the ref, but warns first.
|
|
291
|
+
export async function resolveExplicitRef(
|
|
292
|
+
ref: string,
|
|
293
|
+
loadCatalog: () => Promise<{ options: ModelOption[] }> = fetchModelOptions,
|
|
294
|
+
): Promise<PickedModelRef> {
|
|
295
|
+
if (isKnownModelRef(ref)) return { ref }
|
|
296
|
+
const { options } = await loadCatalog()
|
|
297
|
+
const option = options.find((candidate) => candidate.ref === ref)
|
|
298
|
+
if (option === undefined) {
|
|
299
|
+
log.warn(
|
|
300
|
+
`"${ref}" isn't in the live catalog; saving the ref without metadata. ` +
|
|
301
|
+
`The agent will use fallback defaults (reasoning off, text-only input, zero cost, provider-default context).`,
|
|
302
|
+
)
|
|
303
|
+
return { ref }
|
|
304
|
+
}
|
|
305
|
+
const meta = customModelMetaFromOption(option)
|
|
306
|
+
return { ref, ...(meta !== undefined ? { meta } : {}) }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export type { PickedModelRef }
|
|
310
|
+
|
|
311
|
+
async function listCredentialedModelOptions(refs: KnownModelRef[]): Promise<ModelOption[]> {
|
|
312
|
+
const credentialedProviders = new Set<KnownProviderId>(refs.map((ref) => providerForModelRef(ref)))
|
|
313
|
+
const catalog = await fetchModelOptions()
|
|
314
|
+
const options = catalog.options.filter((option) => credentialedProviders.has(option.providerId))
|
|
315
|
+
if (options.length > 0) return options
|
|
316
|
+
return refs.map((ref) => {
|
|
317
|
+
const providerId = providerForModelRef(ref)
|
|
318
|
+
const modelId = ref.slice(providerId.length + 1)
|
|
319
|
+
const model = (
|
|
320
|
+
KNOWN_PROVIDERS[providerId].models as Record<
|
|
321
|
+
string,
|
|
322
|
+
{ name: string; reasoning?: boolean; contextWindow?: number; input?: ReadonlyArray<string> }
|
|
323
|
+
>
|
|
324
|
+
)[modelId]
|
|
325
|
+
return {
|
|
326
|
+
ref,
|
|
327
|
+
providerId,
|
|
328
|
+
providerName: KNOWN_PROVIDERS[providerId].name,
|
|
329
|
+
modelId,
|
|
330
|
+
modelName: model?.name ?? modelId,
|
|
331
|
+
reasoning: model?.reasoning ?? false,
|
|
332
|
+
contextWindow: model?.contextWindow ?? null,
|
|
333
|
+
curated: true,
|
|
334
|
+
supportsVision: model?.input?.includes('image') ?? false,
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function describeRef(ref: string): string {
|
|
340
|
+
try {
|
|
341
|
+
const providerId = providerForModelRef(ref)
|
|
342
|
+
const modelId = ref.slice(providerId.length + 1)
|
|
343
|
+
const provider = KNOWN_PROVIDERS[providerId]
|
|
344
|
+
const model = (provider.models as Record<string, { name: string }>)[modelId]
|
|
345
|
+
return `${provider.name} · ${model?.name ?? modelId}`
|
|
346
|
+
} catch {
|
|
347
|
+
return ref
|
|
348
|
+
}
|
|
272
349
|
}
|
|
273
350
|
|
|
274
|
-
function printAvailableRefs(): void {
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
351
|
+
async function printAvailableRefs(): Promise<void> {
|
|
352
|
+
const { options, source, warning } = await fetchModelOptions()
|
|
353
|
+
if (options.length === 0) {
|
|
277
354
|
console.log(c.dim('No models registered.'))
|
|
278
355
|
return
|
|
279
356
|
}
|
|
280
357
|
console.log(c.dim('Use `typeclaw model set <profile> <ref>` to apply.'))
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
358
|
+
if (source === 'curated' && warning !== undefined) {
|
|
359
|
+
console.log(c.dim(`Using built-in catalog (models.dev unavailable: ${warning}).`))
|
|
360
|
+
}
|
|
361
|
+
for (const providerId of Object.keys(KNOWN_PROVIDERS) as KnownProviderId[]) {
|
|
362
|
+
const providerOptions = options.filter((option) => option.providerId === providerId)
|
|
363
|
+
if (providerOptions.length === 0) continue
|
|
364
|
+
console.log('')
|
|
365
|
+
console.log(c.cyan(KNOWN_PROVIDERS[providerId].name))
|
|
366
|
+
for (const option of providerOptions) {
|
|
367
|
+
console.log(` ${option.ref}`)
|
|
288
368
|
}
|
|
289
|
-
console.log(` ${ref}`)
|
|
290
369
|
}
|
|
291
370
|
}
|