typeclaw 0.36.7 → 0.37.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/README.md +2 -2
- 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 +11 -2
- 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 +143 -12
- package/src/cli/inspect.ts +11 -5
- package/src/cli/model.ts +112 -34
- 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 +164 -51
- package/src/init/gitignore.ts +7 -7
- package/src/init/index.ts +41 -9
- package/src/init/line-auth.ts +50 -21
- 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 +41 -7
- 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/init/index.ts
CHANGED
|
@@ -10,13 +10,15 @@ import {
|
|
|
10
10
|
GWS_MULTI_ACCOUNT_PLUGIN_VERSION,
|
|
11
11
|
migrateLegacyConfigShape,
|
|
12
12
|
type Config,
|
|
13
|
+
type CustomModelMeta,
|
|
13
14
|
} from '@/config'
|
|
14
15
|
import {
|
|
15
16
|
DEFAULT_MODEL_REF,
|
|
16
17
|
KNOWN_PROVIDERS,
|
|
18
|
+
isKnownModelRef,
|
|
17
19
|
providerForModelRef,
|
|
18
|
-
type KnownModelRef,
|
|
19
20
|
type KnownProviderId,
|
|
21
|
+
type ModelRef,
|
|
20
22
|
} from '@/config/providers'
|
|
21
23
|
import { checkDockerAvailable, type DockerAvailability, type DockerExec, start } from '@/container'
|
|
22
24
|
import { commitSystemFile } from '@/git/system-commit'
|
|
@@ -171,7 +173,8 @@ export type InitOptions = {
|
|
|
171
173
|
cwd: string
|
|
172
174
|
// Selected `provider/model` ref written into typeclaw.json. Defaults to
|
|
173
175
|
// DEFAULT_MODEL_REF when callers (or older test fixtures) omit it.
|
|
174
|
-
model?:
|
|
176
|
+
model?: ModelRef | string
|
|
177
|
+
modelMeta?: CustomModelMeta
|
|
175
178
|
// How the agent will authenticate to the LLM provider. When omitted,
|
|
176
179
|
// defaults to the api-key path with `apiKey` (legacy field, still
|
|
177
180
|
// supported for backwards compat with the old `runInit` signature).
|
|
@@ -181,7 +184,8 @@ export type InitOptions = {
|
|
|
181
184
|
// when both refer to the same provider; the wizard enforces this
|
|
182
185
|
// pairing rule, so by the time we get here `visionAuth` is either
|
|
183
186
|
// (a) absent, or (b) the right auth for `visionModel`'s provider.
|
|
184
|
-
visionModel?:
|
|
187
|
+
visionModel?: ModelRef | string
|
|
188
|
+
visionModelMeta?: CustomModelMeta
|
|
185
189
|
visionAuth?: LLMAuth
|
|
186
190
|
apiKey?: string
|
|
187
191
|
discordBotToken?: string
|
|
@@ -224,7 +228,9 @@ export async function runInit({
|
|
|
224
228
|
apiKey,
|
|
225
229
|
llmAuth,
|
|
226
230
|
model = DEFAULT_MODEL_REF,
|
|
231
|
+
modelMeta,
|
|
227
232
|
visionModel,
|
|
233
|
+
visionModelMeta,
|
|
228
234
|
visionAuth,
|
|
229
235
|
discordBotToken,
|
|
230
236
|
slackBotToken,
|
|
@@ -304,7 +310,9 @@ export async function runInit({
|
|
|
304
310
|
emit({ step: 'scaffold', phase: 'start' })
|
|
305
311
|
await scaffold(cwd, {
|
|
306
312
|
model,
|
|
313
|
+
...(modelMeta !== undefined ? { modelMeta } : {}),
|
|
307
314
|
...(visionModel !== undefined ? { visionModel } : {}),
|
|
315
|
+
...(visionModelMeta !== undefined ? { visionModelMeta } : {}),
|
|
308
316
|
withDiscord: wantsDiscord,
|
|
309
317
|
withSlack: wantsSlack,
|
|
310
318
|
withTelegram: wantsTelegram,
|
|
@@ -520,8 +528,10 @@ export async function isHatched(dir: string): Promise<boolean> {
|
|
|
520
528
|
}
|
|
521
529
|
|
|
522
530
|
export type ScaffoldOptions = {
|
|
523
|
-
model?:
|
|
524
|
-
|
|
531
|
+
model?: ModelRef | string
|
|
532
|
+
modelMeta?: CustomModelMeta
|
|
533
|
+
visionModel?: ModelRef | string
|
|
534
|
+
visionModelMeta?: CustomModelMeta
|
|
525
535
|
withDiscord?: boolean
|
|
526
536
|
withSlack?: boolean
|
|
527
537
|
withTelegram?: boolean
|
|
@@ -545,12 +555,14 @@ export async function scaffold(root: string, options: ScaffoldOptions = {}): Pro
|
|
|
545
555
|
// `memory.*`) is omitted to keep the scaffold minimal — duplicating defaults
|
|
546
556
|
// here would mean every schema change has to be mirrored in two places, and
|
|
547
557
|
// users would feel obligated to maintain values they never set.
|
|
548
|
-
const models: Record<string,
|
|
558
|
+
const models: Record<string, string> = { default: options.model ?? DEFAULT_MODEL_REF }
|
|
549
559
|
if (options.visionModel !== undefined) models.vision = options.visionModel
|
|
550
560
|
const config: Record<string, unknown> = {
|
|
551
561
|
$schema: './node_modules/typeclaw/typeclaw.schema.json',
|
|
552
562
|
models,
|
|
553
563
|
}
|
|
564
|
+
const customModels = collectCustomModels(options)
|
|
565
|
+
if (Object.keys(customModels).length > 0) config.customModels = customModels
|
|
554
566
|
const channels: Record<string, Record<string, never>> = {}
|
|
555
567
|
if (options.withDiscord) channels['discord-bot'] = {}
|
|
556
568
|
if (options.withSlack) channels['slack-bot'] = {}
|
|
@@ -578,12 +590,32 @@ export async function scaffold(root: string, options: ScaffoldOptions = {}): Pro
|
|
|
578
590
|
await writeFile(join(root, GITIGNORE_FILE), buildGitignore(), { flag: 'wx' }).catch(ignoreExists)
|
|
579
591
|
}
|
|
580
592
|
|
|
593
|
+
function collectCustomModels(options: ScaffoldOptions): Record<string, CustomModelMeta> {
|
|
594
|
+
const customModels: Record<string, CustomModelMeta> = {}
|
|
595
|
+
addCustomModel(customModels, options.model ?? DEFAULT_MODEL_REF, options.modelMeta)
|
|
596
|
+
if (options.visionModel !== undefined) addCustomModel(customModels, options.visionModel, options.visionModelMeta)
|
|
597
|
+
return customModels
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function addCustomModel(
|
|
601
|
+
customModels: Record<string, CustomModelMeta>,
|
|
602
|
+
ref: string,
|
|
603
|
+
meta: CustomModelMeta | undefined,
|
|
604
|
+
): void {
|
|
605
|
+
if (isKnownModelRef(ref)) return
|
|
606
|
+
customModels[ref] = meta ?? {}
|
|
607
|
+
}
|
|
608
|
+
|
|
581
609
|
// agent-browser ships in every agent: the bundled SKILL.md (src/skills/
|
|
582
610
|
// agent-browser/SKILL.md) is a discovery stub that calls `agent-browser
|
|
583
611
|
// skills get core` at runtime, so the CLI must be installed for the skill
|
|
584
612
|
// to function. The Dockerfile pre-downloads Chromium too, so the agent
|
|
585
613
|
// can drive a browser without any first-run setup.
|
|
586
|
-
|
|
614
|
+
//
|
|
615
|
+
// Must match the Dockerfile Layer 4 global install (dockerfile.ts); they are
|
|
616
|
+
// two installs of the same CLI and a skew is silent. Enforced by a guard test
|
|
617
|
+
// in packagejson.test.ts.
|
|
618
|
+
export const AGENT_BROWSER_VERSION = '^0.27.0'
|
|
587
619
|
function buildPackageJson(root: string, name: string): Record<string, unknown> {
|
|
588
620
|
return {
|
|
589
621
|
name,
|
|
@@ -738,10 +770,10 @@ export async function writeSecrets(
|
|
|
738
770
|
slackAppToken,
|
|
739
771
|
telegramBotToken,
|
|
740
772
|
}: {
|
|
741
|
-
model?:
|
|
773
|
+
model?: ModelRef | string
|
|
742
774
|
// Omitted on the OAuth path — credentials live in secrets.json via the OAuth runner.
|
|
743
775
|
apiKey?: string
|
|
744
|
-
visionModel?:
|
|
776
|
+
visionModel?: ModelRef | string
|
|
745
777
|
visionApiKey?: string
|
|
746
778
|
discordBotToken?: string
|
|
747
779
|
slackBotToken?: string
|
package/src/init/line-auth.ts
CHANGED
|
@@ -50,30 +50,49 @@ export function lineSecretsPath(agentDir: string): string {
|
|
|
50
50
|
return join(agentDir, 'secrets.json')
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// The SDK persists E2EE (Letter-Sealing) key material under
|
|
54
|
+
// `<AGENT_MESSENGER_CONFIG_DIR>/line-storage/`. The container sets that env to
|
|
55
|
+
// the agent workspace (src/init/dockerfile.ts), but a host-stage login (init /
|
|
56
|
+
// `channel reauth line`) would otherwise fall back to `~/.config/agent-messenger`
|
|
57
|
+
// — so the E2EE key gets written somewhere the container never reads, and inbound
|
|
58
|
+
// Letter-Sealing messages stay undecryptable. Point the host login at the same
|
|
59
|
+
// per-agent dir the container uses so the key lands where the runtime reads it.
|
|
60
|
+
export function lineConfigDir(agentDir: string): string {
|
|
61
|
+
return join(agentDir, 'workspace', '.agent-messenger')
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
export async function runLineBootstrap(input: LineLoginInput): Promise<LineBootstrapStatus> {
|
|
54
65
|
try {
|
|
55
66
|
const store = new SecretsLineCredentialStore({ mode: 'host', secretsPath: lineSecretsPath(input.agentDir) })
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
|
|
68
|
+
// The env is set only for the duration of client construction + login (when
|
|
69
|
+
// the SDK reads it to locate line-storage) and restored after, so a second
|
|
70
|
+
// bootstrap for a different agent in the same process can't inherit the
|
|
71
|
+
// first agent's path. An already-set value (the container's Dockerfile env)
|
|
72
|
+
// is left untouched.
|
|
73
|
+
const result = await withLineConfigDir(lineConfigDir(input.agentDir), () => {
|
|
74
|
+
// The LINE SDK persists the minted auth_token + certificate by calling
|
|
75
|
+
// setAccount() on whatever credential manager the client was built with.
|
|
76
|
+
// Wiring our secrets.json-backed store in here means a successful login
|
|
77
|
+
// writes straight to secrets.json#channels.line — no second copy in
|
|
78
|
+
// ~/.config/agent-messenger to keep in sync.
|
|
79
|
+
const client = input.client ?? buildLineClient(store)
|
|
80
|
+
|
|
81
|
+
return suppressLineTokenInfoDump(() =>
|
|
82
|
+
input.method === 'qr'
|
|
83
|
+
? client.loginWithQR({
|
|
84
|
+
onQRUrl: async (url) => {
|
|
85
|
+
await input.callbacks.onQRUrl?.(url)
|
|
86
|
+
},
|
|
87
|
+
onPincode: input.callbacks.onPincode,
|
|
88
|
+
})
|
|
89
|
+
: client.loginWithEmail({
|
|
90
|
+
email: input.email,
|
|
91
|
+
password: input.password,
|
|
92
|
+
onPincode: input.callbacks.onPincode,
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
})
|
|
77
96
|
|
|
78
97
|
if (!result.authenticated || result.account_id === undefined) {
|
|
79
98
|
const reason = result.message ?? result.error ?? 'LINE login did not authenticate'
|
|
@@ -105,6 +124,16 @@ function buildLineClient(store: SecretsLineCredentialStore): LineLoginClient {
|
|
|
105
124
|
return new RealLineClient(credManager) as unknown as LineLoginClient
|
|
106
125
|
}
|
|
107
126
|
|
|
127
|
+
async function withLineConfigDir<T>(dir: string, fn: () => Promise<T>): Promise<T> {
|
|
128
|
+
const previous = process.env.AGENT_MESSENGER_CONFIG_DIR
|
|
129
|
+
if (previous === undefined) process.env.AGENT_MESSENGER_CONFIG_DIR = dir
|
|
130
|
+
try {
|
|
131
|
+
return await fn()
|
|
132
|
+
} finally {
|
|
133
|
+
if (previous === undefined) delete process.env.AGENT_MESSENGER_CONFIG_DIR
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
108
137
|
async function suppressLineTokenInfoDump<T>(fn: () => Promise<T>): Promise<T> {
|
|
109
138
|
const previous = lineTokenInfoSuppressionQueue
|
|
110
139
|
let release: () => void = () => {}
|
package/src/init/models-dev.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { CustomModelMeta } from '@/config'
|
|
2
|
+
import {
|
|
3
|
+
KNOWN_PROVIDERS,
|
|
4
|
+
isKnownModelRef,
|
|
5
|
+
isModelRef,
|
|
6
|
+
listKnownModelRefs,
|
|
7
|
+
providerForModelRef,
|
|
8
|
+
type KnownProviderId,
|
|
9
|
+
type ModelRef,
|
|
10
|
+
} from '@/config/providers'
|
|
2
11
|
|
|
3
12
|
const MODELS_DEV_URL = 'https://models.dev/api.json'
|
|
4
13
|
const REQUEST_TIMEOUT_MS = 10_000
|
|
@@ -23,16 +32,24 @@ const PROVIDER_TO_MODELS_DEV: Record<KnownProviderId, string> = {
|
|
|
23
32
|
xai: 'xai',
|
|
24
33
|
minimax: 'minimax',
|
|
25
34
|
deepseek: 'deepseek',
|
|
35
|
+
moonshot: 'moonshot',
|
|
36
|
+
// moonshot-coding (Kimi Code subscription) is a billing surface, not a
|
|
37
|
+
// separate model catalog. models.dev tracks the underlying Kimi model
|
|
38
|
+
// metadata under `moonshot`, so we route lookups there; the curated
|
|
39
|
+
// `kimi-for-coding` alias is surfaced regardless of upstream membership.
|
|
40
|
+
'moonshot-coding': 'moonshot',
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
export type ModelOption = {
|
|
29
|
-
ref:
|
|
44
|
+
ref: ModelRef | string
|
|
30
45
|
providerId: KnownProviderId
|
|
31
46
|
providerName: string
|
|
32
47
|
modelId: string
|
|
33
48
|
modelName: string
|
|
34
49
|
reasoning: boolean
|
|
35
50
|
contextWindow: number | null
|
|
51
|
+
maxTokens?: number | null
|
|
52
|
+
cost?: ModelOptionCost | null
|
|
36
53
|
curated: boolean
|
|
37
54
|
// True iff the model accepts image input. Sourced from the curated
|
|
38
55
|
// `Model.input` array (which is the source of truth — pi-ai consumes it
|
|
@@ -43,6 +60,13 @@ export type ModelOption = {
|
|
|
43
60
|
supportsVision: boolean
|
|
44
61
|
}
|
|
45
62
|
|
|
63
|
+
export type ModelOptionCost = {
|
|
64
|
+
input: number
|
|
65
|
+
output: number
|
|
66
|
+
cacheRead: number
|
|
67
|
+
cacheWrite: number
|
|
68
|
+
}
|
|
69
|
+
|
|
46
70
|
type ModelsDevModel = {
|
|
47
71
|
id?: string
|
|
48
72
|
name?: string
|
|
@@ -51,7 +75,15 @@ type ModelsDevModel = {
|
|
|
51
75
|
status?: string
|
|
52
76
|
release_date?: string
|
|
53
77
|
modalities?: { input?: string[]; output?: string[] }
|
|
54
|
-
limit?: { context?: number }
|
|
78
|
+
limit?: { context?: number; output?: number }
|
|
79
|
+
cost?: {
|
|
80
|
+
input?: number
|
|
81
|
+
output?: number
|
|
82
|
+
cacheRead?: number
|
|
83
|
+
cacheWrite?: number
|
|
84
|
+
cache_read?: number
|
|
85
|
+
cache_write?: number
|
|
86
|
+
}
|
|
55
87
|
}
|
|
56
88
|
|
|
57
89
|
type ModelsDevProvider = {
|
|
@@ -99,22 +131,47 @@ export function curatedOptions(): ModelOption[] {
|
|
|
99
131
|
return refs.map((ref) => buildOption(ref, { curated: true }))
|
|
100
132
|
}
|
|
101
133
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
export function customModelMetaFromOption(option: ModelOption): CustomModelMeta | undefined {
|
|
135
|
+
if (isKnownModelRef(option.ref)) return undefined
|
|
136
|
+
if (!isModelRef(option.ref)) return undefined
|
|
137
|
+
return {
|
|
138
|
+
name: option.modelName,
|
|
139
|
+
reasoning: option.reasoning,
|
|
140
|
+
input: option.supportsVision ? ['text', 'image'] : ['text'],
|
|
141
|
+
...(option.contextWindow !== null ? { contextWindow: option.contextWindow } : {}),
|
|
142
|
+
...(option.maxTokens !== undefined && option.maxTokens !== null ? { maxTokens: option.maxTokens } : {}),
|
|
143
|
+
...(option.cost !== undefined && option.cost !== null ? { cost: option.cost } : {}),
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// `data` is the parsed models.dev JSON. We keep every curated entry first
|
|
148
|
+
// (including provider-specific aliases models.dev does not list), then append
|
|
149
|
+
// live upstream models whose refs validate against a known TypeClaw provider.
|
|
108
150
|
function mergeWithCurated(data: Record<string, ModelsDevProvider>): ModelOption[] {
|
|
109
151
|
const out: ModelOption[] = []
|
|
152
|
+
const seen = new Set<string>()
|
|
110
153
|
for (const providerId of Object.keys(KNOWN_PROVIDERS) as KnownProviderId[]) {
|
|
111
154
|
const known = KNOWN_PROVIDERS[providerId]
|
|
112
155
|
const upstream = data[PROVIDER_TO_MODELS_DEV[providerId]]
|
|
113
156
|
const upstreamModels = upstream?.models ?? {}
|
|
114
157
|
for (const modelId of Object.keys(known.models)) {
|
|
115
158
|
const upstreamModel = upstreamModels[modelId]
|
|
116
|
-
const ref = `${providerId}/${modelId}`
|
|
159
|
+
const ref = `${providerId}/${modelId}`
|
|
117
160
|
out.push(buildOption(ref, { curated: true, upstream: upstreamModel }))
|
|
161
|
+
seen.add(ref)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const providerId of Object.keys(KNOWN_PROVIDERS) as KnownProviderId[]) {
|
|
166
|
+
const upstream = data[PROVIDER_TO_MODELS_DEV[providerId]]
|
|
167
|
+
const upstreamModels = upstream?.models ?? {}
|
|
168
|
+
for (const [fallbackModelId, upstreamModel] of Object.entries(upstreamModels)) {
|
|
169
|
+
const modelId = upstreamModel.id ?? fallbackModelId
|
|
170
|
+
if (modelId.trim().length === 0) continue
|
|
171
|
+
const ref = `${providerId}/${modelId}`
|
|
172
|
+
if (seen.has(ref) || !isModelRef(ref)) continue
|
|
173
|
+
out.push(buildOption(ref, { curated: isKnownModelRef(ref), upstream: upstreamModel }))
|
|
174
|
+
seen.add(ref)
|
|
118
175
|
}
|
|
119
176
|
}
|
|
120
177
|
return out
|
|
@@ -125,17 +182,23 @@ type BuildOptionOpts = {
|
|
|
125
182
|
upstream?: ModelsDevModel
|
|
126
183
|
}
|
|
127
184
|
|
|
128
|
-
function buildOption(ref:
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
const modelId = ref.slice(slash + 1)
|
|
185
|
+
function buildOption(ref: ModelRef | string, opts: BuildOptionOpts): ModelOption {
|
|
186
|
+
const providerId = providerForModelRef(ref)
|
|
187
|
+
const modelId = ref.slice(providerId.length + 1)
|
|
132
188
|
const provider = KNOWN_PROVIDERS[providerId]
|
|
133
189
|
const curatedModel = (
|
|
134
190
|
provider.models as Record<
|
|
135
191
|
string,
|
|
136
|
-
{
|
|
192
|
+
{
|
|
193
|
+
name: string
|
|
194
|
+
contextWindow?: number
|
|
195
|
+
maxTokens?: number
|
|
196
|
+
reasoning?: boolean
|
|
197
|
+
input?: ReadonlyArray<string>
|
|
198
|
+
}
|
|
137
199
|
>
|
|
138
200
|
)[modelId]
|
|
201
|
+
const input = resolveInput(curatedModel?.input, opts.upstream?.modalities?.input)
|
|
139
202
|
return {
|
|
140
203
|
ref,
|
|
141
204
|
providerId,
|
|
@@ -144,16 +207,28 @@ function buildOption(ref: KnownModelRef, opts: BuildOptionOpts): ModelOption {
|
|
|
144
207
|
modelName: opts.upstream?.name ?? curatedModel?.name ?? modelId,
|
|
145
208
|
reasoning: opts.upstream?.reasoning ?? curatedModel?.reasoning ?? false,
|
|
146
209
|
contextWindow: opts.upstream?.limit?.context ?? curatedModel?.contextWindow ?? null,
|
|
210
|
+
maxTokens: opts.upstream?.limit?.output ?? curatedModel?.maxTokens ?? null,
|
|
211
|
+
cost: resolveCost(opts.upstream?.cost),
|
|
147
212
|
curated: opts.curated,
|
|
148
|
-
supportsVision:
|
|
213
|
+
supportsVision: input.includes('image'),
|
|
149
214
|
}
|
|
150
215
|
}
|
|
151
216
|
|
|
152
|
-
function
|
|
217
|
+
function resolveInput(
|
|
153
218
|
curatedInput: ReadonlyArray<string> | undefined,
|
|
154
219
|
upstreamInput: ReadonlyArray<string> | undefined,
|
|
155
|
-
):
|
|
156
|
-
if (curatedInput !== undefined) return curatedInput
|
|
157
|
-
if (upstreamInput !== undefined) return upstreamInput
|
|
158
|
-
return
|
|
220
|
+
): string[] {
|
|
221
|
+
if (curatedInput !== undefined) return [...curatedInput]
|
|
222
|
+
if (upstreamInput !== undefined && upstreamInput.length > 0) return [...upstreamInput]
|
|
223
|
+
return ['text']
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function resolveCost(cost: ModelsDevModel['cost']): ModelOptionCost | null {
|
|
227
|
+
if (cost === undefined) return null
|
|
228
|
+
return {
|
|
229
|
+
input: cost.input ?? 0,
|
|
230
|
+
output: cost.output ?? 0,
|
|
231
|
+
cacheRead: cost.cacheRead ?? cost.cache_read ?? 0,
|
|
232
|
+
cacheWrite: cost.cacheWrite ?? cost.cache_write ?? 0,
|
|
233
|
+
}
|
|
159
234
|
}
|
package/src/init/oauth-login.ts
CHANGED
|
@@ -4,14 +4,14 @@ import {
|
|
|
4
4
|
KNOWN_PROVIDERS,
|
|
5
5
|
providerForModelRef,
|
|
6
6
|
supportsOAuth,
|
|
7
|
-
type KnownModelRef,
|
|
8
7
|
type KnownProviderId,
|
|
8
|
+
type ModelRef,
|
|
9
9
|
} from '@/config/providers'
|
|
10
10
|
import { createSecretsStoreForAgent } from '@/secrets'
|
|
11
11
|
|
|
12
12
|
export type OAuthLoginResult = { ok: true } | { ok: false; reason: string }
|
|
13
13
|
|
|
14
|
-
export type OAuthLoginRunner = (options: { cwd: string; model:
|
|
14
|
+
export type OAuthLoginRunner = (options: { cwd: string; model: ModelRef | string }) => Promise<OAuthLoginResult>
|
|
15
15
|
|
|
16
16
|
// Wrap pi-ai's OAuth callbacks so the CLI doesn't have to know about the
|
|
17
17
|
// upstream callback shape. The CLI sees four lifecycle events:
|
|
@@ -76,7 +76,7 @@ export function makeOAuthLoginRunner(callbacks: OAuthCallbacks): OAuthLoginRunne
|
|
|
76
76
|
// params" without spinning up a real secrets store / browser callback server.
|
|
77
77
|
export type FakeOAuthLoginRunnerOptions = {
|
|
78
78
|
result?: OAuthLoginResult
|
|
79
|
-
onCalled?: (options: { cwd: string; model:
|
|
79
|
+
onCalled?: (options: { cwd: string; model: ModelRef | string; providerId: KnownProviderId }) => void
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export function makeFakeOAuthLoginRunner(options: FakeOAuthLoginRunnerOptions = {}): OAuthLoginRunner {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { WizardAnswerCheckpointV1, WizardCheckpointStore } from './checkpoint'
|
|
2
|
+
import { isHatched } from './index'
|
|
3
|
+
|
|
4
|
+
export type InitProgressStatus =
|
|
5
|
+
| { kind: 'none' }
|
|
6
|
+
| { kind: 'incomplete'; checkpoint: WizardAnswerCheckpointV1 }
|
|
7
|
+
| { kind: 'complete-stale-checkpoint'; checkpoint: WizardAnswerCheckpointV1 }
|
|
8
|
+
|
|
9
|
+
export interface DetectInitProgressOptions {
|
|
10
|
+
cwd: string
|
|
11
|
+
checkpointStore: WizardCheckpointStore
|
|
12
|
+
isHatched?: (dir: string) => Promise<boolean>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Single shared predicate for "is this init incomplete?", consumed by both the
|
|
16
|
+
// init resume-prompt and the start/restart launchers so the two never drift.
|
|
17
|
+
//
|
|
18
|
+
// `isHatched` is the completion authority — NOT the presence of node_modules,
|
|
19
|
+
// Dockerfile, or typeclaw.json, which are intermediate artifacts that start can
|
|
20
|
+
// regenerate. A checkpoint that outlives a hatched agent (clear failed after a
|
|
21
|
+
// successful run) is reported as `complete-stale-checkpoint` so callers can
|
|
22
|
+
// opportunistically clean it up instead of falsely blocking a working agent.
|
|
23
|
+
export async function detectInitProgress(options: DetectInitProgressOptions): Promise<InitProgressStatus> {
|
|
24
|
+
const hatchedCheck = options.isHatched ?? isHatched
|
|
25
|
+
const checkpoint = await options.checkpointStore.load(options.cwd)
|
|
26
|
+
if (checkpoint === undefined) return { kind: 'none' }
|
|
27
|
+
if (await hatchedCheck(options.cwd)) return { kind: 'complete-stale-checkpoint', checkpoint }
|
|
28
|
+
return { kind: 'incomplete', checkpoint }
|
|
29
|
+
}
|
|
@@ -10,6 +10,8 @@ const PROVIDER_PROBE: Partial<Record<KnownProviderId, { url: string; authHeader:
|
|
|
10
10
|
xai: { url: 'https://api.x.ai/v1/models', authHeader: 'bearer' },
|
|
11
11
|
minimax: { url: 'https://api.minimax.io/v1/models', authHeader: 'bearer' },
|
|
12
12
|
deepseek: { url: 'https://api.deepseek.com/models', authHeader: 'bearer' },
|
|
13
|
+
moonshot: { url: 'https://api.moonshot.ai/v1/models', authHeader: 'bearer' },
|
|
14
|
+
'moonshot-coding': { url: 'https://api.kimi.com/coding/v1/models', authHeader: 'bearer' },
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
// When a base-URL override (ANTHROPIC_BASE_URL / OPENAI_BASE_URL) points at a
|
|
@@ -165,6 +167,8 @@ export const API_KEY_DASHBOARD_URL: Partial<Record<KnownProviderId, string>> = {
|
|
|
165
167
|
xai: 'https://console.x.ai',
|
|
166
168
|
minimax: 'https://platform.minimax.io/user-center/basic-information/interface-key',
|
|
167
169
|
deepseek: 'https://platform.deepseek.com/api_keys',
|
|
170
|
+
moonshot: 'https://platform.moonshot.ai/console/api-keys',
|
|
171
|
+
'moonshot-coding': 'https://www.kimi.com/code/console',
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
// MiniMax sells the same `minimax` provider under two billing surfaces that
|
package/src/inspect/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ export { originLabel, shortSessionId } from './label'
|
|
|
14
14
|
export { renderEvent } from './render'
|
|
15
15
|
export { replayJsonl } from './replay'
|
|
16
16
|
export { streamLive } from './live'
|
|
17
|
+
export { fetchLiveSessions } from './live-list'
|
|
18
|
+
export type { FetchLiveSessionsOptions } from './live-list'
|
|
17
19
|
export { parseDuration, parseFilter } from './types'
|
|
18
20
|
export type { InspectCategory, InspectEvent, InspectFilter } from './types'
|
|
19
21
|
export { runInspectLoop, runViewerLoop } from './loop'
|
|
@@ -219,12 +221,17 @@ export async function streamSessionEvents(opts: StreamSessionEventsOptions): Pro
|
|
|
219
221
|
opts.onEvent(event)
|
|
220
222
|
}
|
|
221
223
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
// A live-only session (registry-derived, no .jsonl yet) has an empty
|
|
225
|
+
// sessionFile: skip replay and go straight to the live tail. Replaying ''
|
|
226
|
+
// would just emit a spurious "file does not exist" warning.
|
|
227
|
+
if (opts.summary.sessionFile !== '') {
|
|
228
|
+
for await (const event of replayJsonl(
|
|
229
|
+
opts.summary.sessionFile,
|
|
230
|
+
opts.onWarn !== undefined ? { onWarn: opts.onWarn } : {},
|
|
231
|
+
)) {
|
|
232
|
+
if (aborted()) return { escToPicker: true }
|
|
233
|
+
deliver(event)
|
|
234
|
+
}
|
|
228
235
|
}
|
|
229
236
|
opts.onPhase?.({ phase: 'replay-end' })
|
|
230
237
|
|
package/src/inspect/item-list.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { LiveSessionPayload } from '@/shared'
|
|
2
|
+
|
|
1
3
|
import type { ViewerItem } from './item'
|
|
2
|
-
import { listSessions, type ListSessionsOptions, type SessionSummary } from './session-list'
|
|
4
|
+
import { listSessions, type ListSessionsOptions, mergeLiveSessions, type SessionSummary } from './session-list'
|
|
3
5
|
|
|
4
6
|
export type ListViewerItemsOptions = ListSessionsOptions & {
|
|
5
7
|
containerRunning: boolean
|
|
@@ -9,6 +11,9 @@ export type ListViewerItemsOptions = ListSessionsOptions & {
|
|
|
9
11
|
// (most-recent) tui transcript — and any older tui transcript the heuristic
|
|
10
12
|
// would otherwise promote — must NOT be offered as a writable live row.
|
|
11
13
|
allowWritable?: boolean
|
|
14
|
+
// Registry sessions not yet flushed to disk, fetched by the CLI over the
|
|
15
|
+
// /inspect WS. The lib layer stays I/O-free; the caller owns the connection.
|
|
16
|
+
liveSessions?: LiveSessionPayload[]
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export type ViewerList = {
|
|
@@ -23,7 +28,11 @@ export type ViewerList = {
|
|
|
23
28
|
// sessions are read-only. The `logs` row is appended last (container stdout,
|
|
24
29
|
// available offline) so it sits below the divider in the picker.
|
|
25
30
|
export async function listViewerItems(opts: ListViewerItemsOptions): Promise<ViewerList> {
|
|
26
|
-
const
|
|
31
|
+
const diskSessions = await listSessions(opts)
|
|
32
|
+
const sessions =
|
|
33
|
+
opts.liveSessions !== undefined && opts.liveSessions.length > 0
|
|
34
|
+
? mergeLiveSessions(diskSessions, opts.liveSessions)
|
|
35
|
+
: diskSessions
|
|
27
36
|
const allowWritable = opts.allowWritable !== false
|
|
28
37
|
const writableSessionId = opts.containerRunning && allowWritable ? pickWritableSession(sessions) : null
|
|
29
38
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { InspectClientMessage, InspectServerMessage, LiveSessionPayload } from '@/shared'
|
|
2
|
+
|
|
3
|
+
export type FetchLiveSessionsOptions = {
|
|
4
|
+
url: string
|
|
5
|
+
signal?: AbortSignal
|
|
6
|
+
WebSocketImpl?: typeof WebSocket
|
|
7
|
+
timeoutMs?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 5_000
|
|
11
|
+
|
|
12
|
+
// One-shot query of the container's in-memory session registry over the
|
|
13
|
+
// /inspect WS: open, send list_live, read the single reply, close. Failure
|
|
14
|
+
// (container down, timeout, abort) resolves to [] so the picker degrades to the
|
|
15
|
+
// disk-only listing rather than erroring — the live overlay is best-effort.
|
|
16
|
+
export async function fetchLiveSessions(opts: FetchLiveSessionsOptions): Promise<LiveSessionPayload[]> {
|
|
17
|
+
const WS = opts.WebSocketImpl ?? WebSocket
|
|
18
|
+
if (opts.signal?.aborted === true) return []
|
|
19
|
+
|
|
20
|
+
return new Promise<LiveSessionPayload[]>((resolve) => {
|
|
21
|
+
let settled = false
|
|
22
|
+
const ws = new WS(opts.url)
|
|
23
|
+
|
|
24
|
+
const finish = (result: LiveSessionPayload[]): void => {
|
|
25
|
+
if (settled) return
|
|
26
|
+
settled = true
|
|
27
|
+
clearTimeout(timer)
|
|
28
|
+
try {
|
|
29
|
+
ws.close()
|
|
30
|
+
} catch {
|
|
31
|
+
/* ignore */
|
|
32
|
+
}
|
|
33
|
+
resolve(result)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const timer = setTimeout(() => finish([]), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
37
|
+
|
|
38
|
+
if (opts.signal !== undefined) {
|
|
39
|
+
opts.signal.addEventListener('abort', () => finish([]), { once: true })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ws.addEventListener('open', () => {
|
|
43
|
+
const req: InspectClientMessage = { type: 'list_live' }
|
|
44
|
+
try {
|
|
45
|
+
ws.send(JSON.stringify(req))
|
|
46
|
+
} catch {
|
|
47
|
+
finish([])
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
ws.addEventListener('message', (e) => {
|
|
52
|
+
let msg: InspectServerMessage
|
|
53
|
+
try {
|
|
54
|
+
msg = JSON.parse(String((e as MessageEvent).data)) as InspectServerMessage
|
|
55
|
+
} catch {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if (msg.type === 'live_sessions') finish(msg.sessions)
|
|
59
|
+
else if (msg.type === 'error') finish([])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
ws.addEventListener('error', () => finish([]))
|
|
63
|
+
ws.addEventListener('close', () => finish([]))
|
|
64
|
+
})
|
|
65
|
+
}
|
package/src/inspect/open-item.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
1
3
|
import type { LiveSourceFactory, RunInspectResult } from './index'
|
|
2
4
|
import { createTranscriptView, streamInspectTarget } from './index'
|
|
3
5
|
import type { ViewerItem } from './item'
|
|
4
6
|
import { streamLogs } from './logs-item'
|
|
5
7
|
import type { OpenItemContext, OpenItemResult, TailController } from './loop'
|
|
8
|
+
import { resolveSession } from './session-list'
|
|
6
9
|
import { runTuiViewer } from './tui-item'
|
|
7
10
|
import type { InspectFilter } from './types'
|
|
8
11
|
|
|
@@ -28,7 +31,14 @@ export type OpenViewerDeps = {
|
|
|
28
31
|
// would corrupt input). The line/JSON session path and logs run UNDER the tail
|
|
29
32
|
// scope, which owns the raw-mode esc/q/ctrl-c handling.
|
|
30
33
|
export function openViewerItem(deps: OpenViewerDeps) {
|
|
31
|
-
return async (
|
|
34
|
+
return async (rawItem: ViewerItem, ctx: OpenItemContext): Promise<OpenItemResult> => {
|
|
35
|
+
// A live-only row captured `sessionFile: ''` when it was listed. By the time
|
|
36
|
+
// the user opens it the reply may have flushed to disk AND the registry
|
|
37
|
+
// entry may be gone — leaving the live tail broadcast-only and skipping the
|
|
38
|
+
// now-existing transcript. Re-resolve against the sessions dir so a
|
|
39
|
+
// flushed session opens as its real disk summary (replay + live tail).
|
|
40
|
+
const item = await reresolveLiveItem(rawItem, deps.cwd, deps.stderr)
|
|
41
|
+
|
|
32
42
|
if (item.kind === 'tui') {
|
|
33
43
|
const result = await runTuiViewer({
|
|
34
44
|
resolveUrl: deps.resolveTuiUrl,
|
|
@@ -93,6 +103,17 @@ export function openViewerItem(deps: OpenViewerDeps) {
|
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
105
|
|
|
106
|
+
export async function reresolveLiveItem(
|
|
107
|
+
item: ViewerItem,
|
|
108
|
+
cwd: string,
|
|
109
|
+
onWarn: (line: string) => void,
|
|
110
|
+
): Promise<ViewerItem> {
|
|
111
|
+
if (item.kind === 'logs' || item.summary.live !== true) return item
|
|
112
|
+
const resolved = await resolveSession(join(cwd, 'sessions'), item.summary.sessionId, onWarn)
|
|
113
|
+
if (!resolved.ok) return item
|
|
114
|
+
return { kind: 'session', summary: resolved.summary, writable: false }
|
|
115
|
+
}
|
|
116
|
+
|
|
96
117
|
function toResult(escToPicker: boolean, scope: TailController): RunInspectResult {
|
|
97
118
|
if (scope.intent() === 'exit') return { ok: true, exitCode: 0 }
|
|
98
119
|
if (escToPicker) return { ok: true, exitCode: 0, escToPicker: true }
|