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/init.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
|
|
1
4
|
import { cancel, confirm, intro, isCancel, log, note, password, select, spinner, text } from '@clack/prompts'
|
|
2
5
|
import { defineCommand } from 'citty'
|
|
3
6
|
|
|
@@ -8,14 +11,17 @@ import {
|
|
|
8
11
|
type KnownModelRef,
|
|
9
12
|
type KnownProviderId,
|
|
10
13
|
} from '@/config/providers'
|
|
11
|
-
import type
|
|
14
|
+
import { checkDockerAvailable, type DockerAvailability } from '@/container'
|
|
12
15
|
import {
|
|
13
16
|
findAgentDir,
|
|
17
|
+
formatEagerGithubWebhookInstallResult,
|
|
14
18
|
hasExistingChannelSecrets,
|
|
15
19
|
isDirectoryNonEmpty,
|
|
16
20
|
isHatched,
|
|
17
21
|
readExistingProviderApiKey,
|
|
18
22
|
runInit,
|
|
23
|
+
type GithubInitCredentials,
|
|
24
|
+
type GithubTunnelProvider,
|
|
19
25
|
type InitStep,
|
|
20
26
|
type InitStepEvent,
|
|
21
27
|
type KakaotalkAuthResult,
|
|
@@ -23,23 +29,48 @@ import {
|
|
|
23
29
|
} from '@/init'
|
|
24
30
|
import { runKakaotalkBootstrap } from '@/init/kakaotalk-auth'
|
|
25
31
|
import { fetchModelOptions, type ModelOption } from '@/init/models-dev'
|
|
26
|
-
import { makeOAuthLoginRunner } from '@/init/oauth-login'
|
|
32
|
+
import { makeOAuthLoginRunner, type OAuthLoginResult } from '@/init/oauth-login'
|
|
27
33
|
|
|
34
|
+
import { buildOAuthCallbacks } from './oauth-callbacks'
|
|
28
35
|
import { c, done, errorLine } from './ui'
|
|
29
36
|
|
|
30
37
|
// ESC and Ctrl+C both produce clack's cancel symbol (the keypress layer
|
|
31
38
|
// aliases both to the same "cancel" action — there's no way to tell them
|
|
32
39
|
// apart through @clack/prompts). The wizard treats every cancel as "go
|
|
33
40
|
// back to the previous step": each step that runs an interactive prompt
|
|
34
|
-
// either advances with a value or rewinds.
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
+
// either advances with a value or rewinds.
|
|
42
|
+
//
|
|
43
|
+
// Two cancel patterns must not trap the user:
|
|
44
|
+
// 1. Single-prompt cancel-loop. On the first step (pick-provider) there
|
|
45
|
+
// is no previous step, so `back` re-displays the same prompt. Two
|
|
46
|
+
// consecutive cancels on that same prompt = the user wants out.
|
|
47
|
+
// 2. Auto-advance round-trip. `back` from `enter-api-key` routes to
|
|
48
|
+
// `pick-auth-method`, which for single-method providers (e.g.
|
|
49
|
+
// Fireworks, api-key only) returns its value without prompting and
|
|
50
|
+
// sends the wizard straight back to `enter-api-key`. The user only
|
|
51
|
+
// ever sees the same api-key prompt and has no way to escape.
|
|
52
|
+
//
|
|
53
|
+
// Both patterns are detected in `collectWizardInputs` and surfaced as
|
|
54
|
+
// `WizardAbortedError`, which the `init` command catches and turns into
|
|
55
|
+
// a clean exit. Inside an active clack prompt Ctrl+C is still aliased to
|
|
56
|
+
// cancel, so the abort hotkey is "cancel twice in a row".
|
|
57
|
+
export class WizardAbortedError extends Error {
|
|
58
|
+
// When the wizard ran a successful eager OAuth login before aborting, the
|
|
59
|
+
// resulting credentials are already on disk at `<cwd>/secrets.json`. The
|
|
60
|
+
// CLI surfaces this on abort so the user knows to either re-run init in
|
|
61
|
+
// the same directory (the credentials will be reused) or delete the file.
|
|
62
|
+
readonly oauthCredentialsSaved: boolean
|
|
63
|
+
constructor(options: { oauthCredentialsSaved?: boolean } = {}) {
|
|
64
|
+
super('Wizard aborted by user')
|
|
65
|
+
this.name = 'WizardAbortedError'
|
|
66
|
+
this.oauthCredentialsSaved = options.oauthCredentialsSaved === true
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type StepResult<T> = { kind: 'value'; value: T; auto?: boolean } | { kind: 'back' }
|
|
41
71
|
const back = <T>(): StepResult<T> => ({ kind: 'back' })
|
|
42
72
|
const value = <T>(v: T): StepResult<T> => ({ kind: 'value', value: v })
|
|
73
|
+
const autoValue = <T>(v: T): StepResult<T> => ({ kind: 'value', value: v, auto: true })
|
|
43
74
|
|
|
44
75
|
export const init = defineCommand({
|
|
45
76
|
meta: {
|
|
@@ -76,12 +107,54 @@ export const init = defineCommand({
|
|
|
76
107
|
}
|
|
77
108
|
|
|
78
109
|
intro('Initializing TypeClaw...')
|
|
79
|
-
log.info('Press ESC at any prompt to go back
|
|
110
|
+
log.info('Press ESC at any prompt to go back. Press ESC twice in a row to abort.')
|
|
111
|
+
|
|
112
|
+
// Docker preflight runs BEFORE the wizard so an OAuth login (which the
|
|
113
|
+
// wizard fires the moment the user picks "OAuth (browser login)") doesn't
|
|
114
|
+
// burn a real browser flow on an agent folder we can't actually start.
|
|
115
|
+
// `runInit` re-runs the preflight as a defense-in-depth gate, but
|
|
116
|
+
// surfacing the failure here lets the user fix Docker without re-doing
|
|
117
|
+
// every wizard step.
|
|
118
|
+
const preflightSpinner = spinner()
|
|
119
|
+
preflightSpinner.start('Checking Docker...')
|
|
120
|
+
const preflight = await checkDockerAvailable()
|
|
121
|
+
if (!preflight.ok) {
|
|
122
|
+
preflightSpinner.error(preflightFailureSummary(preflight))
|
|
123
|
+
note(preflightFailureGuidance(preflight).join('\n'), 'Docker check failed')
|
|
124
|
+
process.exit(1)
|
|
125
|
+
}
|
|
126
|
+
preflightSpinner.stop('Docker is reachable.')
|
|
80
127
|
|
|
81
|
-
|
|
128
|
+
let collected: CollectedInputs
|
|
129
|
+
try {
|
|
130
|
+
collected = await collectWizardInputs(cwd, defaultWizardPrompts)
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error instanceof WizardAbortedError) {
|
|
133
|
+
if (error.oauthCredentialsSaved) {
|
|
134
|
+
note(
|
|
135
|
+
[
|
|
136
|
+
'OAuth credentials were saved to `secrets.json` before you aborted.',
|
|
137
|
+
'Re-run `typeclaw init` here to pick up where you left off (the credentials',
|
|
138
|
+
'will be reused), or delete `secrets.json` if you want a clean restart.',
|
|
139
|
+
].join('\n'),
|
|
140
|
+
'Saved OAuth credentials',
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
cancel('Aborted.')
|
|
144
|
+
process.exit(0)
|
|
145
|
+
}
|
|
146
|
+
throw error
|
|
147
|
+
}
|
|
82
148
|
const { model, llmAuth, vision, channelChoice, reuseExistingChannel, channelSecrets } = collected
|
|
83
|
-
const {
|
|
84
|
-
|
|
149
|
+
const {
|
|
150
|
+
discordBotToken,
|
|
151
|
+
slackBotToken,
|
|
152
|
+
slackAppToken,
|
|
153
|
+
telegramBotToken,
|
|
154
|
+
kakaotalkEmail,
|
|
155
|
+
kakaotalkPassword,
|
|
156
|
+
github: githubCredentials,
|
|
157
|
+
} = channelSecrets
|
|
85
158
|
|
|
86
159
|
// TODO: add remaining wizard steps from TypeClaw.md once their runtime lands:
|
|
87
160
|
// - git backup (url + PAT) — Phase 10
|
|
@@ -96,7 +169,9 @@ export const init = defineCommand({
|
|
|
96
169
|
const reuseSlack = reuseExistingChannel && channelChoice === 'slack'
|
|
97
170
|
const reuseTelegram = reuseExistingChannel && channelChoice === 'telegram'
|
|
98
171
|
const reuseKakaotalk = reuseExistingChannel && channelChoice === 'kakaotalk'
|
|
172
|
+
const reuseGithub = reuseExistingChannel && channelChoice === 'github'
|
|
99
173
|
const wantsKakaotalk = (kakaotalkEmail !== undefined && kakaotalkPassword !== undefined) || reuseKakaotalk
|
|
174
|
+
const wantsGithub = githubCredentials !== undefined || reuseGithub
|
|
100
175
|
let hatchingOk = false
|
|
101
176
|
let preflightFailure: Extract<DockerAvailability, { ok: false }> | null = null
|
|
102
177
|
try {
|
|
@@ -130,6 +205,12 @@ export const init = defineCommand({
|
|
|
130
205
|
}),
|
|
131
206
|
}
|
|
132
207
|
: {}),
|
|
208
|
+
...(wantsGithub
|
|
209
|
+
? {
|
|
210
|
+
withGithub: true,
|
|
211
|
+
...(reuseGithub || githubCredentials === undefined ? {} : { githubCredentials }),
|
|
212
|
+
}
|
|
213
|
+
: {}),
|
|
133
214
|
onProgress: reportProgress(
|
|
134
215
|
(ok) => {
|
|
135
216
|
hatchingOk = ok
|
|
@@ -149,6 +230,12 @@ export const init = defineCommand({
|
|
|
149
230
|
process.exit(1)
|
|
150
231
|
}
|
|
151
232
|
|
|
233
|
+
if (githubCredentials?.tunnelProvider === 'none') {
|
|
234
|
+
log.warn(
|
|
235
|
+
'Webhook delivery is disabled until you add a `tunnels[]` entry or set `channels.github.webhookUrl` manually.',
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
152
239
|
if (hatchingOk) {
|
|
153
240
|
done({
|
|
154
241
|
title: c.green('Hatched. Your agent is ready.'),
|
|
@@ -179,7 +266,7 @@ interface WizardState {
|
|
|
179
266
|
channelReuseExisting?: boolean
|
|
180
267
|
}
|
|
181
268
|
|
|
182
|
-
type ChannelChoice = 'slack' | 'discord' | 'telegram' | 'kakaotalk' | 'none'
|
|
269
|
+
type ChannelChoice = 'slack' | 'discord' | 'telegram' | 'kakaotalk' | 'github' | 'none'
|
|
183
270
|
|
|
184
271
|
interface CollectedInputs {
|
|
185
272
|
model: ModelOption
|
|
@@ -201,6 +288,11 @@ interface CollectedInputs {
|
|
|
201
288
|
telegramBotToken?: string
|
|
202
289
|
kakaotalkEmail?: string
|
|
203
290
|
kakaotalkPassword?: string
|
|
291
|
+
// Structured (auth union + webhook + repo allowlist) rather than flat
|
|
292
|
+
// tokens, so it rides as one sub-object instead of sibling fields.
|
|
293
|
+
// `runInit` delegates to `runAddChannel` for GitHub to keep the github
|
|
294
|
+
// config-writing in one place.
|
|
295
|
+
github?: GithubInitCredentials
|
|
204
296
|
}
|
|
205
297
|
}
|
|
206
298
|
|
|
@@ -250,9 +342,24 @@ export interface WizardPrompts {
|
|
|
250
342
|
hasExistingChannelSecrets: (cwd: string, channel: Exclude<ChannelChoice, 'none'>) => Promise<boolean>
|
|
251
343
|
askReuseExistingChannel: (channel: Exclude<ChannelChoice, 'none'>) => Promise<StepResult<'reuse' | 'prompt'>>
|
|
252
344
|
runChannelFlow: (choice: ChannelChoice) => Promise<StepResult<CollectedInputs['channelSecrets']>>
|
|
253
|
-
|
|
345
|
+
runOAuthLogin: (
|
|
346
|
+
provider: (typeof KNOWN_PROVIDERS)[KnownProviderId],
|
|
347
|
+
cwd: string,
|
|
348
|
+
model: KnownModelRef,
|
|
349
|
+
) => Promise<OAuthLoginResult>
|
|
350
|
+
// Asked after a failed OAuth login. `apiKeyAvailable` is true when the
|
|
351
|
+
// provider also supports api-key auth (so the wizard can offer a fallback
|
|
352
|
+
// path); false for OAuth-only providers like openai-codex, where the only
|
|
353
|
+
// options are retry or abort.
|
|
354
|
+
askOAuthFailureRecovery: (
|
|
355
|
+
provider: (typeof KNOWN_PROVIDERS)[KnownProviderId],
|
|
356
|
+
reason: string,
|
|
357
|
+
apiKeyAvailable: boolean,
|
|
358
|
+
) => Promise<OAuthFailureRecovery>
|
|
254
359
|
}
|
|
255
360
|
|
|
361
|
+
export type OAuthFailureRecovery = 'retry' | 'api-key' | 'abort'
|
|
362
|
+
|
|
256
363
|
export const defaultWizardPrompts: WizardPrompts = {
|
|
257
364
|
loadCatalog,
|
|
258
365
|
readExistingApiKey: readExistingProviderApiKey,
|
|
@@ -267,21 +374,35 @@ export const defaultWizardPrompts: WizardPrompts = {
|
|
|
267
374
|
hasExistingChannelSecrets,
|
|
268
375
|
askReuseExistingChannel,
|
|
269
376
|
runChannelFlow,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
runLogin: makeOAuthLoginRunner(buildOAuthCallbacks(provider.name)),
|
|
273
|
-
}),
|
|
377
|
+
runOAuthLogin: (provider, cwd, model) => makeOAuthLoginRunner(buildOAuthCallbacks(provider.name))({ cwd, model }),
|
|
378
|
+
askOAuthFailureRecovery,
|
|
274
379
|
}
|
|
275
380
|
|
|
276
381
|
export async function collectWizardInputs(cwd: string, prompts: WizardPrompts): Promise<CollectedInputs> {
|
|
277
382
|
const catalog = await prompts.loadCatalog()
|
|
278
383
|
const state: WizardState = { catalog }
|
|
279
384
|
let step: StepId = 'pick-provider'
|
|
385
|
+
let pendingBackOrigin: StepId | null = null
|
|
386
|
+
let oauthCredentialsSaved = false
|
|
387
|
+
|
|
388
|
+
const abort = (): never => {
|
|
389
|
+
throw new WizardAbortedError({ oauthCredentialsSaved })
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const onResult = <T>(currentStep: StepId, result: StepResult<T>): StepResult<T> => {
|
|
393
|
+
if (result.kind === 'back') {
|
|
394
|
+
if (pendingBackOrigin === currentStep) abort()
|
|
395
|
+
pendingBackOrigin = currentStep
|
|
396
|
+
} else if (!result.auto) {
|
|
397
|
+
pendingBackOrigin = null
|
|
398
|
+
}
|
|
399
|
+
return result
|
|
400
|
+
}
|
|
280
401
|
|
|
281
402
|
while (true) {
|
|
282
403
|
switch (step) {
|
|
283
404
|
case 'pick-provider': {
|
|
284
|
-
const result = await prompts.pickProvider(catalog.options, state.providerId)
|
|
405
|
+
const result = onResult(step, await prompts.pickProvider(catalog.options, state.providerId))
|
|
285
406
|
if (result.kind === 'back') {
|
|
286
407
|
break
|
|
287
408
|
}
|
|
@@ -297,7 +418,7 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
297
418
|
}
|
|
298
419
|
|
|
299
420
|
case 'pick-model': {
|
|
300
|
-
const result = await prompts.pickModel(catalog.options, state.providerId!, state.model?.ref)
|
|
421
|
+
const result = onResult(step, await prompts.pickModel(catalog.options, state.providerId!, state.model?.ref))
|
|
301
422
|
if (result.kind === 'back') {
|
|
302
423
|
step = 'pick-provider'
|
|
303
424
|
break
|
|
@@ -310,7 +431,10 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
310
431
|
case 'reuse-existing-key': {
|
|
311
432
|
const provider = KNOWN_PROVIDERS[state.providerId!]
|
|
312
433
|
const existingApiKey = await prompts.readExistingApiKey(cwd, state.providerId!)
|
|
313
|
-
const decision =
|
|
434
|
+
const decision = onResult(
|
|
435
|
+
step,
|
|
436
|
+
await prompts.askReuseExistingKey(provider, existingApiKey, state.reuseExisting),
|
|
437
|
+
)
|
|
314
438
|
if (decision.kind === 'back') {
|
|
315
439
|
step = 'pick-model'
|
|
316
440
|
break
|
|
@@ -330,14 +454,39 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
330
454
|
|
|
331
455
|
case 'pick-auth-method': {
|
|
332
456
|
const provider = KNOWN_PROVIDERS[state.providerId!]
|
|
333
|
-
const result = await prompts.pickAuthMethod(provider, state.authMethod)
|
|
457
|
+
const result = onResult(step, await prompts.pickAuthMethod(provider, state.authMethod))
|
|
334
458
|
if (result.kind === 'back') {
|
|
335
459
|
step = 'reuse-existing-key'
|
|
336
460
|
break
|
|
337
461
|
}
|
|
338
462
|
state.authMethod = result.value
|
|
339
463
|
if (result.value === 'oauth') {
|
|
340
|
-
|
|
464
|
+
// Run the browser login eagerly so the user sees the OAuth URL the
|
|
465
|
+
// moment they pick "OAuth (browser login)" — not at the end of the
|
|
466
|
+
// wizard. On failure we ask the user how to recover (retry / fall
|
|
467
|
+
// back to API key / abort) instead of dumping them back into the
|
|
468
|
+
// auth method picker with no guidance.
|
|
469
|
+
const login = await runOAuthLoginSafely(prompts, provider, cwd, state.model!.ref)
|
|
470
|
+
if (!login.ok) {
|
|
471
|
+
const recovery = await prompts.askOAuthFailureRecovery(
|
|
472
|
+
provider,
|
|
473
|
+
login.reason,
|
|
474
|
+
providerSupportsApiKey(provider),
|
|
475
|
+
)
|
|
476
|
+
// The recovery prompt is a fresh user decision, so it must clear
|
|
477
|
+
// any back-token left over from an earlier step. Without this, a
|
|
478
|
+
// sequence like `enter-api-key → back → autoValue('oauth') →
|
|
479
|
+
// OAuth fails → recovery=api-key → enter-api-key` would treat the
|
|
480
|
+
// user's NEXT back press as a double-back and abort the wizard.
|
|
481
|
+
pendingBackOrigin = null
|
|
482
|
+
if (recovery === 'abort') abort()
|
|
483
|
+
state.authMethod = recovery === 'api-key' ? 'api-key' : undefined
|
|
484
|
+
state.llmAuth = undefined
|
|
485
|
+
step = recovery === 'api-key' ? 'enter-api-key' : 'pick-auth-method'
|
|
486
|
+
break
|
|
487
|
+
}
|
|
488
|
+
oauthCredentialsSaved = true
|
|
489
|
+
state.llmAuth = { kind: 'oauth-completed' }
|
|
341
490
|
step = stepAfterDefaultAuth(state)
|
|
342
491
|
} else {
|
|
343
492
|
step = 'enter-api-key'
|
|
@@ -347,7 +496,7 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
347
496
|
|
|
348
497
|
case 'enter-api-key': {
|
|
349
498
|
const provider = KNOWN_PROVIDERS[state.providerId!]
|
|
350
|
-
const result = await prompts.askApiKey(provider)
|
|
499
|
+
const result = onResult(step, await prompts.askApiKey(provider))
|
|
351
500
|
if (result.kind === 'back') {
|
|
352
501
|
step = 'pick-auth-method'
|
|
353
502
|
break
|
|
@@ -359,7 +508,7 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
359
508
|
|
|
360
509
|
case 'pick-vision-provider': {
|
|
361
510
|
const visionOptions = catalog.options.filter((o) => o.supportsVision)
|
|
362
|
-
const result = await prompts.pickVisionProvider(visionOptions, state.visionProviderId)
|
|
511
|
+
const result = onResult(step, await prompts.pickVisionProvider(visionOptions, state.visionProviderId))
|
|
363
512
|
if (result.kind === 'back') {
|
|
364
513
|
step = stepBeforeVision(state)
|
|
365
514
|
break
|
|
@@ -386,7 +535,10 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
386
535
|
|
|
387
536
|
case 'pick-vision-model': {
|
|
388
537
|
const visionOptions = catalog.options.filter((o) => o.supportsVision)
|
|
389
|
-
const result =
|
|
538
|
+
const result = onResult(
|
|
539
|
+
step,
|
|
540
|
+
await prompts.pickVisionModel(visionOptions, state.visionProviderId!, state.visionModel?.ref),
|
|
541
|
+
)
|
|
390
542
|
if (result.kind === 'back') {
|
|
391
543
|
step = 'pick-vision-provider'
|
|
392
544
|
break
|
|
@@ -415,14 +567,32 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
415
567
|
|
|
416
568
|
case 'pick-vision-auth-method': {
|
|
417
569
|
const provider = KNOWN_PROVIDERS[state.visionProviderId!]
|
|
418
|
-
const result = await prompts.pickAuthMethod(provider, state.visionAuthMethod)
|
|
570
|
+
const result = onResult(step, await prompts.pickAuthMethod(provider, state.visionAuthMethod))
|
|
419
571
|
if (result.kind === 'back') {
|
|
420
572
|
step = 'pick-vision-model'
|
|
421
573
|
break
|
|
422
574
|
}
|
|
423
575
|
state.visionAuthMethod = result.value
|
|
424
576
|
if (result.value === 'oauth') {
|
|
425
|
-
|
|
577
|
+
// Same eager-login + recovery-prompt rationale as the default-provider branch above.
|
|
578
|
+
const login = await runOAuthLoginSafely(prompts, provider, cwd, state.visionModel!.ref)
|
|
579
|
+
if (!login.ok) {
|
|
580
|
+
const recovery = await prompts.askOAuthFailureRecovery(
|
|
581
|
+
provider,
|
|
582
|
+
login.reason,
|
|
583
|
+
providerSupportsApiKey(provider),
|
|
584
|
+
)
|
|
585
|
+
// See the matching pendingBackOrigin reset in the default-provider
|
|
586
|
+
// branch above — same reasoning applies to vision auth recovery.
|
|
587
|
+
pendingBackOrigin = null
|
|
588
|
+
if (recovery === 'abort') abort()
|
|
589
|
+
state.visionAuthMethod = recovery === 'api-key' ? 'api-key' : undefined
|
|
590
|
+
state.visionLlmAuth = undefined
|
|
591
|
+
step = recovery === 'api-key' ? 'enter-vision-api-key' : 'pick-vision-auth-method'
|
|
592
|
+
break
|
|
593
|
+
}
|
|
594
|
+
oauthCredentialsSaved = true
|
|
595
|
+
state.visionLlmAuth = { kind: 'oauth-completed' }
|
|
426
596
|
step = 'pick-channel'
|
|
427
597
|
} else {
|
|
428
598
|
step = 'enter-vision-api-key'
|
|
@@ -432,7 +602,7 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
432
602
|
|
|
433
603
|
case 'enter-vision-api-key': {
|
|
434
604
|
const provider = KNOWN_PROVIDERS[state.visionProviderId!]
|
|
435
|
-
const result = await prompts.askApiKey(provider)
|
|
605
|
+
const result = onResult(step, await prompts.askApiKey(provider))
|
|
436
606
|
if (result.kind === 'back') {
|
|
437
607
|
step = 'pick-vision-auth-method'
|
|
438
608
|
break
|
|
@@ -443,7 +613,7 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
443
613
|
}
|
|
444
614
|
|
|
445
615
|
case 'pick-channel': {
|
|
446
|
-
const result = await prompts.pickChannel(state.channelChoice)
|
|
616
|
+
const result: StepResult<ChannelChoice> = onResult(step, await prompts.pickChannel(state.channelChoice))
|
|
447
617
|
if (result.kind === 'back') {
|
|
448
618
|
step = stepBeforePickChannel(state)
|
|
449
619
|
break
|
|
@@ -467,7 +637,7 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
467
637
|
break
|
|
468
638
|
}
|
|
469
639
|
state.channelReuseOffered = true
|
|
470
|
-
const decision = await prompts.askReuseExistingChannel(choice)
|
|
640
|
+
const decision = onResult(step, await prompts.askReuseExistingChannel(choice))
|
|
471
641
|
if (decision.kind === 'back') {
|
|
472
642
|
step = 'pick-channel'
|
|
473
643
|
break
|
|
@@ -483,7 +653,7 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
483
653
|
}
|
|
484
654
|
|
|
485
655
|
case 'channel-flow': {
|
|
486
|
-
const result = await prompts.runChannelFlow(state.channelChoice!)
|
|
656
|
+
const result = onResult(step, await prompts.runChannelFlow(state.channelChoice!))
|
|
487
657
|
if (result.kind === 'back') {
|
|
488
658
|
step = state.channelReuseOffered === true ? 'reuse-existing-channel' : 'pick-channel'
|
|
489
659
|
break
|
|
@@ -494,6 +664,25 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
494
664
|
}
|
|
495
665
|
}
|
|
496
666
|
|
|
667
|
+
// Belt-and-suspenders wrapper: `makeOAuthLoginRunner` already catches the
|
|
668
|
+
// upstream pi-ai login flow and returns `{ ok: false, reason }`, but the
|
|
669
|
+
// wizard cannot afford ANY uncaught throw from a custom runner (test seam,
|
|
670
|
+
// future plugin-contributed runner) — it would bubble out of
|
|
671
|
+
// `collectWizardInputs` and exit the whole init. Coerce unexpected throws to
|
|
672
|
+
// the normal failure path so the recovery prompt always fires.
|
|
673
|
+
async function runOAuthLoginSafely(
|
|
674
|
+
prompts: WizardPrompts,
|
|
675
|
+
provider: (typeof KNOWN_PROVIDERS)[KnownProviderId],
|
|
676
|
+
cwd: string,
|
|
677
|
+
model: KnownModelRef,
|
|
678
|
+
): Promise<OAuthLoginResult> {
|
|
679
|
+
try {
|
|
680
|
+
return await prompts.runOAuthLogin(provider, cwd, model)
|
|
681
|
+
} catch (error) {
|
|
682
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) }
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
497
686
|
function finalize(state: WizardState, channelSecrets: CollectedInputs['channelSecrets']): CollectedInputs {
|
|
498
687
|
return {
|
|
499
688
|
model: state.model!,
|
|
@@ -517,6 +706,8 @@ function channelDisplayName(choice: Exclude<ChannelChoice, 'none'>): string {
|
|
|
517
706
|
return 'Telegram'
|
|
518
707
|
case 'kakaotalk':
|
|
519
708
|
return 'KakaoTalk'
|
|
709
|
+
case 'github':
|
|
710
|
+
return 'GitHub'
|
|
520
711
|
}
|
|
521
712
|
}
|
|
522
713
|
|
|
@@ -632,8 +823,7 @@ async function pickAuthMethod(
|
|
|
632
823
|
if (isCancel(choice)) return back()
|
|
633
824
|
return value(choice)
|
|
634
825
|
}
|
|
635
|
-
|
|
636
|
-
return value(supportsOAuth ? 'oauth' : 'api-key')
|
|
826
|
+
return autoValue(supportsOAuth ? 'oauth' : 'api-key')
|
|
637
827
|
}
|
|
638
828
|
|
|
639
829
|
async function pickVisionProvider(
|
|
@@ -643,7 +833,7 @@ async function pickVisionProvider(
|
|
|
643
833
|
const providers = uniqueProviders(options)
|
|
644
834
|
if (providers.length === 0) {
|
|
645
835
|
log.warn('No vision-capable models available; skipping vision profile.')
|
|
646
|
-
return
|
|
836
|
+
return autoValue('skip')
|
|
647
837
|
}
|
|
648
838
|
const choice = await select<KnownProviderId | 'skip'>({
|
|
649
839
|
message: 'Your model is text-only. Pick a provider for the `vision` profile (used for image input)',
|
|
@@ -691,6 +881,28 @@ async function askApiKey(provider: (typeof KNOWN_PROVIDERS)[KnownProviderId]): P
|
|
|
691
881
|
return value(apiKey)
|
|
692
882
|
}
|
|
693
883
|
|
|
884
|
+
async function askOAuthFailureRecovery(
|
|
885
|
+
provider: (typeof KNOWN_PROVIDERS)[KnownProviderId],
|
|
886
|
+
reason: string,
|
|
887
|
+
apiKeyAvailable: boolean,
|
|
888
|
+
): Promise<OAuthFailureRecovery> {
|
|
889
|
+
note(reason, `${provider.name} OAuth login failed`)
|
|
890
|
+
const options: Array<{ value: OAuthFailureRecovery; label: string; hint?: string }> = [
|
|
891
|
+
{ value: 'retry', label: 'Retry OAuth login' },
|
|
892
|
+
]
|
|
893
|
+
if (apiKeyAvailable) {
|
|
894
|
+
options.push({ value: 'api-key', label: `Use a ${provider.name} API key instead` })
|
|
895
|
+
}
|
|
896
|
+
options.push({ value: 'abort', label: 'Abort init', hint: 'you can re-run `typeclaw init` later' })
|
|
897
|
+
const choice = await select<OAuthFailureRecovery>({
|
|
898
|
+
message: 'What next?',
|
|
899
|
+
options,
|
|
900
|
+
initialValue: 'retry',
|
|
901
|
+
})
|
|
902
|
+
if (isCancel(choice)) return 'abort'
|
|
903
|
+
return choice
|
|
904
|
+
}
|
|
905
|
+
|
|
694
906
|
async function pickChannel(initial: ChannelChoice | undefined): Promise<StepResult<ChannelChoice>> {
|
|
695
907
|
const choice = await select<ChannelChoice>({
|
|
696
908
|
message: 'Pick a channel to wire (you can add more later by editing typeclaw.json + secrets.json)',
|
|
@@ -699,6 +911,7 @@ async function pickChannel(initial: ChannelChoice | undefined): Promise<StepResu
|
|
|
699
911
|
{ value: 'discord', label: 'Discord' },
|
|
700
912
|
{ value: 'telegram', label: 'Telegram' },
|
|
701
913
|
{ value: 'kakaotalk', label: 'KakaoTalk' },
|
|
914
|
+
{ value: 'github', label: 'GitHub' },
|
|
702
915
|
{ value: 'none', label: 'Skip — no channel right now' },
|
|
703
916
|
],
|
|
704
917
|
initialValue: initial ?? 'slack',
|
|
@@ -719,6 +932,8 @@ async function runChannelFlow(choice: ChannelChoice): Promise<StepResult<Collect
|
|
|
719
932
|
return runSlackFlow()
|
|
720
933
|
case 'telegram':
|
|
721
934
|
return runTelegramFlow()
|
|
935
|
+
case 'github':
|
|
936
|
+
return runGithubFlow()
|
|
722
937
|
}
|
|
723
938
|
}
|
|
724
939
|
|
|
@@ -881,6 +1096,151 @@ async function runSlackFlow(): Promise<StepResult<CollectedInputs['channelSecret
|
|
|
881
1096
|
}
|
|
882
1097
|
}
|
|
883
1098
|
|
|
1099
|
+
async function runGithubFlow(): Promise<StepResult<CollectedInputs['channelSecrets']>> {
|
|
1100
|
+
note(
|
|
1101
|
+
[
|
|
1102
|
+
'Choose PAT auth for a quick setup, or GitHub App auth for expiring installation tokens.',
|
|
1103
|
+
'Required permissions: Issues read/write, Pull requests read/write, Discussions read/write (if used),',
|
|
1104
|
+
'Metadata read, and Webhooks read/write (TypeClaw will create and manage the repository webhooks for you).',
|
|
1105
|
+
].join('\n'),
|
|
1106
|
+
'Get GitHub credentials',
|
|
1107
|
+
)
|
|
1108
|
+
const authType = await select<'pat' | 'app'>({
|
|
1109
|
+
message: 'GitHub authentication type',
|
|
1110
|
+
options: [
|
|
1111
|
+
{ value: 'pat', label: 'Fine-grained personal access token' },
|
|
1112
|
+
{ value: 'app', label: 'GitHub App installation token' },
|
|
1113
|
+
],
|
|
1114
|
+
})
|
|
1115
|
+
if (isCancel(authType)) return back()
|
|
1116
|
+
const auth = authType === 'pat' ? await promptGithubPatAuth() : await promptGithubAppAuth()
|
|
1117
|
+
if (auth === null) return back()
|
|
1118
|
+
note('GitHub webhooks need a public URL. TypeClaw can manage a tunnel for you.', 'GitHub webhook tunnel')
|
|
1119
|
+
const tunnelProvider = await select<GithubTunnelProvider>({
|
|
1120
|
+
message: 'Tunnel provider',
|
|
1121
|
+
options: [
|
|
1122
|
+
{
|
|
1123
|
+
value: 'cloudflare-quick',
|
|
1124
|
+
label: 'Cloudflare Quick Tunnel — no signup, URL rotates on restart (recommended)',
|
|
1125
|
+
},
|
|
1126
|
+
{ value: 'external', label: 'External URL — I have my own reverse proxy / tunnel' },
|
|
1127
|
+
{ value: 'none', label: 'None — configure later by hand-editing typeclaw.json' },
|
|
1128
|
+
],
|
|
1129
|
+
initialValue: 'cloudflare-quick',
|
|
1130
|
+
})
|
|
1131
|
+
if (isCancel(tunnelProvider)) return back()
|
|
1132
|
+
const webhookUrl =
|
|
1133
|
+
tunnelProvider === 'external'
|
|
1134
|
+
? await text({
|
|
1135
|
+
message: 'Public webhook URL (GitHub will POST events here)',
|
|
1136
|
+
validate: (v) => validateGithubUrl(v ?? '', 'Webhook URL is required'),
|
|
1137
|
+
})
|
|
1138
|
+
: undefined
|
|
1139
|
+
if (isCancel(webhookUrl)) return back()
|
|
1140
|
+
const port = await text({
|
|
1141
|
+
message: 'Local webhook port inside the agent container',
|
|
1142
|
+
initialValue: '8975',
|
|
1143
|
+
validate: (v) => {
|
|
1144
|
+
const parsed = Number(v)
|
|
1145
|
+
return Number.isInteger(parsed) && parsed > 0 ? undefined : 'Port must be a positive integer'
|
|
1146
|
+
},
|
|
1147
|
+
})
|
|
1148
|
+
if (isCancel(port)) return back()
|
|
1149
|
+
const secret = await password({
|
|
1150
|
+
message: 'Webhook secret (leave blank to auto-generate)',
|
|
1151
|
+
})
|
|
1152
|
+
if (isCancel(secret)) return back()
|
|
1153
|
+
// clack's password() returns `undefined` on an empty submission (it has no
|
|
1154
|
+
// validate guard and never coerces to ''), so we normalize before the
|
|
1155
|
+
// length checks below to avoid a TypeError on the "leave blank" path.
|
|
1156
|
+
const enteredSecret = typeof secret === 'string' ? secret : ''
|
|
1157
|
+
const reposRaw = await text({
|
|
1158
|
+
message: 'Repositories to allow (comma-separated owner/repo)',
|
|
1159
|
+
validate: (v) => (parseGithubRepos(v ?? '').length > 0 ? undefined : 'At least one owner/repo is required'),
|
|
1160
|
+
})
|
|
1161
|
+
if (isCancel(reposRaw)) return back()
|
|
1162
|
+
const resolvedSecret = enteredSecret.length > 0 ? enteredSecret : randomBytes(32).toString('hex')
|
|
1163
|
+
return value({
|
|
1164
|
+
github: {
|
|
1165
|
+
webhookSecret: resolvedSecret,
|
|
1166
|
+
tunnelProvider,
|
|
1167
|
+
...(webhookUrl !== undefined ? { webhookUrl } : {}),
|
|
1168
|
+
webhookPort: Number(port),
|
|
1169
|
+
repos: parseGithubRepos(reposRaw),
|
|
1170
|
+
auth,
|
|
1171
|
+
},
|
|
1172
|
+
})
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
async function promptGithubPatAuth(): Promise<{ type: 'pat'; pat: string } | null> {
|
|
1176
|
+
const pat = await password({
|
|
1177
|
+
message: 'GitHub fine-grained PAT',
|
|
1178
|
+
validate: (v) => (v && v.length > 0 ? undefined : 'PAT is required'),
|
|
1179
|
+
})
|
|
1180
|
+
if (isCancel(pat)) return null
|
|
1181
|
+
return { type: 'pat', pat }
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
async function promptGithubAppAuth(): Promise<{
|
|
1185
|
+
type: 'app'
|
|
1186
|
+
appId: number
|
|
1187
|
+
privateKey: string
|
|
1188
|
+
installationId?: number
|
|
1189
|
+
} | null> {
|
|
1190
|
+
const appId = await text({
|
|
1191
|
+
message: 'GitHub App ID',
|
|
1192
|
+
validate: (v) => validatePositiveInteger(v ?? '', 'App ID is required'),
|
|
1193
|
+
})
|
|
1194
|
+
if (isCancel(appId)) return null
|
|
1195
|
+
const privateKeyInput = await text({
|
|
1196
|
+
message: 'GitHub App private key PEM, escaped PEM, or path to .pem file',
|
|
1197
|
+
validate: (v) => (v && v.length > 0 ? undefined : 'Private key is required'),
|
|
1198
|
+
})
|
|
1199
|
+
if (isCancel(privateKeyInput)) return null
|
|
1200
|
+
const installationId = await text({
|
|
1201
|
+
message: 'Installation ID (optional; leave blank to auto-discover)',
|
|
1202
|
+
validate: (v) =>
|
|
1203
|
+
v === undefined || v === '' ? undefined : validatePositiveInteger(v, 'Installation ID is required'),
|
|
1204
|
+
})
|
|
1205
|
+
if (isCancel(installationId)) return null
|
|
1206
|
+
const parsedInstallationId = installationId === '' ? undefined : Number(installationId)
|
|
1207
|
+
return {
|
|
1208
|
+
type: 'app',
|
|
1209
|
+
appId: Number(appId),
|
|
1210
|
+
privateKey: await resolveGithubPrivateKey(privateKeyInput),
|
|
1211
|
+
...(parsedInstallationId !== undefined ? { installationId: parsedInstallationId } : {}),
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
async function resolveGithubPrivateKey(input: string): Promise<string> {
|
|
1216
|
+
const normalized = input.replace(/\\n/g, '\n')
|
|
1217
|
+
if (normalized.includes('-----BEGIN') && normalized.includes('PRIVATE KEY-----')) return normalized
|
|
1218
|
+
return await readFile(input, 'utf8')
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function parseGithubRepos(input: string): string[] {
|
|
1222
|
+
return input
|
|
1223
|
+
.split(',')
|
|
1224
|
+
.map((v) => v.trim())
|
|
1225
|
+
.filter((v) => /^[^\s/]+\/[^\s/]+$/.test(v))
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function validateGithubUrl(v: string, requiredMessage: string): string | undefined {
|
|
1229
|
+
if (!v || v.length === 0) return requiredMessage
|
|
1230
|
+
try {
|
|
1231
|
+
new URL(v)
|
|
1232
|
+
return undefined
|
|
1233
|
+
} catch {
|
|
1234
|
+
return 'Must be a valid URL'
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function validatePositiveInteger(v: string, requiredMessage: string): string | undefined {
|
|
1239
|
+
if (!v || v.length === 0) return requiredMessage
|
|
1240
|
+
const parsed = Number(v)
|
|
1241
|
+
return Number.isInteger(parsed) && parsed > 0 ? undefined : 'Must be a positive integer'
|
|
1242
|
+
}
|
|
1243
|
+
|
|
884
1244
|
async function runTelegramFlow(): Promise<StepResult<CollectedInputs['channelSecrets']>> {
|
|
885
1245
|
note(
|
|
886
1246
|
[
|
|
@@ -950,6 +1310,9 @@ function reportProgress(
|
|
|
950
1310
|
case 'kakaotalk-auth':
|
|
951
1311
|
s.stop(reportKakaotalkAuth(event.result))
|
|
952
1312
|
break
|
|
1313
|
+
case 'github-webhooks':
|
|
1314
|
+
s.stop(formatEagerGithubWebhookInstallResult(event.result))
|
|
1315
|
+
break
|
|
953
1316
|
case 'oauth-login':
|
|
954
1317
|
s.stop(event.result.ok ? 'Logged in.' : `OAuth login failed: ${event.result.reason}`)
|
|
955
1318
|
break
|
|
@@ -1033,37 +1396,6 @@ export async function decideExistingApiKeyReuse(
|
|
|
1033
1396
|
return reuse === true ? 'reuse' : 'prompt'
|
|
1034
1397
|
}
|
|
1035
1398
|
|
|
1036
|
-
// Wraps the OAuth lifecycle into the same clack idiom the rest of the wizard
|
|
1037
|
-
// uses: a spinner over the "waiting for login" period, with onAuth printing
|
|
1038
|
-
// the URL the user needs to open and onPrompt falling back to a `text`
|
|
1039
|
-
// prompt for the manual code path. The spinner is started by onAuth and
|
|
1040
|
-
// stopped by the caller (runInit) — we don't try to manage it here because
|
|
1041
|
-
// the spinner lifecycle has to span emit('start') -> emit('done').
|
|
1042
|
-
function buildOAuthCallbacks(providerName: string) {
|
|
1043
|
-
return {
|
|
1044
|
-
onAuth: (url: string, instructions?: string) => {
|
|
1045
|
-
// Don't put the URL inside note(): clack wraps long lines with the box
|
|
1046
|
-
// border `│` on each wrapped segment, which corrupts the URL when the
|
|
1047
|
-
// user copy-pastes it. Keep instructional text in the box, but print
|
|
1048
|
-
// the URL itself as a bare console.log line that any terminal will
|
|
1049
|
-
// hyperlink intact.
|
|
1050
|
-
const preamble = [`Open this URL in your browser to authorize ${providerName}.`]
|
|
1051
|
-
if (instructions) preamble.push('', instructions)
|
|
1052
|
-
note(preamble.join('\n'), 'Browser login')
|
|
1053
|
-
console.log(url)
|
|
1054
|
-
console.log('')
|
|
1055
|
-
},
|
|
1056
|
-
onProgress: (message: string) => {
|
|
1057
|
-
log.info(message)
|
|
1058
|
-
},
|
|
1059
|
-
onPrompt: async (message: string, placeholder?: string): Promise<string | null> => {
|
|
1060
|
-
const value = await text({ message, ...(placeholder !== undefined ? { placeholder } : {}) })
|
|
1061
|
-
if (isCancel(value)) return null
|
|
1062
|
-
return value
|
|
1063
|
-
},
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
1399
|
function uniqueProviders(options: ModelOption[]): KnownProviderId[] {
|
|
1068
1400
|
const seen = new Set<KnownProviderId>()
|
|
1069
1401
|
const out: KnownProviderId[] = []
|
|
@@ -1096,6 +1428,7 @@ const START_MESSAGES: Record<Exclude<InitStep, 'hatching'>, string> = {
|
|
|
1096
1428
|
'oauth-login': 'Waiting for browser login...',
|
|
1097
1429
|
scaffold: 'Laying the egg...',
|
|
1098
1430
|
'kakaotalk-auth': 'Logging in to KakaoTalk...',
|
|
1431
|
+
'github-webhooks': 'Installing GitHub repository webhooks...',
|
|
1099
1432
|
install: 'Installing dependencies with bun...',
|
|
1100
1433
|
dockerfile: 'Writing Dockerfile...',
|
|
1101
1434
|
git: 'Initializing git repository...',
|