typeclaw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/auth.schema.json +63 -0
- package/cron.schema.json +96 -0
- package/package.json +72 -0
- package/scripts/emit-base-dockerfile.ts +5 -0
- package/scripts/generate-schema.ts +34 -0
- package/secrets.schema.json +63 -0
- package/src/agent/auth.ts +119 -0
- package/src/agent/compaction.ts +35 -0
- package/src/agent/git-nudge.ts +95 -0
- package/src/agent/index.ts +451 -0
- package/src/agent/plugin-tools.ts +269 -0
- package/src/agent/reload-tool.ts +71 -0
- package/src/agent/self.ts +45 -0
- package/src/agent/session-origin.ts +288 -0
- package/src/agent/subagents.ts +253 -0
- package/src/agent/system-prompt.ts +68 -0
- package/src/agent/tools/channel-fetch-attachment.ts +118 -0
- package/src/agent/tools/channel-history.ts +119 -0
- package/src/agent/tools/channel-reply.ts +182 -0
- package/src/agent/tools/channel-send.ts +212 -0
- package/src/agent/tools/ddg.ts +218 -0
- package/src/agent/tools/restart.ts +122 -0
- package/src/agent/tools/stream-snapshot.ts +181 -0
- package/src/agent/tools/webfetch/fetch.ts +102 -0
- package/src/agent/tools/webfetch/index.ts +1 -0
- package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
- package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
- package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
- package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
- package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
- package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
- package/src/agent/tools/webfetch/tool.ts +281 -0
- package/src/agent/tools/webfetch/types.ts +33 -0
- package/src/agent/tools/websearch.ts +96 -0
- package/src/agent/tools/wikipedia.ts +52 -0
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
- package/src/bundled-plugins/agent-browser/index.ts +179 -0
- package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
- package/src/bundled-plugins/agent-browser/shim.ts +152 -0
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
- package/src/bundled-plugins/guard/index.ts +26 -0
- package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
- package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
- package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
- package/src/bundled-plugins/guard/policy.ts +18 -0
- package/src/bundled-plugins/memory/README.md +71 -0
- package/src/bundled-plugins/memory/append-tool.ts +84 -0
- package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
- package/src/bundled-plugins/memory/dreaming.ts +470 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
- package/src/bundled-plugins/memory/index.ts +238 -0
- package/src/bundled-plugins/memory/load-memory.ts +122 -0
- package/src/bundled-plugins/memory/memory-logger.ts +257 -0
- package/src/bundled-plugins/memory/secret-detector.ts +49 -0
- package/src/bundled-plugins/memory/watermark.ts +15 -0
- package/src/bundled-plugins/security/index.ts +35 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
- package/src/bundled-plugins/security/policy.ts +9 -0
- package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
- package/src/channels/adapters/discord-bot-classify.ts +148 -0
- package/src/channels/adapters/discord-bot.ts +640 -0
- package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
- package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
- package/src/channels/adapters/kakaotalk-classify.ts +77 -0
- package/src/channels/adapters/kakaotalk.ts +622 -0
- package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
- package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
- package/src/channels/adapters/slack-bot-classify.ts +213 -0
- package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
- package/src/channels/adapters/slack-bot-time.ts +10 -0
- package/src/channels/adapters/slack-bot.ts +881 -0
- package/src/channels/adapters/telegram-bot-classify.ts +155 -0
- package/src/channels/adapters/telegram-bot-format.ts +309 -0
- package/src/channels/adapters/telegram-bot.ts +604 -0
- package/src/channels/engagement.ts +227 -0
- package/src/channels/index.ts +21 -0
- package/src/channels/manager.ts +292 -0
- package/src/channels/membership-cache.ts +116 -0
- package/src/channels/membership-from-history.ts +53 -0
- package/src/channels/membership.ts +30 -0
- package/src/channels/participants.ts +47 -0
- package/src/channels/persistence.ts +209 -0
- package/src/channels/reloadable.ts +28 -0
- package/src/channels/router.ts +1570 -0
- package/src/channels/schema.ts +273 -0
- package/src/channels/types.ts +160 -0
- package/src/cli/channel.ts +403 -0
- package/src/cli/compose-status.ts +95 -0
- package/src/cli/compose.ts +240 -0
- package/src/cli/hostd.ts +163 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/init.ts +592 -0
- package/src/cli/logs.ts +38 -0
- package/src/cli/reload.ts +68 -0
- package/src/cli/restart.ts +66 -0
- package/src/cli/run.ts +77 -0
- package/src/cli/shell.ts +33 -0
- package/src/cli/start.ts +57 -0
- package/src/cli/status.ts +178 -0
- package/src/cli/stop.ts +31 -0
- package/src/cli/tui.ts +35 -0
- package/src/cli/ui.ts +110 -0
- package/src/commands/index.ts +74 -0
- package/src/compose/discover.ts +43 -0
- package/src/compose/index.ts +25 -0
- package/src/compose/logs.ts +162 -0
- package/src/compose/restart.ts +69 -0
- package/src/compose/start.ts +62 -0
- package/src/compose/status.ts +28 -0
- package/src/compose/stop.ts +43 -0
- package/src/config/config.ts +424 -0
- package/src/config/index.ts +25 -0
- package/src/config/providers.ts +234 -0
- package/src/config/reloadable.ts +47 -0
- package/src/container/index.ts +27 -0
- package/src/container/logs.ts +37 -0
- package/src/container/port.ts +137 -0
- package/src/container/shared.ts +290 -0
- package/src/container/shell.ts +58 -0
- package/src/container/start.ts +670 -0
- package/src/container/status.ts +76 -0
- package/src/container/stop.ts +120 -0
- package/src/container/verify-running.ts +149 -0
- package/src/cron/consumer.ts +138 -0
- package/src/cron/index.ts +54 -0
- package/src/cron/reloadable.ts +64 -0
- package/src/cron/scheduler.ts +200 -0
- package/src/cron/schema.ts +96 -0
- package/src/hostd/client.ts +113 -0
- package/src/hostd/daemon.ts +587 -0
- package/src/hostd/index.ts +25 -0
- package/src/hostd/paths.ts +82 -0
- package/src/hostd/portbroker-manager.ts +101 -0
- package/src/hostd/protocol.ts +48 -0
- package/src/hostd/spawn.ts +224 -0
- package/src/hostd/supervisor.ts +60 -0
- package/src/hostd/tailscale.ts +172 -0
- package/src/hostd/version.ts +115 -0
- package/src/init/dockerfile.ts +327 -0
- package/src/init/ensure-deps.ts +152 -0
- package/src/init/gitignore.ts +46 -0
- package/src/init/hatching.ts +60 -0
- package/src/init/index.ts +786 -0
- package/src/init/kakaotalk-auth.ts +114 -0
- package/src/init/models-dev.ts +130 -0
- package/src/init/oauth-login.ts +74 -0
- package/src/init/packagejson.ts +94 -0
- package/src/init/paths.ts +2 -0
- package/src/init/run-bun-install.ts +20 -0
- package/src/markdown/chunk.ts +299 -0
- package/src/markdown/index.ts +1 -0
- package/src/plugin/context.ts +40 -0
- package/src/plugin/define.ts +35 -0
- package/src/plugin/hooks.ts +204 -0
- package/src/plugin/index.ts +63 -0
- package/src/plugin/loader.ts +111 -0
- package/src/plugin/manager.ts +136 -0
- package/src/plugin/registry.ts +145 -0
- package/src/plugin/skills.ts +62 -0
- package/src/plugin/types.ts +172 -0
- package/src/portbroker/bind-with-forward.ts +102 -0
- package/src/portbroker/container-server.ts +305 -0
- package/src/portbroker/forward-result-bus.ts +36 -0
- package/src/portbroker/hostd-client.ts +443 -0
- package/src/portbroker/index.ts +33 -0
- package/src/portbroker/policy.ts +24 -0
- package/src/portbroker/proc-net-tcp.ts +72 -0
- package/src/portbroker/protocol.ts +39 -0
- package/src/reload/client.ts +59 -0
- package/src/reload/index.ts +3 -0
- package/src/reload/registry.ts +60 -0
- package/src/reload/types.ts +13 -0
- package/src/run/bundled-plugins.ts +24 -0
- package/src/run/channel-session-factory.ts +105 -0
- package/src/run/index.ts +432 -0
- package/src/run/plugin-runtime.ts +43 -0
- package/src/run/schema-with-plugins.ts +14 -0
- package/src/secrets/index.ts +13 -0
- package/src/secrets/migrate.ts +95 -0
- package/src/secrets/schema.ts +75 -0
- package/src/secrets/storage.ts +231 -0
- package/src/server/index.ts +436 -0
- package/src/sessions/index.ts +23 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/local-time.ts +21 -0
- package/src/shared/protocol.ts +25 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
- package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
- package/src/skills/typeclaw-config/SKILL.md +643 -0
- package/src/skills/typeclaw-cron/SKILL.md +159 -0
- package/src/skills/typeclaw-git/SKILL.md +89 -0
- package/src/skills/typeclaw-memory/SKILL.md +174 -0
- package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
- package/src/skills/typeclaw-plugins/SKILL.md +594 -0
- package/src/skills/typeclaw-skills/SKILL.md +246 -0
- package/src/stream/broker.ts +161 -0
- package/src/stream/index.ts +16 -0
- package/src/stream/types.ts +69 -0
- package/src/tui/client.ts +45 -0
- package/src/tui/format.ts +317 -0
- package/src/tui/index.ts +225 -0
- package/src/tui/theme.ts +41 -0
- package/typeclaw.schema.json +826 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { KakaoCredentialManager } from 'agent-messenger/kakaotalk'
|
|
5
|
+
|
|
6
|
+
export type KakaotalkBootstrapStatus = { ok: true } | { ok: false; reason: string }
|
|
7
|
+
|
|
8
|
+
export type KakaotalkLoginCallbacks = {
|
|
9
|
+
onPasscode: (passcode: string) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type KakaotalkLoginInput = {
|
|
13
|
+
email: string
|
|
14
|
+
password: string
|
|
15
|
+
agentDir: string
|
|
16
|
+
callbacks: KakaotalkLoginCallbacks
|
|
17
|
+
loginFlow?: LoginFlowFn
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type LoginFlowOptions = {
|
|
21
|
+
email: string
|
|
22
|
+
password: string
|
|
23
|
+
deviceType?: 'pc' | 'tablet'
|
|
24
|
+
force?: boolean
|
|
25
|
+
savedDeviceUuid?: string
|
|
26
|
+
onPasscodeDisplay?: (code: string) => void
|
|
27
|
+
debugLog?: (message: string) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type LoginFlowCredentials = {
|
|
31
|
+
access_token: string
|
|
32
|
+
refresh_token: string
|
|
33
|
+
user_id: string
|
|
34
|
+
device_uuid: string
|
|
35
|
+
device_type: 'pc' | 'tablet'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type LoginFlowResult = {
|
|
39
|
+
authenticated: boolean
|
|
40
|
+
next_action?: string
|
|
41
|
+
message?: string
|
|
42
|
+
warning?: string
|
|
43
|
+
error?: string
|
|
44
|
+
credentials?: LoginFlowCredentials
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type LoginFlowFn = (options: LoginFlowOptions) => Promise<LoginFlowResult>
|
|
48
|
+
|
|
49
|
+
export function kakaotalkConfigDir(agentDir: string): string {
|
|
50
|
+
return join(agentDir, 'workspace', '.agent-messenger')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function runKakaotalkBootstrap(input: KakaotalkLoginInput): Promise<KakaotalkBootstrapStatus> {
|
|
54
|
+
const configDir = kakaotalkConfigDir(input.agentDir)
|
|
55
|
+
try {
|
|
56
|
+
const loginFlow = input.loginFlow ?? (await resolveLoginFlow())
|
|
57
|
+
const credManager = new KakaoCredentialManager(configDir)
|
|
58
|
+
const pending = await credManager.loadPendingLogin()
|
|
59
|
+
const existing = await credManager.getAccount()
|
|
60
|
+
const savedDeviceUuid =
|
|
61
|
+
pending?.device_uuid ?? (existing?.auth_method === 'login' ? existing.device_uuid : undefined)
|
|
62
|
+
|
|
63
|
+
const result = await loginFlow({
|
|
64
|
+
email: input.email,
|
|
65
|
+
password: input.password,
|
|
66
|
+
deviceType: 'tablet',
|
|
67
|
+
force: false,
|
|
68
|
+
...(savedDeviceUuid !== undefined ? { savedDeviceUuid } : {}),
|
|
69
|
+
onPasscodeDisplay: input.callbacks.onPasscode,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (!result.authenticated || !result.credentials) {
|
|
73
|
+
const reason = result.message ?? result.error ?? 'agent-kakaotalk did not authenticate (check email/password)'
|
|
74
|
+
return { ok: false, reason }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const now = new Date().toISOString()
|
|
78
|
+
const accountId = result.credentials.user_id || 'default'
|
|
79
|
+
await credManager.setAccount({
|
|
80
|
+
account_id: accountId,
|
|
81
|
+
oauth_token: result.credentials.access_token,
|
|
82
|
+
user_id: result.credentials.user_id,
|
|
83
|
+
refresh_token: result.credentials.refresh_token,
|
|
84
|
+
device_uuid: result.credentials.device_uuid,
|
|
85
|
+
device_type: result.credentials.device_type,
|
|
86
|
+
auth_method: 'login',
|
|
87
|
+
created_at: now,
|
|
88
|
+
updated_at: now,
|
|
89
|
+
})
|
|
90
|
+
await credManager.setCurrentAccount(accountId)
|
|
91
|
+
await credManager.clearPendingLogin()
|
|
92
|
+
|
|
93
|
+
return { ok: true }
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// agent-messenger does not export `loginFlow` from its public `exports` map
|
|
100
|
+
// (only the runtime client + credential manager), so we resolve the package's
|
|
101
|
+
// installed location and import the implementation file directly. This
|
|
102
|
+
// bypasses the exports gate but stays within the same installed copy of the
|
|
103
|
+
// package — no version drift risk. If a future agent-messenger release adds
|
|
104
|
+
// `loginFlow` to its public exports, swap this for a normal import and delete
|
|
105
|
+
// the resolveLoginFlow helper.
|
|
106
|
+
async function resolveLoginFlow(): Promise<LoginFlowFn> {
|
|
107
|
+
const require = createRequire(import.meta.url)
|
|
108
|
+
const pkgJson = require.resolve('agent-messenger/package.json')
|
|
109
|
+
const pkgDir = pkgJson.replace(/\/package\.json$/, '')
|
|
110
|
+
const mod = (await import(`${pkgDir}/dist/src/platforms/kakaotalk/auth/kakao-login.js`)) as {
|
|
111
|
+
loginFlow: LoginFlowFn
|
|
112
|
+
}
|
|
113
|
+
return mod.loginFlow
|
|
114
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { KNOWN_PROVIDERS, type KnownModelRef, type KnownProviderId, listKnownModelRefs } from '@/config/providers'
|
|
2
|
+
|
|
3
|
+
const MODELS_DEV_URL = 'https://models.dev/api.json'
|
|
4
|
+
const REQUEST_TIMEOUT_MS = 10_000
|
|
5
|
+
|
|
6
|
+
// models.dev keys providers by a string id that does NOT always match our
|
|
7
|
+
// KnownProviderId. Specifically, they ship Fireworks under `fireworks-ai`.
|
|
8
|
+
// This map is the single place that bridges the two namespaces; every other
|
|
9
|
+
// helper in this file works in OUR namespace.
|
|
10
|
+
const PROVIDER_TO_MODELS_DEV: Record<KnownProviderId, string> = {
|
|
11
|
+
openai: 'openai',
|
|
12
|
+
// openai-codex models live under the `openai` namespace on models.dev too
|
|
13
|
+
// (Codex is a backend, not a separate provider in their taxonomy). Curated
|
|
14
|
+
// entries are surfaced regardless of upstream membership.
|
|
15
|
+
'openai-codex': 'openai',
|
|
16
|
+
fireworks: 'fireworks-ai',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ModelOption = {
|
|
20
|
+
ref: KnownModelRef
|
|
21
|
+
providerId: KnownProviderId
|
|
22
|
+
providerName: string
|
|
23
|
+
modelId: string
|
|
24
|
+
modelName: string
|
|
25
|
+
reasoning: boolean
|
|
26
|
+
contextWindow: number | null
|
|
27
|
+
curated: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ModelsDevModel = {
|
|
31
|
+
id?: string
|
|
32
|
+
name?: string
|
|
33
|
+
reasoning?: boolean
|
|
34
|
+
tool_call?: boolean
|
|
35
|
+
status?: string
|
|
36
|
+
release_date?: string
|
|
37
|
+
modalities?: { input?: string[]; output?: string[] }
|
|
38
|
+
limit?: { context?: number }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ModelsDevProvider = {
|
|
42
|
+
id?: string
|
|
43
|
+
name?: string
|
|
44
|
+
models?: Record<string, ModelsDevModel>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type FetchModelsResult = {
|
|
48
|
+
options: ModelOption[]
|
|
49
|
+
source: 'models.dev' | 'curated'
|
|
50
|
+
warning?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Pulls the live model catalog from models.dev, intersects it with our
|
|
54
|
+
// curated KNOWN_PROVIDERS allowlist, and returns one ModelOption per
|
|
55
|
+
// (provider, model) pair the user is allowed to pick at init time.
|
|
56
|
+
//
|
|
57
|
+
// Falls back to the curated list alone if the network is unreachable, the
|
|
58
|
+
// response is malformed, or any unexpected error fires — the wizard MUST
|
|
59
|
+
// stay functional offline because `typeclaw init` is the very first thing a
|
|
60
|
+
// user does on a fresh machine, often before networking is sorted.
|
|
61
|
+
export async function fetchModelOptions(
|
|
62
|
+
options: { fetchImpl?: typeof fetch; timeoutMs?: number } = {},
|
|
63
|
+
): Promise<FetchModelsResult> {
|
|
64
|
+
const fetchImpl = options.fetchImpl ?? fetch
|
|
65
|
+
const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetchImpl(MODELS_DEV_URL, { signal: AbortSignal.timeout(timeoutMs) })
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
return { options: curatedOptions(), source: 'curated', warning: `models.dev returned ${res.status}` }
|
|
70
|
+
}
|
|
71
|
+
const data = (await res.json()) as Record<string, ModelsDevProvider>
|
|
72
|
+
return { options: mergeWithCurated(data), source: 'models.dev' }
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
75
|
+
return { options: curatedOptions(), source: 'curated', warning: reason }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The curated-only path: every model in KNOWN_PROVIDERS, sorted with the
|
|
80
|
+
// default model first so the picker can use index-0 as `initialValue`.
|
|
81
|
+
export function curatedOptions(): ModelOption[] {
|
|
82
|
+
const refs = listKnownModelRefs()
|
|
83
|
+
return refs.map((ref) => buildOption(ref, { curated: true }))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// `data` is the parsed models.dev JSON. We walk only the providers we care
|
|
87
|
+
// about (openai, fireworks-ai) and only emit options for models that are
|
|
88
|
+
// also in our curated allowlist — anything outside the allowlist would fail
|
|
89
|
+
// schema validation when written to typeclaw.json. Curated entries that
|
|
90
|
+
// models.dev doesn't list (e.g. kimi-k2p6-turbo) are still surfaced so the
|
|
91
|
+
// user can pick them.
|
|
92
|
+
function mergeWithCurated(data: Record<string, ModelsDevProvider>): ModelOption[] {
|
|
93
|
+
const out: ModelOption[] = []
|
|
94
|
+
for (const providerId of Object.keys(KNOWN_PROVIDERS) as KnownProviderId[]) {
|
|
95
|
+
const known = KNOWN_PROVIDERS[providerId]
|
|
96
|
+
const upstream = data[PROVIDER_TO_MODELS_DEV[providerId]]
|
|
97
|
+
const upstreamModels = upstream?.models ?? {}
|
|
98
|
+
for (const modelId of Object.keys(known.models)) {
|
|
99
|
+
const upstreamModel = upstreamModels[modelId]
|
|
100
|
+
const ref = `${providerId}/${modelId}` as KnownModelRef
|
|
101
|
+
out.push(buildOption(ref, { curated: true, upstream: upstreamModel }))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type BuildOptionOpts = {
|
|
108
|
+
curated: boolean
|
|
109
|
+
upstream?: ModelsDevModel
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildOption(ref: KnownModelRef, opts: BuildOptionOpts): ModelOption {
|
|
113
|
+
const slash = ref.indexOf('/')
|
|
114
|
+
const providerId = ref.slice(0, slash) as KnownProviderId
|
|
115
|
+
const modelId = ref.slice(slash + 1)
|
|
116
|
+
const provider = KNOWN_PROVIDERS[providerId]
|
|
117
|
+
const curatedModel = (
|
|
118
|
+
provider.models as Record<string, { name: string; contextWindow?: number; reasoning?: boolean }>
|
|
119
|
+
)[modelId]
|
|
120
|
+
return {
|
|
121
|
+
ref,
|
|
122
|
+
providerId,
|
|
123
|
+
providerName: provider.name,
|
|
124
|
+
modelId,
|
|
125
|
+
modelName: opts.upstream?.name ?? curatedModel?.name ?? modelId,
|
|
126
|
+
reasoning: opts.upstream?.reasoning ?? curatedModel?.reasoning ?? false,
|
|
127
|
+
contextWindow: opts.upstream?.limit?.context ?? curatedModel?.contextWindow ?? null,
|
|
128
|
+
curated: opts.curated,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
KNOWN_PROVIDERS,
|
|
5
|
+
providerForModelRef,
|
|
6
|
+
supportsOAuth,
|
|
7
|
+
type KnownModelRef,
|
|
8
|
+
type KnownProviderId,
|
|
9
|
+
} from '@/config/providers'
|
|
10
|
+
import { createSecretsStoreForAgent } from '@/secrets'
|
|
11
|
+
|
|
12
|
+
export type OAuthLoginResult = { ok: true } | { ok: false; reason: string }
|
|
13
|
+
|
|
14
|
+
export type OAuthLoginRunner = (options: { cwd: string; model: KnownModelRef }) => Promise<OAuthLoginResult>
|
|
15
|
+
|
|
16
|
+
// Wrap pi-ai's OAuth callbacks so the CLI doesn't have to know about the
|
|
17
|
+
// upstream callback shape. The CLI only sees three lifecycle events:
|
|
18
|
+
// (1) onAuth(url) — print the URL the user must visit
|
|
19
|
+
// (2) onProgress(message) — show waiting/finalizing status
|
|
20
|
+
// (3) onPrompt(prompt) — ask the user for a manual code if the browser flow
|
|
21
|
+
// can't reach the local callback server. Most users won't see this; it
|
|
22
|
+
// fires when they paste the post-redirect URL by hand.
|
|
23
|
+
export type OAuthCallbacks = {
|
|
24
|
+
onAuth: (url: string, instructions?: string) => void
|
|
25
|
+
onProgress?: (message: string) => void
|
|
26
|
+
onPrompt: (message: string, placeholder?: string) => Promise<string | null>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Default runner: real OAuth flow against pi-ai. Tests inject a stub to skip
|
|
30
|
+
// network entirely. The runner's only job is to log in, write to the secrets
|
|
31
|
+
// file, and report ok/error — it does NOT update typeclaw.json (the model
|
|
32
|
+
// ref is already chosen by the caller and written by `scaffold`).
|
|
33
|
+
export function makeOAuthLoginRunner(callbacks: OAuthCallbacks): OAuthLoginRunner {
|
|
34
|
+
return async ({ cwd, model }) => {
|
|
35
|
+
const providerId = providerForModelRef(model)
|
|
36
|
+
const provider = KNOWN_PROVIDERS[providerId]
|
|
37
|
+
if (!supportsOAuth(provider) || !provider.oauthProviderId) {
|
|
38
|
+
return { ok: false, reason: `Provider ${provider.name} does not support OAuth` }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const secrets = createSecretsStoreForAgent(join(cwd, 'secrets.json'))
|
|
43
|
+
await secrets.login(provider.oauthProviderId, {
|
|
44
|
+
onAuth: (info) => callbacks.onAuth(info.url, info.instructions),
|
|
45
|
+
onProgress: callbacks.onProgress,
|
|
46
|
+
onPrompt: async (prompt) => {
|
|
47
|
+
const value = await callbacks.onPrompt(prompt.message, prompt.placeholder)
|
|
48
|
+
if (value === null) {
|
|
49
|
+
throw new Error('Login cancelled by user')
|
|
50
|
+
}
|
|
51
|
+
return value
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
return { ok: true }
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Test seam: lets unit tests assert "OAuth login was invoked with these
|
|
62
|
+
// params" without spinning up a real secrets store / browser callback server.
|
|
63
|
+
export type FakeOAuthLoginRunnerOptions = {
|
|
64
|
+
result?: OAuthLoginResult
|
|
65
|
+
onCalled?: (options: { cwd: string; model: KnownModelRef; providerId: KnownProviderId }) => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function makeFakeOAuthLoginRunner(options: FakeOAuthLoginRunnerOptions = {}): OAuthLoginRunner {
|
|
69
|
+
return async ({ cwd, model }) => {
|
|
70
|
+
const providerId = providerForModelRef(model)
|
|
71
|
+
options.onCalled?.({ cwd, model, providerId })
|
|
72
|
+
return options.result ?? { ok: true }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { GITKEEP_FILE, PACKAGES_DIR } from './paths'
|
|
6
|
+
|
|
7
|
+
export const PACKAGE_FILE = 'package.json'
|
|
8
|
+
export const WORKSPACES_GLOB = `${PACKAGES_DIR}/*`
|
|
9
|
+
|
|
10
|
+
export type PackageJsonRefreshResult = {
|
|
11
|
+
changed: boolean
|
|
12
|
+
files: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Migrates an existing agent folder into a bun monorepo layout. Idempotent —
|
|
16
|
+
// running twice is a no-op. Skips silently when:
|
|
17
|
+
// - package.json is missing (folder not initialized yet)
|
|
18
|
+
// - package.json is unparseable (we never touch corrupt user files)
|
|
19
|
+
// - workspaces is already set (user opted in or customized)
|
|
20
|
+
//
|
|
21
|
+
// Always ensures `packages/<GITKEEP_FILE>` exists so the directory survives the
|
|
22
|
+
// initial git commit, regardless of whether package.json was touched.
|
|
23
|
+
//
|
|
24
|
+
// Returns the list of paths the caller should consider for git auto-commit.
|
|
25
|
+
// The caller (typeclaw start) commits these via the same `commitSystemFile`
|
|
26
|
+
// pattern as .gitignore, keeping the monorepo migration on git's record.
|
|
27
|
+
export async function refreshPackageJson(cwd: string): Promise<PackageJsonRefreshResult> {
|
|
28
|
+
const changed: string[] = []
|
|
29
|
+
const pkgPath = join(cwd, PACKAGE_FILE)
|
|
30
|
+
|
|
31
|
+
if (existsSync(pkgPath)) {
|
|
32
|
+
const updated = await ensureWorkspacesField(pkgPath)
|
|
33
|
+
if (updated) changed.push(PACKAGE_FILE)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const gitkeepRel = join(PACKAGES_DIR, GITKEEP_FILE)
|
|
37
|
+
const gitkeepPath = join(cwd, gitkeepRel)
|
|
38
|
+
if (!existsSync(gitkeepPath)) {
|
|
39
|
+
await mkdir(join(cwd, PACKAGES_DIR), { recursive: true })
|
|
40
|
+
await writeFile(gitkeepPath, '')
|
|
41
|
+
changed.push(gitkeepRel)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { changed: changed.length > 0, files: changed }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function ensureWorkspacesField(pkgPath: string): Promise<boolean> {
|
|
48
|
+
let raw: string
|
|
49
|
+
try {
|
|
50
|
+
raw = await readFile(pkgPath, 'utf8')
|
|
51
|
+
} catch {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let pkg: Record<string, unknown>
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(raw) as unknown
|
|
58
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return false
|
|
59
|
+
pkg = parsed as Record<string, unknown>
|
|
60
|
+
} catch {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if ('workspaces' in pkg) return false
|
|
65
|
+
|
|
66
|
+
// Insertion order matters: place `workspaces` right after `type` (or after
|
|
67
|
+
// top-of-object metadata if `type` is absent) so the diff reads cleanly
|
|
68
|
+
// alongside the buildPackageJson template's field order. Without this, a
|
|
69
|
+
// freshly-migrated package.json has `workspaces` at the bottom — visually
|
|
70
|
+
// jarring and harder to spot on review.
|
|
71
|
+
const next = insertAfterKey(pkg, 'type', 'workspaces', [WORKSPACES_GLOB])
|
|
72
|
+
await writeFile(pkgPath, `${JSON.stringify(next, null, 2)}\n`)
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function insertAfterKey(
|
|
77
|
+
obj: Record<string, unknown>,
|
|
78
|
+
anchor: string,
|
|
79
|
+
key: string,
|
|
80
|
+
value: unknown,
|
|
81
|
+
): Record<string, unknown> {
|
|
82
|
+
const out: Record<string, unknown> = {}
|
|
83
|
+
const keys = Object.keys(obj)
|
|
84
|
+
const anchorIdx = keys.indexOf(anchor)
|
|
85
|
+
if (anchorIdx === -1) {
|
|
86
|
+
return { [key]: value, ...obj }
|
|
87
|
+
}
|
|
88
|
+
for (let i = 0; i < keys.length; i++) {
|
|
89
|
+
const k = keys[i] as string
|
|
90
|
+
out[k] = obj[k]
|
|
91
|
+
if (i === anchorIdx) out[key] = value
|
|
92
|
+
}
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type InstallResult = { ok: true } | { ok: false; reason: string }
|
|
2
|
+
|
|
3
|
+
export async function runBunInstall(cwd: string): Promise<InstallResult> {
|
|
4
|
+
const bun = (globalThis as { Bun?: { spawn: typeof Bun.spawn } }).Bun
|
|
5
|
+
if (!bun) return { ok: false, reason: 'bun runtime not available' }
|
|
6
|
+
try {
|
|
7
|
+
const proc = bun.spawn({
|
|
8
|
+
cmd: ['bun', 'install'],
|
|
9
|
+
cwd,
|
|
10
|
+
stdout: 'pipe',
|
|
11
|
+
stderr: 'pipe',
|
|
12
|
+
})
|
|
13
|
+
const code = await proc.exited
|
|
14
|
+
if (code === 0) return { ok: true }
|
|
15
|
+
const stderr = await new Response(proc.stderr).text()
|
|
16
|
+
return { ok: false, reason: `bun install exited with code ${code}: ${stderr.trim() || 'no stderr'}` }
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) }
|
|
19
|
+
}
|
|
20
|
+
}
|