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.
Files changed (125) hide show
  1. package/README.md +20 -15
  2. package/auth.schema.json +113 -0
  3. package/package.json +1 -1
  4. package/secrets.schema.json +113 -0
  5. package/src/agent/auth.ts +4 -2
  6. package/src/agent/index.ts +16 -28
  7. package/src/agent/model-fallback.ts +127 -0
  8. package/src/agent/session-meta.ts +1 -1
  9. package/src/agent/session-origin.ts +3 -2
  10. package/src/agent/tools/curl-impersonate.ts +300 -0
  11. package/src/agent/tools/ddg.ts +13 -88
  12. package/src/agent/tools/webfetch/fetch.ts +105 -2
  13. package/src/agent/tools/webfetch/tool.ts +4 -0
  14. package/src/bundled-plugins/agent-browser/shim.ts +47 -0
  15. package/src/bundled-plugins/backup/subagents.ts +2 -0
  16. package/src/bundled-plugins/memory/README.md +49 -12
  17. package/src/bundled-plugins/memory/citation-superset.ts +63 -0
  18. package/src/bundled-plugins/memory/dreaming.ts +105 -17
  19. package/src/bundled-plugins/memory/index.ts +2 -2
  20. package/src/bundled-plugins/memory/memory-logger.ts +45 -26
  21. package/src/bundled-plugins/memory/strength.ts +127 -0
  22. package/src/bundled-plugins/memory/topics.ts +75 -0
  23. package/src/bundled-plugins/security/index.ts +88 -43
  24. package/src/bundled-plugins/security/permissions.ts +36 -0
  25. package/src/bundled-plugins/security/policies/git-exfil.ts +20 -0
  26. package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +12 -0
  27. package/src/bundled-plugins/security/policies/prompt-injection.ts +23 -3
  28. package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +7 -0
  29. package/src/bundled-plugins/security/policies/secret-exfil-read.ts +6 -0
  30. package/src/bundled-plugins/security/policies/session-search-secrets.ts +9 -0
  31. package/src/bundled-plugins/security/policies/ssrf.ts +6 -0
  32. package/src/bundled-plugins/security/policies/system-prompt-leak.ts +7 -0
  33. package/src/channels/adapters/github/auth-app.ts +120 -0
  34. package/src/channels/adapters/github/auth-pat.ts +50 -0
  35. package/src/channels/adapters/github/auth.ts +33 -0
  36. package/src/channels/adapters/github/channel-resolver.ts +30 -0
  37. package/src/channels/adapters/github/dedup.ts +26 -0
  38. package/src/channels/adapters/github/event-allowlist.ts +8 -0
  39. package/src/channels/adapters/github/fetch-attachment.ts +5 -0
  40. package/src/channels/adapters/github/history.ts +63 -0
  41. package/src/channels/adapters/github/inbound.ts +286 -0
  42. package/src/channels/adapters/github/index.ts +370 -0
  43. package/src/channels/adapters/github/managed-path.ts +54 -0
  44. package/src/channels/adapters/github/membership.ts +35 -0
  45. package/src/channels/adapters/github/outbound.ts +145 -0
  46. package/src/channels/adapters/github/webhook-register.ts +349 -0
  47. package/src/channels/manager.ts +94 -9
  48. package/src/channels/router.ts +194 -28
  49. package/src/channels/schema.ts +31 -1
  50. package/src/channels/tunnel-bridge.ts +51 -0
  51. package/src/channels/types.ts +3 -1
  52. package/src/cli/builtins.ts +28 -0
  53. package/src/cli/channel.ts +511 -25
  54. package/src/cli/container-command-client.ts +244 -0
  55. package/src/cli/cron.ts +173 -0
  56. package/src/cli/host-command-runner.ts +150 -0
  57. package/src/cli/index.ts +42 -1
  58. package/src/cli/init.ts +400 -67
  59. package/src/cli/model.ts +14 -4
  60. package/src/cli/oauth-callbacks.ts +49 -0
  61. package/src/cli/plugin-command-help.ts +49 -0
  62. package/src/cli/plugin-commands-dispatch.ts +112 -0
  63. package/src/cli/plugin-commands.ts +118 -0
  64. package/src/cli/provider.ts +3 -20
  65. package/src/cli/tui.ts +10 -2
  66. package/src/cli/tunnel.ts +533 -0
  67. package/src/cli/ui.ts +8 -3
  68. package/src/config/config.ts +134 -24
  69. package/src/config/models-mutation.ts +42 -8
  70. package/src/config/providers-mutation.ts +12 -8
  71. package/src/container/start.ts +48 -4
  72. package/src/cron/bridge.ts +136 -0
  73. package/src/cron/consumer.ts +174 -48
  74. package/src/cron/index.ts +19 -2
  75. package/src/cron/list.ts +105 -0
  76. package/src/cron/scheduler.ts +12 -3
  77. package/src/cron/schema.ts +11 -3
  78. package/src/doctor/checks.ts +0 -50
  79. package/src/init/dockerfile.ts +165 -13
  80. package/src/init/ensure-deps.ts +15 -4
  81. package/src/init/github-webhook-install.ts +109 -0
  82. package/src/init/hatching.ts +2 -2
  83. package/src/init/index.ts +519 -12
  84. package/src/init/oauth-login.ts +17 -3
  85. package/src/init/run-bun-install.ts +17 -3
  86. package/src/init/run-owner-claim.ts +11 -2
  87. package/src/permissions/builtins.ts +29 -2
  88. package/src/permissions/match-rule.ts +24 -2
  89. package/src/permissions/permissions.ts +24 -7
  90. package/src/permissions/resolve.ts +1 -0
  91. package/src/plugin/define.ts +44 -1
  92. package/src/plugin/index.ts +18 -3
  93. package/src/plugin/manager.ts +16 -0
  94. package/src/plugin/registry.ts +85 -3
  95. package/src/plugin/types.ts +144 -1
  96. package/src/plugin/zod-introspect.ts +100 -0
  97. package/src/role-claim/match-rule.ts +2 -1
  98. package/src/run/index.ts +112 -4
  99. package/src/secrets/index.ts +1 -1
  100. package/src/secrets/schema.ts +21 -0
  101. package/src/server/command-runner.ts +476 -0
  102. package/src/server/index.ts +388 -5
  103. package/src/shared/index.ts +8 -0
  104. package/src/shared/protocol.ts +80 -1
  105. package/src/skills/typeclaw-channel-github/SKILL.md +24 -0
  106. package/src/skills/typeclaw-config/SKILL.md +27 -26
  107. package/src/skills/typeclaw-cron/SKILL.md +234 -3
  108. package/src/skills/typeclaw-memory/SKILL.md +25 -15
  109. package/src/skills/typeclaw-monorepo/SKILL.md +2 -2
  110. package/src/skills/typeclaw-permissions/SKILL.md +35 -16
  111. package/src/skills/typeclaw-plugins/SKILL.md +251 -5
  112. package/src/skills/typeclaw-tunnels/SKILL.md +111 -0
  113. package/src/test-helpers/wait-for.ts +50 -0
  114. package/src/tui/index.ts +70 -7
  115. package/src/tunnels/__fixtures__/cloudflared-quick-stderr.txt +11 -0
  116. package/src/tunnels/events.ts +14 -0
  117. package/src/tunnels/index.ts +12 -0
  118. package/src/tunnels/log-ring.ts +54 -0
  119. package/src/tunnels/manager.ts +139 -0
  120. package/src/tunnels/providers/cloudflare-quick.ts +189 -0
  121. package/src/tunnels/providers/external.ts +53 -0
  122. package/src/tunnels/quick-url-parser.ts +5 -0
  123. package/src/tunnels/types.ts +43 -0
  124. package/src/usage/report.ts +15 -12
  125. 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 to ${ref}.`),
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}" → ${ref}.`),
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 refWidth = Math.max(3, ...entries.map((e) => e.ref.length))
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.ref.padEnd(refWidth)} ${e.providerId.padEnd(12)} ${status}`
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
+ }
@@ -1,4 +1,4 @@
1
- import { cancel, intro, isCancel, log, note, password, select, text } from '@clack/prompts'
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 callbacks = {
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 { createTui } from '@/tui'
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({ url, initialPrompt: args.prompt })
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
  })