typeclaw 0.1.5 → 0.2.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 +14 -12
- package/auth.schema.json +41 -0
- package/cron.schema.json +8 -0
- package/package.json +1 -1
- package/secrets.schema.json +41 -0
- package/src/agent/auth.ts +45 -22
- package/src/agent/index.ts +189 -19
- package/src/agent/multimodal/index.ts +12 -0
- package/src/agent/multimodal/look-at.ts +185 -0
- package/src/agent/multimodal/looker.ts +145 -0
- package/src/agent/plugin-tools.ts +30 -1
- package/src/agent/session-origin.ts +194 -46
- package/src/agent/subagents.ts +57 -1
- package/src/agent/system-prompt.ts +1 -1
- package/src/agent/tool-result-budget.ts +121 -0
- package/src/bundled-plugins/backup/index.ts +23 -8
- package/src/bundled-plugins/backup/runner.ts +22 -0
- package/src/bundled-plugins/memory/README.md +7 -4
- package/src/bundled-plugins/memory/append-tool.ts +87 -61
- package/src/bundled-plugins/memory/dreaming.ts +23 -9
- package/src/bundled-plugins/memory/find-entry-tool.ts +62 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +19 -44
- package/src/bundled-plugins/memory/index.ts +91 -8
- package/src/bundled-plugins/memory/load-memory.ts +74 -34
- package/src/bundled-plugins/memory/memory-logger.ts +72 -29
- package/src/bundled-plugins/memory/migration.ts +276 -0
- package/src/bundled-plugins/memory/stream-events.ts +55 -0
- package/src/bundled-plugins/memory/stream-io.ts +63 -0
- package/src/bundled-plugins/memory/watermark.ts +48 -8
- package/src/bundled-plugins/security/index.ts +103 -10
- package/src/bundled-plugins/security/permissions.ts +12 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +51 -18
- package/src/bundled-plugins/tool-result-cap/README.md +9 -4
- package/src/bundled-plugins/tool-result-cap/cap-jsonl.ts +115 -0
- package/src/bundled-plugins/tool-result-cap/cap-result.ts +25 -13
- package/src/bundled-plugins/tool-result-cap/index.ts +16 -2
- package/src/channels/adapters/discord-bot-classify.ts +2 -6
- package/src/channels/adapters/discord-bot.ts +4 -45
- package/src/channels/adapters/kakaotalk-classify.ts +3 -7
- package/src/channels/adapters/kakaotalk.ts +28 -47
- package/src/channels/adapters/slack-bot-classify.ts +2 -6
- package/src/channels/adapters/slack-bot.ts +4 -50
- package/src/channels/adapters/telegram-bot-classify.ts +8 -10
- package/src/channels/adapters/telegram-bot.ts +3 -16
- package/src/channels/index.ts +3 -2
- package/src/channels/manager.ts +15 -1
- package/src/channels/persistence.ts +44 -10
- package/src/channels/router.ts +228 -19
- package/src/channels/schema.ts +6 -156
- package/src/cli/channel.ts +200 -4
- package/src/cli/compose-usage.ts +182 -0
- package/src/cli/compose.ts +33 -0
- package/src/cli/hostd.ts +49 -1
- package/src/cli/index.ts +4 -0
- package/src/cli/init.ts +799 -319
- package/src/cli/model.ts +244 -0
- package/src/cli/provider.ts +404 -0
- package/src/cli/reload.ts +6 -1
- package/src/cli/role.ts +156 -0
- package/src/cli/run.ts +3 -1
- package/src/cli/tui.ts +8 -1
- package/src/cli/usage-args.ts +47 -0
- package/src/cli/usage.ts +97 -0
- package/src/compose/index.ts +1 -0
- package/src/compose/usage.ts +65 -0
- package/src/config/config.ts +385 -12
- package/src/config/index.ts +7 -0
- package/src/config/models-mutation.ts +209 -0
- package/src/config/providers-mutation.ts +250 -0
- package/src/config/providers.ts +141 -2
- package/src/config/reloadable.ts +15 -4
- package/src/container/index.ts +5 -0
- package/src/container/require-running.ts +33 -0
- package/src/container/start.ts +39 -58
- package/src/cron/consumer.ts +22 -2
- package/src/cron/index.ts +45 -4
- package/src/cron/schema.ts +104 -0
- package/src/doctor/checks.ts +50 -33
- package/src/git/system-commit.ts +103 -0
- package/src/hostd/daemon.ts +16 -0
- package/src/hostd/kakao-renewal-manager.ts +223 -0
- package/src/hostd/paths.ts +7 -0
- package/src/init/dockerfile.ts +32 -6
- package/src/init/index.ts +190 -61
- package/src/init/kakaotalk-auth.ts +18 -1
- package/src/init/models-dev.ts +26 -1
- package/src/init/run-owner-claim.ts +77 -0
- package/src/permissions/builtins.ts +70 -0
- package/src/permissions/grant.ts +99 -0
- package/src/permissions/index.ts +29 -0
- package/src/permissions/match-rule.ts +305 -0
- package/src/permissions/permissions.ts +196 -0
- package/src/permissions/resolve.ts +80 -0
- package/src/permissions/schema.ts +79 -0
- package/src/plugin/context.ts +8 -4
- package/src/plugin/define.ts +2 -0
- package/src/plugin/index.ts +2 -0
- package/src/plugin/manager.ts +41 -0
- package/src/plugin/registry.ts +9 -0
- package/src/plugin/types.ts +35 -1
- package/src/role-claim/client.ts +182 -0
- package/src/role-claim/code.ts +53 -0
- package/src/role-claim/controller.ts +194 -0
- package/src/role-claim/index.ts +19 -0
- package/src/role-claim/match-rule.ts +43 -0
- package/src/role-claim/pending.ts +100 -0
- package/src/run/channel-session-factory.ts +76 -5
- package/src/run/index.ts +55 -6
- package/src/secrets/encryption.ts +116 -0
- package/src/secrets/kakao-renewal.ts +248 -0
- package/src/secrets/kakao-store.ts +66 -7
- package/src/secrets/keys.ts +173 -0
- package/src/secrets/schema.ts +23 -0
- package/src/secrets/storage.ts +68 -0
- package/src/server/index.ts +122 -11
- package/src/shared/index.ts +4 -0
- package/src/shared/protocol.ts +27 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +3 -3
- package/src/skills/typeclaw-config/SKILL.md +38 -64
- package/src/skills/typeclaw-memory/SKILL.md +1 -1
- package/src/skills/typeclaw-permissions/SKILL.md +166 -0
- package/src/stream/types.ts +7 -1
- package/src/usage/aggregate.ts +117 -0
- package/src/usage/format.ts +30 -0
- package/src/usage/index.ts +68 -0
- package/src/usage/report.ts +354 -0
- package/src/usage/scan.ts +186 -0
- package/typeclaw.schema.json +57 -45
package/src/cli/model.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { cancel, intro, isCancel, select } from '@clack/prompts'
|
|
2
|
+
import { defineCommand } from 'citty'
|
|
3
|
+
|
|
4
|
+
import { addProfile, listModelProfiles, removeProfile, setProfile } from '@/config/models-mutation'
|
|
5
|
+
import {
|
|
6
|
+
KNOWN_PROVIDERS,
|
|
7
|
+
listKnownModelRefs,
|
|
8
|
+
providerForModelRef,
|
|
9
|
+
type KnownModelRef,
|
|
10
|
+
type KnownProviderId,
|
|
11
|
+
} from '@/config/providers'
|
|
12
|
+
import { findAgentDir, isInitialized } from '@/init'
|
|
13
|
+
|
|
14
|
+
import { c, done, errorLine } from './ui'
|
|
15
|
+
|
|
16
|
+
const setSub = defineCommand({
|
|
17
|
+
meta: {
|
|
18
|
+
name: 'set',
|
|
19
|
+
description: 'set or update a model profile (default | fast | vision | <custom>)',
|
|
20
|
+
},
|
|
21
|
+
args: {
|
|
22
|
+
profile: {
|
|
23
|
+
type: 'positional',
|
|
24
|
+
description: 'profile name (typically `default`); omit to pick interactively',
|
|
25
|
+
required: false,
|
|
26
|
+
},
|
|
27
|
+
ref: {
|
|
28
|
+
type: 'positional',
|
|
29
|
+
description: '<provider>/<model> ref; omit to pick interactively',
|
|
30
|
+
required: false,
|
|
31
|
+
},
|
|
32
|
+
force: {
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
description: 'write even when the target provider has no credentials configured',
|
|
35
|
+
required: false,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
async run({ args }) {
|
|
39
|
+
const cwd = ensureAgentDir()
|
|
40
|
+
const profile = args.profile ?? (await pickProfileName())
|
|
41
|
+
const ref = args.ref ?? (await pickModelRef())
|
|
42
|
+
|
|
43
|
+
intro(`Setting model profile: ${profile} → ${ref}`)
|
|
44
|
+
|
|
45
|
+
const result = setProfile(cwd, profile, ref, { force: args.force === true })
|
|
46
|
+
if (!result.ok) {
|
|
47
|
+
console.error(errorLine(result.reason))
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
done({
|
|
51
|
+
title: c.green(`Profile "${profile}" set to ${ref}.`),
|
|
52
|
+
hints: [{ label: 'If the agent is running:', command: 'typeclaw reload' }],
|
|
53
|
+
})
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const addSub = defineCommand({
|
|
58
|
+
meta: {
|
|
59
|
+
name: 'add',
|
|
60
|
+
description: 'create a new (non-default) model profile; refuses when the profile already exists',
|
|
61
|
+
},
|
|
62
|
+
args: {
|
|
63
|
+
profile: {
|
|
64
|
+
type: 'positional',
|
|
65
|
+
description: 'profile name (must not already exist)',
|
|
66
|
+
required: true,
|
|
67
|
+
},
|
|
68
|
+
ref: {
|
|
69
|
+
type: 'positional',
|
|
70
|
+
description: '<provider>/<model> ref; omit to pick interactively',
|
|
71
|
+
required: false,
|
|
72
|
+
},
|
|
73
|
+
force: {
|
|
74
|
+
type: 'boolean',
|
|
75
|
+
description: 'write even when the target provider has no credentials configured',
|
|
76
|
+
required: false,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
async run({ args }) {
|
|
80
|
+
const cwd = ensureAgentDir()
|
|
81
|
+
const ref = args.ref ?? (await pickModelRef())
|
|
82
|
+
|
|
83
|
+
intro(`Adding model profile: ${args.profile} → ${ref}`)
|
|
84
|
+
|
|
85
|
+
const result = addProfile(cwd, args.profile, ref, { force: args.force === true })
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
console.error(errorLine(result.reason))
|
|
88
|
+
process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
done({
|
|
91
|
+
title: c.green(`Profile "${args.profile}" → ${ref}.`),
|
|
92
|
+
hints: [{ label: 'If the agent is running:', command: 'typeclaw reload' }],
|
|
93
|
+
})
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const removeSub = defineCommand({
|
|
98
|
+
meta: {
|
|
99
|
+
name: 'remove',
|
|
100
|
+
description: 'remove a non-default model profile (cannot remove `default`)',
|
|
101
|
+
},
|
|
102
|
+
args: {
|
|
103
|
+
profile: {
|
|
104
|
+
type: 'positional',
|
|
105
|
+
description: 'profile name to remove',
|
|
106
|
+
required: true,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
async run({ args }) {
|
|
110
|
+
const cwd = ensureAgentDir()
|
|
111
|
+
intro(`Removing model profile: ${args.profile}`)
|
|
112
|
+
const result = removeProfile(cwd, args.profile)
|
|
113
|
+
if (!result.ok) {
|
|
114
|
+
console.error(errorLine(result.reason))
|
|
115
|
+
process.exit(1)
|
|
116
|
+
}
|
|
117
|
+
done({
|
|
118
|
+
title: c.green(`Profile "${args.profile}" removed.`),
|
|
119
|
+
hints: [{ label: 'If the agent is running:', command: 'typeclaw reload' }],
|
|
120
|
+
})
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const listSub = defineCommand({
|
|
125
|
+
meta: {
|
|
126
|
+
name: 'list',
|
|
127
|
+
description: 'list configured model profiles (or all known model refs with --available)',
|
|
128
|
+
},
|
|
129
|
+
args: {
|
|
130
|
+
available: {
|
|
131
|
+
type: 'boolean',
|
|
132
|
+
description: 'list every <provider>/<model> ref typeclaw recognizes (not just configured profiles)',
|
|
133
|
+
required: false,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
async run({ args }) {
|
|
137
|
+
if (args.available === true) {
|
|
138
|
+
printAvailableRefs()
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
const cwd = ensureAgentDir()
|
|
142
|
+
const entries = listModelProfiles(cwd)
|
|
143
|
+
if (entries.length === 0) {
|
|
144
|
+
console.log(c.dim('No models configured.'))
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const profileWidth = Math.max(7, ...entries.map((e) => e.profile.length))
|
|
149
|
+
const refWidth = Math.max(3, ...entries.map((e) => e.ref.length))
|
|
150
|
+
|
|
151
|
+
const header = `${'PROFILE'.padEnd(profileWidth)} ${'REF'.padEnd(refWidth)} PROVIDER STATUS`
|
|
152
|
+
console.log(c.dim(header))
|
|
153
|
+
for (const e of entries) {
|
|
154
|
+
const star = e.isDefault ? c.cyan('*') : ' '
|
|
155
|
+
const status = e.credentialStatus === 'available' ? c.green('ok') : c.yellow('missing-credentials')
|
|
156
|
+
const line = `${star}${e.profile.padEnd(profileWidth - 1)} ${e.ref.padEnd(refWidth)} ${e.providerId.padEnd(12)} ${status}`
|
|
157
|
+
console.log(line)
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
export const modelCommand = defineCommand({
|
|
163
|
+
meta: {
|
|
164
|
+
name: 'model',
|
|
165
|
+
description: 'manage model profiles in typeclaw.json (models.default, models.fast, …)',
|
|
166
|
+
},
|
|
167
|
+
subCommands: {
|
|
168
|
+
set: setSub,
|
|
169
|
+
add: addSub,
|
|
170
|
+
remove: removeSub,
|
|
171
|
+
list: listSub,
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
function ensureAgentDir(): string {
|
|
176
|
+
const cwd = findAgentDir(process.cwd()) ?? process.cwd()
|
|
177
|
+
if (!isInitialized(cwd)) {
|
|
178
|
+
console.error(errorLine('TypeClaw config file not found. Run `typeclaw init` first, or cd into an agent folder.'))
|
|
179
|
+
process.exit(1)
|
|
180
|
+
}
|
|
181
|
+
return cwd
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function pickProfileName(): Promise<string> {
|
|
185
|
+
const choice = await select<string>({
|
|
186
|
+
message: 'Pick a profile to set',
|
|
187
|
+
options: [
|
|
188
|
+
{ value: 'default', label: 'default', hint: 'active model for new sessions' },
|
|
189
|
+
{ value: 'fast', label: 'fast', hint: 'optional alias used by some subagents' },
|
|
190
|
+
{ value: 'vision', label: 'vision', hint: 'optional alias used by some subagents' },
|
|
191
|
+
],
|
|
192
|
+
initialValue: 'default',
|
|
193
|
+
})
|
|
194
|
+
if (isCancel(choice)) {
|
|
195
|
+
cancel('Aborted.')
|
|
196
|
+
process.exit(0)
|
|
197
|
+
}
|
|
198
|
+
return choice
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function pickModelRef(): Promise<string> {
|
|
202
|
+
const refs = listKnownModelRefs()
|
|
203
|
+
const choice = await select<KnownModelRef>({
|
|
204
|
+
message: 'Pick a model',
|
|
205
|
+
options: refs.map((ref) => ({
|
|
206
|
+
value: ref,
|
|
207
|
+
label: describeRef(ref),
|
|
208
|
+
hint: ref,
|
|
209
|
+
})),
|
|
210
|
+
initialValue: refs[0],
|
|
211
|
+
})
|
|
212
|
+
if (isCancel(choice)) {
|
|
213
|
+
cancel('Aborted.')
|
|
214
|
+
process.exit(0)
|
|
215
|
+
}
|
|
216
|
+
return choice
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function describeRef(ref: KnownModelRef): string {
|
|
220
|
+
const providerId = providerForModelRef(ref)
|
|
221
|
+
const modelId = ref.slice(providerId.length + 1)
|
|
222
|
+
const provider = KNOWN_PROVIDERS[providerId]
|
|
223
|
+
const model = (provider.models as Record<string, { name: string }>)[modelId]
|
|
224
|
+
return model ? `${provider.name} · ${model.name}` : ref
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function printAvailableRefs(): void {
|
|
228
|
+
const refs = listKnownModelRefs()
|
|
229
|
+
if (refs.length === 0) {
|
|
230
|
+
console.log(c.dim('No models registered.'))
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
console.log(c.dim('Use `typeclaw model set <profile> <ref>` to apply.'))
|
|
234
|
+
let lastProvider: KnownProviderId | null = null
|
|
235
|
+
for (const ref of refs) {
|
|
236
|
+
const providerId = providerForModelRef(ref)
|
|
237
|
+
if (providerId !== lastProvider) {
|
|
238
|
+
console.log('')
|
|
239
|
+
console.log(c.cyan(KNOWN_PROVIDERS[providerId].name))
|
|
240
|
+
lastProvider = providerId
|
|
241
|
+
}
|
|
242
|
+
console.log(` ${ref}`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { cancel, intro, isCancel, log, note, password, select, text } from '@clack/prompts'
|
|
2
|
+
import { defineCommand } from 'citty'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
KNOWN_PROVIDERS,
|
|
6
|
+
supportsApiKey as providerSupportsApiKey,
|
|
7
|
+
supportsOAuth as providerSupportsOAuth,
|
|
8
|
+
type KnownProviderId,
|
|
9
|
+
} from '@/config/providers'
|
|
10
|
+
import {
|
|
11
|
+
addProvider,
|
|
12
|
+
listConfiguredProviders,
|
|
13
|
+
removeProvider,
|
|
14
|
+
setProvider,
|
|
15
|
+
type CredentialSource,
|
|
16
|
+
} from '@/config/providers-mutation'
|
|
17
|
+
import { findAgentDir, isInitialized } from '@/init'
|
|
18
|
+
import { makeOAuthLoginRunner } from '@/init/oauth-login'
|
|
19
|
+
|
|
20
|
+
import { c, done, errorLine } from './ui'
|
|
21
|
+
|
|
22
|
+
const addSub = defineCommand({
|
|
23
|
+
meta: {
|
|
24
|
+
name: 'add',
|
|
25
|
+
description: 'add LLM provider credentials (api key or OAuth) to an existing agent',
|
|
26
|
+
},
|
|
27
|
+
args: {
|
|
28
|
+
provider: {
|
|
29
|
+
type: 'positional',
|
|
30
|
+
description: `provider id (${Object.keys(KNOWN_PROVIDERS).join(' | ')}); omit to pick interactively`,
|
|
31
|
+
required: false,
|
|
32
|
+
},
|
|
33
|
+
key: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'api key value (non-interactive); incompatible with --oauth',
|
|
36
|
+
required: false,
|
|
37
|
+
},
|
|
38
|
+
env: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'bind to a custom env-var name (writes { env: NAME } into secrets.json)',
|
|
41
|
+
required: false,
|
|
42
|
+
},
|
|
43
|
+
oauth: {
|
|
44
|
+
type: 'boolean',
|
|
45
|
+
description: 'force OAuth flow (browser login) for providers that support both methods',
|
|
46
|
+
required: false,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
async run({ args }) {
|
|
50
|
+
const cwd = ensureAgentDir()
|
|
51
|
+
const providerId = await resolveProviderForAdd(args.provider)
|
|
52
|
+
const provider = KNOWN_PROVIDERS[providerId]
|
|
53
|
+
|
|
54
|
+
intro(`Adding provider: ${provider.name}`)
|
|
55
|
+
|
|
56
|
+
const method = await resolveAuthMethod(provider, args)
|
|
57
|
+
if (method === 'oauth') {
|
|
58
|
+
const result = await runOAuthLogin(cwd, providerId)
|
|
59
|
+
if (!result.ok) {
|
|
60
|
+
console.error(errorLine(`OAuth login failed: ${result.reason}`))
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
done({
|
|
64
|
+
title: c.green(`Logged in to ${provider.name}.`),
|
|
65
|
+
hints: nextStepHints({ credentialChanged: true }),
|
|
66
|
+
})
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const credential = await resolveApiKeyInputs(provider, args)
|
|
71
|
+
const result = addProvider(cwd, providerId, credential)
|
|
72
|
+
if (!result.ok) {
|
|
73
|
+
console.error(errorLine(result.reason))
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
done({
|
|
77
|
+
title: c.green(`Added ${provider.name} credentials to secrets.json.`),
|
|
78
|
+
hints: nextStepHints({ credentialChanged: true }),
|
|
79
|
+
})
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const setSub = defineCommand({
|
|
84
|
+
meta: {
|
|
85
|
+
name: 'set',
|
|
86
|
+
description: 'rotate/update credentials for an already-configured provider',
|
|
87
|
+
},
|
|
88
|
+
args: {
|
|
89
|
+
provider: {
|
|
90
|
+
type: 'positional',
|
|
91
|
+
description: `provider id (${Object.keys(KNOWN_PROVIDERS).join(' | ')})`,
|
|
92
|
+
required: true,
|
|
93
|
+
},
|
|
94
|
+
key: {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: 'new api key value (non-interactive); incompatible with --oauth',
|
|
97
|
+
required: false,
|
|
98
|
+
},
|
|
99
|
+
env: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: 'bind to a custom env-var name (writes { env: NAME } into secrets.json)',
|
|
102
|
+
required: false,
|
|
103
|
+
},
|
|
104
|
+
oauth: {
|
|
105
|
+
type: 'boolean',
|
|
106
|
+
description: 're-run OAuth flow (browser login)',
|
|
107
|
+
required: false,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
async run({ args }) {
|
|
111
|
+
const cwd = ensureAgentDir()
|
|
112
|
+
const providerId = validateKnownProvider(args.provider)
|
|
113
|
+
const provider = KNOWN_PROVIDERS[providerId]
|
|
114
|
+
|
|
115
|
+
intro(`Updating provider: ${provider.name}`)
|
|
116
|
+
|
|
117
|
+
const method = await resolveAuthMethod(provider, args)
|
|
118
|
+
if (method === 'oauth') {
|
|
119
|
+
const result = await runOAuthLogin(cwd, providerId)
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
console.error(errorLine(`OAuth login failed: ${result.reason}`))
|
|
122
|
+
process.exit(1)
|
|
123
|
+
}
|
|
124
|
+
done({
|
|
125
|
+
title: c.green(`Refreshed OAuth credentials for ${provider.name}.`),
|
|
126
|
+
hints: nextStepHints({ credentialChanged: true }),
|
|
127
|
+
})
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const credential = await resolveApiKeyInputs(provider, args)
|
|
132
|
+
const result = setProvider(cwd, providerId, credential)
|
|
133
|
+
if (!result.ok) {
|
|
134
|
+
console.error(errorLine(result.reason))
|
|
135
|
+
process.exit(1)
|
|
136
|
+
}
|
|
137
|
+
done({
|
|
138
|
+
title: c.green(`Updated ${provider.name} credentials in secrets.json.`),
|
|
139
|
+
hints: nextStepHints({ credentialChanged: true }),
|
|
140
|
+
})
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const removeSub = defineCommand({
|
|
145
|
+
meta: {
|
|
146
|
+
name: 'remove',
|
|
147
|
+
description: 'remove a provider entry from secrets.json (refuses when a model profile references it)',
|
|
148
|
+
},
|
|
149
|
+
args: {
|
|
150
|
+
provider: {
|
|
151
|
+
type: 'positional',
|
|
152
|
+
description: 'provider id to remove',
|
|
153
|
+
required: true,
|
|
154
|
+
},
|
|
155
|
+
force: {
|
|
156
|
+
type: 'boolean',
|
|
157
|
+
description: 'remove even when a model profile references this provider',
|
|
158
|
+
required: false,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
async run({ args }) {
|
|
162
|
+
const cwd = ensureAgentDir()
|
|
163
|
+
const providerId = args.provider
|
|
164
|
+
const provider = providerId in KNOWN_PROVIDERS ? KNOWN_PROVIDERS[providerId as KnownProviderId] : null
|
|
165
|
+
const label = provider?.name ?? providerId
|
|
166
|
+
|
|
167
|
+
intro(`Removing provider: ${label}`)
|
|
168
|
+
|
|
169
|
+
const result = removeProvider(cwd, providerId, { force: args.force === true })
|
|
170
|
+
if (!result.ok) {
|
|
171
|
+
const list = result.profiles.join(', ')
|
|
172
|
+
console.error(
|
|
173
|
+
errorLine(
|
|
174
|
+
`Cannot remove "${providerId}": referenced by model profile(s) [${list}]. Update those profiles first, or rerun with --force.`,
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
process.exit(1)
|
|
178
|
+
}
|
|
179
|
+
if (!result.existed) {
|
|
180
|
+
log.info(`No "${providerId}" entry in secrets.json — nothing to remove.`)
|
|
181
|
+
}
|
|
182
|
+
done({
|
|
183
|
+
title: c.green(`Removed ${label} from secrets.json.`),
|
|
184
|
+
hints: nextStepHints({ credentialChanged: true }),
|
|
185
|
+
})
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const listSub = defineCommand({
|
|
190
|
+
meta: {
|
|
191
|
+
name: 'list',
|
|
192
|
+
description: 'show configured providers and how each credential resolves (file / env / oauth)',
|
|
193
|
+
},
|
|
194
|
+
async run() {
|
|
195
|
+
const cwd = ensureAgentDir()
|
|
196
|
+
const entries = listConfiguredProviders(cwd)
|
|
197
|
+
if (entries.length === 0) {
|
|
198
|
+
console.log(c.dim('No providers configured. Run `typeclaw provider add <id>` to wire one up.'))
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const rows = entries.map((entry) => {
|
|
203
|
+
const refs =
|
|
204
|
+
entry.referencedByProfiles.length === 0 ? c.dim('(no profile)') : entry.referencedByProfiles.join(',')
|
|
205
|
+
const name = entry.id in KNOWN_PROVIDERS ? KNOWN_PROVIDERS[entry.id as KnownProviderId].name : entry.id
|
|
206
|
+
return {
|
|
207
|
+
id: entry.id,
|
|
208
|
+
name,
|
|
209
|
+
type: entry.type,
|
|
210
|
+
source: describeSource(entry.source),
|
|
211
|
+
refs,
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const idWidth = Math.max(2, ...rows.map((r) => r.id.length))
|
|
216
|
+
const typeWidth = Math.max(4, ...rows.map((r) => r.type.length))
|
|
217
|
+
const sourceWidth = Math.max(6, ...rows.map((r) => r.source.length))
|
|
218
|
+
|
|
219
|
+
const header = `${'ID'.padEnd(idWidth)} ${'TYPE'.padEnd(typeWidth)} ${'SOURCE'.padEnd(sourceWidth)} PROFILES`
|
|
220
|
+
console.log(c.dim(header))
|
|
221
|
+
for (const r of rows) {
|
|
222
|
+
const line = `${r.id.padEnd(idWidth)} ${r.type.padEnd(typeWidth)} ${r.source.padEnd(sourceWidth)} ${r.refs} ${c.dim(`(${r.name})`)}`
|
|
223
|
+
console.log(line)
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
export const providerCommand = defineCommand({
|
|
229
|
+
meta: {
|
|
230
|
+
name: 'provider',
|
|
231
|
+
description: 'manage LLM provider credentials in secrets.json',
|
|
232
|
+
},
|
|
233
|
+
subCommands: {
|
|
234
|
+
add: addSub,
|
|
235
|
+
set: setSub,
|
|
236
|
+
remove: removeSub,
|
|
237
|
+
list: listSub,
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
function ensureAgentDir(): string {
|
|
242
|
+
const cwd = findAgentDir(process.cwd()) ?? process.cwd()
|
|
243
|
+
if (!isInitialized(cwd)) {
|
|
244
|
+
console.error(errorLine('TypeClaw config file not found. Run `typeclaw init` first, or cd into an agent folder.'))
|
|
245
|
+
process.exit(1)
|
|
246
|
+
}
|
|
247
|
+
return cwd
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function validateKnownProvider(input: string): KnownProviderId {
|
|
251
|
+
if (input in KNOWN_PROVIDERS) return input as KnownProviderId
|
|
252
|
+
console.error(errorLine(`Unknown provider "${input}". Available: ${Object.keys(KNOWN_PROVIDERS).join(', ')}.`))
|
|
253
|
+
process.exit(1)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function resolveProviderForAdd(input: string | undefined): Promise<KnownProviderId> {
|
|
257
|
+
if (input !== undefined) return validateKnownProvider(input)
|
|
258
|
+
const ids = Object.keys(KNOWN_PROVIDERS) as KnownProviderId[]
|
|
259
|
+
const choice = await select<KnownProviderId>({
|
|
260
|
+
message: 'Pick a provider to add',
|
|
261
|
+
options: ids.map((id) => ({
|
|
262
|
+
value: id,
|
|
263
|
+
label: KNOWN_PROVIDERS[id].name,
|
|
264
|
+
hint: authHint(id),
|
|
265
|
+
})),
|
|
266
|
+
initialValue: ids[0],
|
|
267
|
+
})
|
|
268
|
+
if (isCancel(choice)) {
|
|
269
|
+
cancel('Aborted.')
|
|
270
|
+
process.exit(0)
|
|
271
|
+
}
|
|
272
|
+
return choice
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
type AuthArgs = { oauth?: boolean | undefined; key?: string | undefined; env?: string | undefined }
|
|
276
|
+
|
|
277
|
+
async function resolveAuthMethod(
|
|
278
|
+
provider: (typeof KNOWN_PROVIDERS)[KnownProviderId],
|
|
279
|
+
args: AuthArgs,
|
|
280
|
+
): Promise<'api-key' | 'oauth'> {
|
|
281
|
+
const apiKeyOk = providerSupportsApiKey(provider)
|
|
282
|
+
const oauthOk = providerSupportsOAuth(provider)
|
|
283
|
+
if (args.oauth === true) {
|
|
284
|
+
if (!oauthOk) {
|
|
285
|
+
console.error(errorLine(`Provider ${provider.name} does not support OAuth.`))
|
|
286
|
+
process.exit(1)
|
|
287
|
+
}
|
|
288
|
+
return 'oauth'
|
|
289
|
+
}
|
|
290
|
+
if (args.key !== undefined || args.env !== undefined) {
|
|
291
|
+
if (!apiKeyOk) {
|
|
292
|
+
console.error(errorLine(`Provider ${provider.name} does not support api-key auth. Re-run with --oauth instead.`))
|
|
293
|
+
process.exit(1)
|
|
294
|
+
}
|
|
295
|
+
return 'api-key'
|
|
296
|
+
}
|
|
297
|
+
if (apiKeyOk && oauthOk) {
|
|
298
|
+
const choice = await select<'api-key' | 'oauth'>({
|
|
299
|
+
message: `How do you want to authenticate to ${provider.name}?`,
|
|
300
|
+
options: [
|
|
301
|
+
{ value: 'api-key', label: 'API key', hint: 'saved to secrets.json' },
|
|
302
|
+
{ value: 'oauth', label: 'OAuth (browser login)', hint: 'saved to secrets.json' },
|
|
303
|
+
],
|
|
304
|
+
initialValue: 'api-key',
|
|
305
|
+
})
|
|
306
|
+
if (isCancel(choice)) {
|
|
307
|
+
cancel('Aborted.')
|
|
308
|
+
process.exit(0)
|
|
309
|
+
}
|
|
310
|
+
return choice
|
|
311
|
+
}
|
|
312
|
+
return oauthOk ? 'oauth' : 'api-key'
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function resolveApiKeyInputs(
|
|
316
|
+
provider: (typeof KNOWN_PROVIDERS)[KnownProviderId],
|
|
317
|
+
args: AuthArgs,
|
|
318
|
+
): Promise<
|
|
319
|
+
{ type: 'api_key'; key: string; envBinding?: string | undefined } | { type: 'env-binding'; envBinding: string }
|
|
320
|
+
> {
|
|
321
|
+
if (args.env !== undefined && args.key === undefined) {
|
|
322
|
+
return { type: 'env-binding', envBinding: args.env }
|
|
323
|
+
}
|
|
324
|
+
if (args.key !== undefined) {
|
|
325
|
+
const result: { type: 'api_key'; key: string; envBinding?: string } = { type: 'api_key', key: args.key }
|
|
326
|
+
if (args.env !== undefined) result.envBinding = args.env
|
|
327
|
+
return result
|
|
328
|
+
}
|
|
329
|
+
return { type: 'api_key', key: await promptApiKey(provider) }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function promptApiKey(provider: (typeof KNOWN_PROVIDERS)[KnownProviderId]): Promise<string> {
|
|
333
|
+
const value = await password({
|
|
334
|
+
message: `Put your ${provider.name} API key (will be saved to secrets.json)`,
|
|
335
|
+
validate: (v) => (v && v.length > 0 ? undefined : 'API key is required'),
|
|
336
|
+
})
|
|
337
|
+
if (isCancel(value)) {
|
|
338
|
+
cancel('Aborted.')
|
|
339
|
+
process.exit(0)
|
|
340
|
+
}
|
|
341
|
+
return value
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function runOAuthLogin(cwd: string, providerId: KnownProviderId): Promise<{ ok: boolean; reason?: string }> {
|
|
345
|
+
const provider = KNOWN_PROVIDERS[providerId]
|
|
346
|
+
// Pick any model ref for the provider; OAuth login only uses the ref to
|
|
347
|
+
// discover the provider's `oauthProviderId`, which is the same regardless
|
|
348
|
+
// of which model the user later selects via `typeclaw model set`.
|
|
349
|
+
const ref = Object.keys(provider.models)[0]
|
|
350
|
+
if (ref === undefined) {
|
|
351
|
+
return { ok: false, reason: `Provider ${provider.name} has no registered models.` }
|
|
352
|
+
}
|
|
353
|
+
const modelRef = `${providerId}/${ref}` as const
|
|
354
|
+
|
|
355
|
+
const callbacks = {
|
|
356
|
+
onAuth: (url: string, instructions?: string) => {
|
|
357
|
+
const preamble = [`Open this URL in your browser to authorize ${provider.name}.`]
|
|
358
|
+
if (instructions) preamble.push('', instructions)
|
|
359
|
+
note(preamble.join('\n'), 'Browser login')
|
|
360
|
+
console.log(url)
|
|
361
|
+
console.log('')
|
|
362
|
+
},
|
|
363
|
+
onProgress: (message: string) => {
|
|
364
|
+
log.info(message)
|
|
365
|
+
},
|
|
366
|
+
onPrompt: async (message: string, placeholder?: string): Promise<string | null> => {
|
|
367
|
+
const value = await text({ message, ...(placeholder !== undefined ? { placeholder } : {}) })
|
|
368
|
+
if (isCancel(value)) return null
|
|
369
|
+
return value
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const runner = makeOAuthLoginRunner(callbacks)
|
|
374
|
+
const result = await runner({ cwd, model: modelRef as Parameters<typeof runner>[0]['model'] })
|
|
375
|
+
if (!result.ok) return { ok: false, reason: result.reason }
|
|
376
|
+
return { ok: true }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function authHint(id: KnownProviderId): string {
|
|
380
|
+
const provider = KNOWN_PROVIDERS[id]
|
|
381
|
+
const apiKey = providerSupportsApiKey(provider)
|
|
382
|
+
const oauth = providerSupportsOAuth(provider)
|
|
383
|
+
if (apiKey && oauth) return 'API key or OAuth'
|
|
384
|
+
if (oauth) return 'OAuth only'
|
|
385
|
+
return 'API key'
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function describeSource(source: CredentialSource): string {
|
|
389
|
+
switch (source.kind) {
|
|
390
|
+
case 'file':
|
|
391
|
+
return 'file'
|
|
392
|
+
case 'env-only':
|
|
393
|
+
return `env (${source.envName})`
|
|
394
|
+
case 'env-overridden':
|
|
395
|
+
return `env (${source.envName}, overrides file)`
|
|
396
|
+
case 'oauth':
|
|
397
|
+
return 'oauth'
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function nextStepHints(opts: { credentialChanged: boolean }): { label: string; command: string }[] {
|
|
402
|
+
if (!opts.credentialChanged) return []
|
|
403
|
+
return [{ label: 'If the agent is running:', command: 'typeclaw reload' }]
|
|
404
|
+
}
|
package/src/cli/reload.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineCommand } from 'citty'
|
|
2
2
|
|
|
3
|
-
import { resolveHostPort, resolveTuiToken } from '@/container'
|
|
3
|
+
import { requireContainerRunning, resolveHostPort, resolveTuiToken } from '@/container'
|
|
4
4
|
import { findAgentDir } from '@/init'
|
|
5
5
|
import { requestReload, type ReloadResult } from '@/reload'
|
|
6
6
|
|
|
@@ -63,6 +63,11 @@ export const reload = defineCommand({
|
|
|
63
63
|
|
|
64
64
|
async function defaultUrl(): Promise<string> {
|
|
65
65
|
const cwd = findAgentDir(process.cwd()) ?? process.cwd()
|
|
66
|
+
const precheck = await requireContainerRunning({ cwd })
|
|
67
|
+
if (!precheck.ok) {
|
|
68
|
+
console.error(errorLine(precheck.reason))
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
66
71
|
const port = await resolveHostPort({ cwd })
|
|
67
72
|
const token = await resolveTuiToken({ cwd })
|
|
68
73
|
const url = new URL(`ws://127.0.0.1:${port}`)
|