typeclaw 0.3.1 → 0.5.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 +20 -15
- package/auth.schema.json +113 -0
- package/package.json +1 -1
- package/secrets.schema.json +113 -0
- package/src/agent/auth.ts +4 -2
- package/src/agent/index.ts +16 -28
- package/src/agent/model-fallback.ts +127 -0
- package/src/agent/session-meta.ts +1 -1
- package/src/agent/session-origin.ts +3 -2
- package/src/agent/tools/curl-impersonate.ts +300 -0
- package/src/agent/tools/ddg.ts +13 -88
- package/src/agent/tools/webfetch/fetch.ts +105 -2
- package/src/agent/tools/webfetch/tool.ts +4 -0
- package/src/bundled-plugins/agent-browser/shim.ts +47 -0
- package/src/bundled-plugins/backup/subagents.ts +2 -0
- package/src/bundled-plugins/memory/README.md +49 -12
- package/src/bundled-plugins/memory/citation-superset.ts +63 -0
- package/src/bundled-plugins/memory/dreaming.ts +105 -17
- package/src/bundled-plugins/memory/index.ts +2 -2
- package/src/bundled-plugins/memory/memory-logger.ts +45 -26
- package/src/bundled-plugins/memory/strength.ts +127 -0
- package/src/bundled-plugins/memory/topics.ts +75 -0
- package/src/bundled-plugins/security/index.ts +88 -43
- package/src/bundled-plugins/security/permissions.ts +36 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +20 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +12 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +23 -3
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +7 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +6 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +9 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +6 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +7 -0
- package/src/channels/adapters/github/auth-app.ts +120 -0
- package/src/channels/adapters/github/auth-pat.ts +50 -0
- package/src/channels/adapters/github/auth.ts +33 -0
- package/src/channels/adapters/github/channel-resolver.ts +30 -0
- package/src/channels/adapters/github/dedup.ts +26 -0
- package/src/channels/adapters/github/event-allowlist.ts +8 -0
- package/src/channels/adapters/github/fetch-attachment.ts +5 -0
- package/src/channels/adapters/github/history.ts +63 -0
- package/src/channels/adapters/github/inbound.ts +286 -0
- package/src/channels/adapters/github/index.ts +370 -0
- package/src/channels/adapters/github/managed-path.ts +54 -0
- package/src/channels/adapters/github/membership.ts +35 -0
- package/src/channels/adapters/github/outbound.ts +145 -0
- package/src/channels/adapters/github/webhook-register.ts +349 -0
- package/src/channels/manager.ts +94 -9
- package/src/channels/router.ts +194 -28
- package/src/channels/schema.ts +31 -1
- package/src/channels/tunnel-bridge.ts +51 -0
- package/src/channels/types.ts +3 -1
- package/src/cli/builtins.ts +28 -0
- package/src/cli/channel.ts +511 -25
- package/src/cli/container-command-client.ts +244 -0
- package/src/cli/cron.ts +173 -0
- package/src/cli/host-command-runner.ts +150 -0
- package/src/cli/index.ts +42 -1
- package/src/cli/init.ts +400 -67
- package/src/cli/model.ts +14 -4
- package/src/cli/oauth-callbacks.ts +49 -0
- package/src/cli/plugin-command-help.ts +49 -0
- package/src/cli/plugin-commands-dispatch.ts +112 -0
- package/src/cli/plugin-commands.ts +118 -0
- package/src/cli/provider.ts +3 -20
- package/src/cli/tui.ts +10 -2
- package/src/cli/tunnel.ts +533 -0
- package/src/cli/ui.ts +8 -3
- package/src/config/config.ts +134 -24
- package/src/config/models-mutation.ts +42 -8
- package/src/config/providers-mutation.ts +12 -8
- package/src/container/start.ts +48 -4
- package/src/cron/bridge.ts +136 -0
- package/src/cron/consumer.ts +174 -48
- package/src/cron/index.ts +19 -2
- package/src/cron/list.ts +105 -0
- package/src/cron/scheduler.ts +12 -3
- package/src/cron/schema.ts +11 -3
- package/src/doctor/checks.ts +0 -50
- package/src/init/dockerfile.ts +165 -13
- package/src/init/ensure-deps.ts +15 -4
- package/src/init/github-webhook-install.ts +109 -0
- package/src/init/hatching.ts +2 -2
- package/src/init/index.ts +519 -12
- package/src/init/oauth-login.ts +17 -3
- package/src/init/run-bun-install.ts +17 -3
- package/src/init/run-owner-claim.ts +11 -2
- package/src/permissions/builtins.ts +29 -2
- package/src/permissions/match-rule.ts +24 -2
- package/src/permissions/permissions.ts +24 -7
- package/src/permissions/resolve.ts +1 -0
- package/src/plugin/define.ts +44 -1
- package/src/plugin/index.ts +18 -3
- package/src/plugin/manager.ts +16 -0
- package/src/plugin/registry.ts +85 -3
- package/src/plugin/types.ts +144 -1
- package/src/plugin/zod-introspect.ts +100 -0
- package/src/role-claim/match-rule.ts +2 -1
- package/src/run/index.ts +112 -4
- package/src/secrets/index.ts +1 -1
- package/src/secrets/schema.ts +21 -0
- package/src/server/command-runner.ts +476 -0
- package/src/server/index.ts +388 -5
- package/src/shared/index.ts +8 -0
- package/src/shared/protocol.ts +80 -1
- package/src/skills/typeclaw-channel-github/SKILL.md +24 -0
- package/src/skills/typeclaw-config/SKILL.md +27 -26
- package/src/skills/typeclaw-cron/SKILL.md +234 -3
- package/src/skills/typeclaw-memory/SKILL.md +25 -15
- package/src/skills/typeclaw-monorepo/SKILL.md +2 -2
- package/src/skills/typeclaw-permissions/SKILL.md +35 -16
- package/src/skills/typeclaw-plugins/SKILL.md +251 -5
- package/src/skills/typeclaw-tunnels/SKILL.md +111 -0
- package/src/test-helpers/wait-for.ts +50 -0
- package/src/tui/index.ts +70 -7
- package/src/tunnels/__fixtures__/cloudflared-quick-stderr.txt +11 -0
- package/src/tunnels/events.ts +14 -0
- package/src/tunnels/index.ts +12 -0
- package/src/tunnels/log-ring.ts +54 -0
- package/src/tunnels/manager.ts +139 -0
- package/src/tunnels/providers/cloudflare-quick.ts +189 -0
- package/src/tunnels/providers/external.ts +53 -0
- package/src/tunnels/quick-url-parser.ts +5 -0
- package/src/tunnels/types.ts +43 -0
- package/src/usage/report.ts +15 -12
- package/typeclaw.schema.json +311 -26
package/src/cli/model.ts
CHANGED
|
@@ -57,7 +57,8 @@ const setSub = defineCommand({
|
|
|
57
57
|
process.exit(1)
|
|
58
58
|
}
|
|
59
59
|
done({
|
|
60
|
-
title: c.green(`Profile "${profile}" set
|
|
60
|
+
title: c.green(`Profile "${profile}" set.`),
|
|
61
|
+
details: `${profile} → ${ref}`,
|
|
61
62
|
hints: [{ label: 'If the agent is running:', command: 'typeclaw reload' }],
|
|
62
63
|
})
|
|
63
64
|
},
|
|
@@ -97,7 +98,8 @@ const addSub = defineCommand({
|
|
|
97
98
|
process.exit(1)
|
|
98
99
|
}
|
|
99
100
|
done({
|
|
100
|
-
title: c.green(`Profile "${args.profile}"
|
|
101
|
+
title: c.green(`Profile "${args.profile}" added.`),
|
|
102
|
+
details: `${args.profile} → ${ref}`,
|
|
101
103
|
hints: [{ label: 'If the agent is running:', command: 'typeclaw reload' }],
|
|
102
104
|
})
|
|
103
105
|
},
|
|
@@ -155,15 +157,23 @@ const listSub = defineCommand({
|
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
const profileWidth = Math.max(7, ...entries.map((e) => e.profile.length))
|
|
158
|
-
const
|
|
160
|
+
const refDisplay = (e: (typeof entries)[number]): string =>
|
|
161
|
+
e.refs.length > 1 ? `${e.ref} ${c.dim(`(+${e.refs.length - 1} fallback)`)}` : e.ref
|
|
162
|
+
const refWidth = Math.max(3, ...entries.map((e) => e.ref.length + (e.refs.length > 1 ? 14 : 0)))
|
|
159
163
|
|
|
160
164
|
const header = `${'PROFILE'.padEnd(profileWidth)} ${'REF'.padEnd(refWidth)} PROVIDER STATUS`
|
|
161
165
|
console.log(c.dim(header))
|
|
162
166
|
for (const e of entries) {
|
|
163
167
|
const star = e.isDefault ? c.cyan('*') : ' '
|
|
164
168
|
const status = e.credentialStatus === 'available' ? c.green('ok') : c.yellow('missing-credentials')
|
|
165
|
-
const line = `${star}${e.profile.padEnd(profileWidth - 1)} ${e.
|
|
169
|
+
const line = `${star}${e.profile.padEnd(profileWidth - 1)} ${refDisplay(e).padEnd(refWidth)} ${e.providerId.padEnd(12)} ${status}`
|
|
166
170
|
console.log(line)
|
|
171
|
+
if (e.refs.length > 1) {
|
|
172
|
+
for (let i = 1; i < e.refs.length; i++) {
|
|
173
|
+
const fb = e.refs[i]!
|
|
174
|
+
console.log(`${' '.padEnd(profileWidth + 2)}↳ ${c.dim(fb)}`)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
167
177
|
}
|
|
168
178
|
},
|
|
169
179
|
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isCancel, log, note, text } from '@clack/prompts'
|
|
2
|
+
|
|
3
|
+
import type { OAuthCallbacks } from '@/init/oauth-login'
|
|
4
|
+
|
|
5
|
+
// Shared between `typeclaw init` (src/cli/init.ts) and `typeclaw provider
|
|
6
|
+
// add/set` (src/cli/provider.ts). Both call into the same OAuth runner, so
|
|
7
|
+
// they need to render the same UX: a note() box with the URL + cross-device
|
|
8
|
+
// guidance, a `text()` prompt for the post-callback manual fallback, and a
|
|
9
|
+
// concurrent `onManualCodeInput` prompt for users whose browser is on a
|
|
10
|
+
// different host than the CLI. See src/init/oauth-login.ts for the contract
|
|
11
|
+
// on each callback and why onManualCodeInput is required for cross-device.
|
|
12
|
+
export function buildOAuthCallbacks(providerName: string): OAuthCallbacks {
|
|
13
|
+
return {
|
|
14
|
+
onAuth: (url, instructions) => {
|
|
15
|
+
// Don't put the URL inside note(): clack wraps long lines with the box
|
|
16
|
+
// border `│` on each wrapped segment, which corrupts the URL when the
|
|
17
|
+
// user copy-pastes it. Keep instructional text in the box, but print
|
|
18
|
+
// the URL itself as a bare console.log line that any terminal will
|
|
19
|
+
// hyperlink intact.
|
|
20
|
+
const preamble = [
|
|
21
|
+
`Open this URL in your browser to sign in to ${providerName}.`,
|
|
22
|
+
'',
|
|
23
|
+
'If your browser shows "this site can\'t be reached" after you sign in,',
|
|
24
|
+
'copy the full address from the top of the browser and paste it below.',
|
|
25
|
+
]
|
|
26
|
+
if (instructions) preamble.push('', instructions)
|
|
27
|
+
note(preamble.join('\n'), 'Browser login')
|
|
28
|
+
console.log(url)
|
|
29
|
+
console.log('')
|
|
30
|
+
},
|
|
31
|
+
onProgress: (message) => {
|
|
32
|
+
log.info(message)
|
|
33
|
+
},
|
|
34
|
+
onPrompt: async (message, placeholder) => {
|
|
35
|
+
const value = await text({ message, ...(placeholder !== undefined ? { placeholder } : {}) })
|
|
36
|
+
if (isCancel(value)) return null
|
|
37
|
+
return value
|
|
38
|
+
},
|
|
39
|
+
onManualCodeInput: async () => {
|
|
40
|
+
const value = await text({
|
|
41
|
+
message:
|
|
42
|
+
'If your browser shows "this site can\'t be reached" after you sign in, copy the full address from the top of the browser and paste it here:',
|
|
43
|
+
placeholder: 'http://localhost:1455/auth/callback?code=...&state=...',
|
|
44
|
+
})
|
|
45
|
+
if (isCancel(value)) throw new Error('Login cancelled by user')
|
|
46
|
+
return value
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { describeLeaf } from '@/plugin/zod-introspect'
|
|
4
|
+
|
|
5
|
+
import type { DiscoveredCommand } from './plugin-commands'
|
|
6
|
+
|
|
7
|
+
export function renderPluginCommandsSection(commands: readonly DiscoveredCommand[]): string | null {
|
|
8
|
+
if (commands.length === 0) return null
|
|
9
|
+
const lines: string[] = ['Plugin commands:']
|
|
10
|
+
const namePad = Math.max(...commands.map((c) => c.commandName.length))
|
|
11
|
+
for (const c of commands) {
|
|
12
|
+
lines.push(` ${c.commandName.padEnd(namePad)} ${c.command.description}`)
|
|
13
|
+
}
|
|
14
|
+
return lines.join('\n')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderCommandHelp(c: DiscoveredCommand): string {
|
|
18
|
+
const lines: string[] = []
|
|
19
|
+
lines.push(`typeclaw ${c.commandName} — ${c.command.description}`)
|
|
20
|
+
lines.push('')
|
|
21
|
+
lines.push(` Plugin: ${c.pluginName}${c.pluginVersion !== undefined ? ` v${c.pluginVersion}` : ''}`)
|
|
22
|
+
lines.push(` Surface: ${c.command.surface}`)
|
|
23
|
+
lines.push('')
|
|
24
|
+
|
|
25
|
+
if (c.command.args === undefined) {
|
|
26
|
+
lines.push(' Options:')
|
|
27
|
+
lines.push(' (no options)')
|
|
28
|
+
return lines.join('\n')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
lines.push(' Options:')
|
|
32
|
+
for (const line of renderFlags(c.command.args)) {
|
|
33
|
+
lines.push(` ${line}`)
|
|
34
|
+
}
|
|
35
|
+
return lines.join('\n')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderFlags(schema: z.ZodObject<z.ZodRawShape>): string[] {
|
|
39
|
+
const out: string[] = []
|
|
40
|
+
const shape = schema.shape as Record<string, unknown>
|
|
41
|
+
for (const [field, leaf] of Object.entries(shape)) {
|
|
42
|
+
const info = describeLeaf(leaf)
|
|
43
|
+
const required = info.required ? ' (required)' : ''
|
|
44
|
+
const defaultPart = info.defaultValue !== undefined ? ` (default: ${info.defaultValue})` : ''
|
|
45
|
+
const descPart = info.description !== undefined ? ` ${info.description}` : ''
|
|
46
|
+
out.push(`--${field}=<${info.kind}>${descPart}${required}${defaultPart}`)
|
|
47
|
+
}
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { proxyContainerCommand } from './container-command-client'
|
|
2
|
+
import { parseArgs, runHostCommand } from './host-command-runner'
|
|
3
|
+
import { renderCommandHelp } from './plugin-command-help'
|
|
4
|
+
import { discoverCommands } from './plugin-commands'
|
|
5
|
+
|
|
6
|
+
export type PluginCommandDispatchOutcome =
|
|
7
|
+
| { kind: 'not-found' }
|
|
8
|
+
| { kind: 'dispatched'; exitCode: number }
|
|
9
|
+
| { kind: 'error'; exitCode: number; message: string }
|
|
10
|
+
|
|
11
|
+
export type DispatchOptions = {
|
|
12
|
+
name: string
|
|
13
|
+
rawArgs: readonly string[]
|
|
14
|
+
cwd: string
|
|
15
|
+
stdin?: ReadableStream<Uint8Array>
|
|
16
|
+
stdout?: WritableStream<Uint8Array>
|
|
17
|
+
stderr?: WritableStream<Uint8Array>
|
|
18
|
+
signal?: AbortSignal
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function dispatchPluginCommand(opts: DispatchOptions): Promise<PluginCommandDispatchOutcome> {
|
|
22
|
+
const discovery = await discoverCommands({ cwd: opts.cwd })
|
|
23
|
+
const match = discovery.commands.find((c) => c.commandName === opts.name)
|
|
24
|
+
if (match === undefined) {
|
|
25
|
+
// Surface plugin load failures so a user typing `typeclaw <cmd>` sees why
|
|
26
|
+
// their plugin's command isn't listed, instead of a generic "not found".
|
|
27
|
+
if (discovery.loadErrors.length > 0) {
|
|
28
|
+
const stderr = opts.stderr ?? defaultStderr()
|
|
29
|
+
const writer = stderr.getWriter()
|
|
30
|
+
const encoder = new TextEncoder()
|
|
31
|
+
for (const e of discovery.loadErrors) {
|
|
32
|
+
await writer.write(encoder.encode(`[plugin-commands] ${e.entry}: ${e.error}\n`))
|
|
33
|
+
}
|
|
34
|
+
writer.releaseLock()
|
|
35
|
+
}
|
|
36
|
+
return { kind: 'not-found' }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const stdin = opts.stdin ?? defaultStdin()
|
|
40
|
+
const stdout = opts.stdout ?? defaultStdout()
|
|
41
|
+
const stderr = opts.stderr ?? defaultStderr()
|
|
42
|
+
const signal = opts.signal ?? new AbortController().signal
|
|
43
|
+
|
|
44
|
+
if (opts.rawArgs.includes('--help') || opts.rawArgs.includes('-h')) {
|
|
45
|
+
const help = renderCommandHelp(match)
|
|
46
|
+
const writer = stdout.getWriter()
|
|
47
|
+
await writer.write(new TextEncoder().encode(`${help}\n`))
|
|
48
|
+
writer.releaseLock()
|
|
49
|
+
return { kind: 'dispatched', exitCode: 0 }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (match.command.surface === 'container') {
|
|
53
|
+
const parsed = parseArgs(match.command, opts.rawArgs)
|
|
54
|
+
if (!parsed.ok) {
|
|
55
|
+
return { kind: 'error', exitCode: 2, message: parsed.message }
|
|
56
|
+
}
|
|
57
|
+
const containerResult = await proxyContainerCommand({
|
|
58
|
+
agentDir: discovery.agentDir,
|
|
59
|
+
commandName: match.commandName,
|
|
60
|
+
args: parsed.value,
|
|
61
|
+
stdin,
|
|
62
|
+
stdout,
|
|
63
|
+
stderr,
|
|
64
|
+
abortSignal: signal,
|
|
65
|
+
})
|
|
66
|
+
if (!containerResult.ok) {
|
|
67
|
+
return { kind: 'error', exitCode: containerResult.exitCode, message: containerResult.message }
|
|
68
|
+
}
|
|
69
|
+
return { kind: 'dispatched', exitCode: containerResult.exitCode }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const result = await runHostCommand({
|
|
73
|
+
agentDir: discovery.agentDir,
|
|
74
|
+
pluginName: match.pluginName,
|
|
75
|
+
pluginVersion: match.pluginVersion,
|
|
76
|
+
command: match.command,
|
|
77
|
+
rawArgs: opts.rawArgs,
|
|
78
|
+
signal,
|
|
79
|
+
stdin,
|
|
80
|
+
stdout,
|
|
81
|
+
stderr,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
return { kind: 'error', exitCode: result.exitCode, message: result.message }
|
|
86
|
+
}
|
|
87
|
+
return { kind: 'dispatched', exitCode: result.exitCode }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function defaultStdin(): ReadableStream<Uint8Array> {
|
|
91
|
+
return new ReadableStream<Uint8Array>({
|
|
92
|
+
start(controller) {
|
|
93
|
+
controller.close()
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function defaultStdout(): WritableStream<Uint8Array> {
|
|
99
|
+
return new WritableStream<Uint8Array>({
|
|
100
|
+
write(chunk) {
|
|
101
|
+
process.stdout.write(chunk)
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function defaultStderr(): WritableStream<Uint8Array> {
|
|
107
|
+
return new WritableStream<Uint8Array>({
|
|
108
|
+
write(chunk) {
|
|
109
|
+
process.stderr.write(chunk)
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { loadConfigSync } from '@/config/config'
|
|
5
|
+
import {
|
|
6
|
+
loadPluginEntry,
|
|
7
|
+
type LoadPluginEntryFn,
|
|
8
|
+
type PluginCommand,
|
|
9
|
+
type ResolvedPlugin,
|
|
10
|
+
validateCommandDeclaration,
|
|
11
|
+
} from '@/plugin'
|
|
12
|
+
|
|
13
|
+
export type DiscoveredCommand = {
|
|
14
|
+
pluginName: string
|
|
15
|
+
pluginVersion: string | undefined
|
|
16
|
+
commandName: string
|
|
17
|
+
command: PluginCommand
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type DiscoveryResult = {
|
|
21
|
+
agentDir: string
|
|
22
|
+
commands: DiscoveredCommand[]
|
|
23
|
+
loadErrors: { entry: string; error: string }[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type DiscoverOptions = {
|
|
27
|
+
cwd: string
|
|
28
|
+
loadEntry?: LoadPluginEntryFn
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Resolves the agent folder by walking up from cwd until typeclaw.json is
|
|
32
|
+
// found. Returns null when no agent folder is reachable (e.g. typeclaw run
|
|
33
|
+
// from a random shell prompt without an agent).
|
|
34
|
+
export function resolveAgentDir(cwd: string): string | null {
|
|
35
|
+
let cur = cwd
|
|
36
|
+
while (true) {
|
|
37
|
+
if (existsSync(join(cur, 'typeclaw.json'))) return cur
|
|
38
|
+
const parent = join(cur, '..')
|
|
39
|
+
const resolved = normalize(parent)
|
|
40
|
+
if (resolved === cur) return null
|
|
41
|
+
cur = resolved
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalize(p: string): string {
|
|
46
|
+
return p.replace(/\/+$/, '') || '/'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Discovers plugin commands available to the agent at `cwd`. Loads each
|
|
50
|
+
// plugin module to read its static `defined.commands`, but NEVER invokes
|
|
51
|
+
// the plugin factory — that's a runtime concern reserved for `typeclaw run`.
|
|
52
|
+
//
|
|
53
|
+
// Returns an empty result (no error) when no agent folder is resolvable, so
|
|
54
|
+
// `typeclaw --help` outside any agent prints just built-ins.
|
|
55
|
+
//
|
|
56
|
+
// Side effects: `loadConfigSync(agentDir)` may rewrite `typeclaw.json` and
|
|
57
|
+
// commit the result when the on-disk shape is a legacy schema needing
|
|
58
|
+
// migration. This is by design — running ANY typeclaw subcommand should
|
|
59
|
+
// converge the config on the canonical shape. The migration is idempotent
|
|
60
|
+
// (running twice is a no-op).
|
|
61
|
+
export async function discoverCommands(opts: DiscoverOptions): Promise<DiscoveryResult> {
|
|
62
|
+
const agentDir = resolveAgentDir(opts.cwd)
|
|
63
|
+
if (agentDir === null) {
|
|
64
|
+
return { agentDir: opts.cwd, commands: [], loadErrors: [] }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let config
|
|
68
|
+
try {
|
|
69
|
+
config = loadConfigSync(agentDir)
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
72
|
+
return { agentDir, commands: [], loadErrors: [{ entry: '<config>', error: detail }] }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const loadEntry = opts.loadEntry ?? loadPluginEntry
|
|
76
|
+
const commands: DiscoveredCommand[] = []
|
|
77
|
+
const loadErrors: { entry: string; error: string }[] = []
|
|
78
|
+
const seenNames = new Set<string>()
|
|
79
|
+
|
|
80
|
+
for (const entry of config.plugins) {
|
|
81
|
+
let resolved: ResolvedPlugin
|
|
82
|
+
try {
|
|
83
|
+
resolved = await loadEntry(entry, agentDir)
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
86
|
+
loadErrors.push({ entry, error: detail })
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const declared = resolved.defined.commands
|
|
91
|
+
if (declared === undefined) continue
|
|
92
|
+
|
|
93
|
+
for (const [commandName, command] of Object.entries(declared)) {
|
|
94
|
+
try {
|
|
95
|
+
validateCommandDeclaration(resolved.name, commandName, command)
|
|
96
|
+
} catch (err) {
|
|
97
|
+
loadErrors.push({ entry, error: err instanceof Error ? err.message : String(err) })
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
if (seenNames.has(commandName)) {
|
|
101
|
+
loadErrors.push({
|
|
102
|
+
entry,
|
|
103
|
+
error: `command "${commandName}" already declared by another plugin; ignoring`,
|
|
104
|
+
})
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
seenNames.add(commandName)
|
|
108
|
+
commands.push({
|
|
109
|
+
pluginName: resolved.name,
|
|
110
|
+
pluginVersion: resolved.version,
|
|
111
|
+
commandName,
|
|
112
|
+
command,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { agentDir, commands, loadErrors }
|
|
118
|
+
}
|
package/src/cli/provider.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cancel, intro, isCancel, log,
|
|
1
|
+
import { cancel, intro, isCancel, log, password, select } from '@clack/prompts'
|
|
2
2
|
import { defineCommand } from 'citty'
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { findAgentDir, isInitialized } from '@/init'
|
|
18
18
|
import { makeOAuthLoginRunner } from '@/init/oauth-login'
|
|
19
19
|
|
|
20
|
+
import { buildOAuthCallbacks } from './oauth-callbacks'
|
|
20
21
|
import { c, done, errorLine } from './ui'
|
|
21
22
|
|
|
22
23
|
const addSub = defineCommand({
|
|
@@ -366,25 +367,7 @@ async function runOAuthLogin(cwd: string, providerId: KnownProviderId): Promise<
|
|
|
366
367
|
}
|
|
367
368
|
const modelRef = `${providerId}/${ref}` as const
|
|
368
369
|
|
|
369
|
-
const
|
|
370
|
-
onAuth: (url: string, instructions?: string) => {
|
|
371
|
-
const preamble = [`Open this URL in your browser to authorize ${provider.name}.`]
|
|
372
|
-
if (instructions) preamble.push('', instructions)
|
|
373
|
-
note(preamble.join('\n'), 'Browser login')
|
|
374
|
-
console.log(url)
|
|
375
|
-
console.log('')
|
|
376
|
-
},
|
|
377
|
-
onProgress: (message: string) => {
|
|
378
|
-
log.info(message)
|
|
379
|
-
},
|
|
380
|
-
onPrompt: async (message: string, placeholder?: string): Promise<string | null> => {
|
|
381
|
-
const value = await text({ message, ...(placeholder !== undefined ? { placeholder } : {}) })
|
|
382
|
-
if (isCancel(value)) return null
|
|
383
|
-
return value
|
|
384
|
-
},
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const runner = makeOAuthLoginRunner(callbacks)
|
|
370
|
+
const runner = makeOAuthLoginRunner(buildOAuthCallbacks(provider.name))
|
|
388
371
|
const result = await runner({ cwd, model: modelRef as Parameters<typeof runner>[0]['model'] })
|
|
389
372
|
if (!result.ok) return { ok: false, reason: result.reason }
|
|
390
373
|
return { ok: true }
|
package/src/cli/tui.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { defineCommand } from 'citty'
|
|
|
2
2
|
|
|
3
3
|
import { requireContainerRunning, resolveHostPort, resolveTuiToken } from '@/container'
|
|
4
4
|
import { findAgentDir } from '@/init'
|
|
5
|
-
import {
|
|
5
|
+
import { CLI_VERSION } from '@/init/cli-version'
|
|
6
|
+
import { createTui, formatVersionMismatchWarning } from '@/tui'
|
|
6
7
|
|
|
7
8
|
import { errorLine } from './ui'
|
|
8
9
|
|
|
@@ -25,7 +26,14 @@ export const tui = defineCommand({
|
|
|
25
26
|
},
|
|
26
27
|
async run({ args }) {
|
|
27
28
|
const url = args.url ?? (await defaultUrl())
|
|
28
|
-
const tui = createTui({
|
|
29
|
+
const tui = createTui({
|
|
30
|
+
url,
|
|
31
|
+
...(args.prompt !== undefined ? { initialPrompt: args.prompt } : {}),
|
|
32
|
+
expectedVersion: CLI_VERSION,
|
|
33
|
+
onVersionMismatch: (info) => {
|
|
34
|
+
process.stderr.write(`${formatVersionMismatchWarning(info)}\n`)
|
|
35
|
+
},
|
|
36
|
+
})
|
|
29
37
|
await tui.run()
|
|
30
38
|
},
|
|
31
39
|
})
|