typeclaw 0.3.0 → 0.4.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 (101) hide show
  1. package/README.md +20 -15
  2. package/auth.schema.json +113 -0
  3. package/package.json +2 -1
  4. package/scripts/dump-system-prompt.ts +401 -0
  5. package/secrets.schema.json +113 -0
  6. package/src/agent/index.ts +149 -30
  7. package/src/agent/provider-error.ts +44 -0
  8. package/src/agent/session-meta.ts +43 -0
  9. package/src/agent/session-origin.ts +3 -2
  10. package/src/agent/subagents.ts +8 -0
  11. package/src/agent/system-prompt.ts +70 -35
  12. package/src/bundled-plugins/security/index.ts +3 -2
  13. package/src/channels/adapters/github/auth-app.ts +120 -0
  14. package/src/channels/adapters/github/auth-pat.ts +50 -0
  15. package/src/channels/adapters/github/auth.ts +33 -0
  16. package/src/channels/adapters/github/channel-resolver.ts +30 -0
  17. package/src/channels/adapters/github/dedup.ts +26 -0
  18. package/src/channels/adapters/github/event-allowlist.ts +8 -0
  19. package/src/channels/adapters/github/fetch-attachment.ts +5 -0
  20. package/src/channels/adapters/github/history.ts +63 -0
  21. package/src/channels/adapters/github/inbound.ts +286 -0
  22. package/src/channels/adapters/github/index.ts +286 -0
  23. package/src/channels/adapters/github/managed-path.ts +54 -0
  24. package/src/channels/adapters/github/membership.ts +35 -0
  25. package/src/channels/adapters/github/outbound.ts +145 -0
  26. package/src/channels/adapters/github/webhook-register.ts +349 -0
  27. package/src/channels/manager.ts +94 -9
  28. package/src/channels/router.ts +28 -2
  29. package/src/channels/schema.ts +31 -1
  30. package/src/channels/tunnel-bridge.ts +51 -0
  31. package/src/cli/builtins.ts +28 -0
  32. package/src/cli/channel.ts +511 -25
  33. package/src/cli/container-command-client.ts +244 -0
  34. package/src/cli/cron.ts +173 -0
  35. package/src/cli/host-command-runner.ts +150 -0
  36. package/src/cli/index.ts +42 -1
  37. package/src/cli/init.ts +256 -27
  38. package/src/cli/model.ts +4 -2
  39. package/src/cli/plugin-command-help.ts +49 -0
  40. package/src/cli/plugin-commands-dispatch.ts +112 -0
  41. package/src/cli/plugin-commands.ts +118 -0
  42. package/src/cli/tui.ts +10 -2
  43. package/src/cli/tunnel.ts +533 -0
  44. package/src/cli/ui.ts +8 -3
  45. package/src/cli/usage.ts +30 -2
  46. package/src/config/config.ts +90 -4
  47. package/src/config/reloadable.ts +22 -4
  48. package/src/container/start.ts +30 -3
  49. package/src/cron/bridge.ts +136 -0
  50. package/src/cron/consumer.ts +62 -6
  51. package/src/cron/index.ts +19 -2
  52. package/src/cron/list.ts +105 -0
  53. package/src/cron/scheduler.ts +12 -3
  54. package/src/cron/schema.ts +11 -3
  55. package/src/doctor/checks.ts +0 -50
  56. package/src/init/dockerfile.ts +59 -13
  57. package/src/init/ensure-deps.ts +15 -4
  58. package/src/init/github-webhook-install.ts +109 -0
  59. package/src/init/index.ts +505 -9
  60. package/src/init/run-bun-install.ts +17 -3
  61. package/src/init/run-owner-claim.ts +11 -2
  62. package/src/permissions/builtins.ts +6 -1
  63. package/src/permissions/match-rule.ts +24 -2
  64. package/src/permissions/resolve.ts +1 -0
  65. package/src/plugin/define.ts +42 -1
  66. package/src/plugin/index.ts +18 -3
  67. package/src/plugin/manager.ts +2 -0
  68. package/src/plugin/registry.ts +85 -3
  69. package/src/plugin/types.ts +138 -1
  70. package/src/plugin/zod-introspect.ts +100 -0
  71. package/src/role-claim/match-rule.ts +2 -1
  72. package/src/run/index.ts +119 -4
  73. package/src/secrets/index.ts +1 -1
  74. package/src/secrets/schema.ts +21 -0
  75. package/src/server/command-runner.ts +476 -0
  76. package/src/server/index.ts +393 -15
  77. package/src/shared/index.ts +8 -0
  78. package/src/shared/protocol.ts +80 -1
  79. package/src/skills/typeclaw-channel-github/SKILL.md +24 -0
  80. package/src/skills/typeclaw-config/SKILL.md +27 -26
  81. package/src/skills/typeclaw-cron/SKILL.md +234 -3
  82. package/src/skills/typeclaw-monorepo/SKILL.md +2 -2
  83. package/src/skills/typeclaw-permissions/SKILL.md +5 -4
  84. package/src/skills/typeclaw-plugins/SKILL.md +251 -5
  85. package/src/skills/typeclaw-tunnels/SKILL.md +111 -0
  86. package/src/test-helpers/wait-for.ts +50 -0
  87. package/src/tui/index.ts +35 -4
  88. package/src/tunnels/__fixtures__/cloudflared-quick-stderr.txt +11 -0
  89. package/src/tunnels/events.ts +14 -0
  90. package/src/tunnels/index.ts +12 -0
  91. package/src/tunnels/log-ring.ts +54 -0
  92. package/src/tunnels/manager.ts +139 -0
  93. package/src/tunnels/providers/cloudflare-quick.ts +189 -0
  94. package/src/tunnels/providers/external.ts +53 -0
  95. package/src/tunnels/quick-url-parser.ts +5 -0
  96. package/src/tunnels/types.ts +43 -0
  97. package/src/usage/aggregate.ts +30 -1
  98. package/src/usage/index.ts +3 -2
  99. package/src/usage/report.ts +103 -3
  100. package/src/usage/scan.ts +59 -4
  101. package/typeclaw.schema.json +254 -1
@@ -0,0 +1,100 @@
1
+ import { z } from 'zod'
2
+
3
+ export type LeafKind = 'string' | 'number' | 'boolean' | 'unknown'
4
+
5
+ export type LeafDescription = {
6
+ kind: LeafKind
7
+ required: boolean
8
+ defaultValue: string | undefined
9
+ description: string | undefined
10
+ }
11
+
12
+ // Walks the chain of Zod 4 wrappers (optional, default, nullable) and returns
13
+ // the inner-most leaf node. Reads `_def.type` (Zod 4's lowercase discriminator)
14
+ // directly because the public `instanceof ZodOptional` checks don't work for
15
+ // `.innerType` — Zod 4 types it as the base `$ZodType`, not the public class
16
+ // hierarchy. If Zod ships a breaking change to `_def.type`, this is the only
17
+ // file that needs to update.
18
+ export function describeLeaf(leaf: unknown): LeafDescription {
19
+ let cur: unknown = leaf
20
+ let required = true
21
+ let defaultValue: string | undefined
22
+ let description: string | undefined
23
+
24
+ while (cur !== null && typeof cur === 'object') {
25
+ const node = cur as {
26
+ _def?: { type?: string; innerType?: unknown; defaultValue?: unknown }
27
+ description?: string
28
+ }
29
+ if (typeof node.description === 'string') description = node.description
30
+ const def = node._def
31
+ if (def === undefined) break
32
+ if (def.type === 'optional') {
33
+ required = false
34
+ cur = def.innerType
35
+ continue
36
+ }
37
+ if (def.type === 'default') {
38
+ required = false
39
+ const raw = typeof def.defaultValue === 'function' ? (def.defaultValue as () => unknown)() : def.defaultValue
40
+ defaultValue = raw === undefined ? undefined : JSON.stringify(raw)
41
+ cur = def.innerType
42
+ continue
43
+ }
44
+ if (def.type === 'nullable') {
45
+ cur = def.innerType
46
+ continue
47
+ }
48
+ return { kind: classify(def.type), required, defaultValue, description }
49
+ }
50
+ return { kind: 'unknown', required, defaultValue, description }
51
+ }
52
+
53
+ function classify(t: string | undefined): LeafKind {
54
+ switch (t) {
55
+ case 'string':
56
+ case 'literal':
57
+ case 'enum':
58
+ return 'string'
59
+ case 'number':
60
+ case 'int':
61
+ return 'number'
62
+ case 'boolean':
63
+ return 'boolean'
64
+ default:
65
+ return 'unknown'
66
+ }
67
+ }
68
+
69
+ // Coerces a single CLI flag value (string from argv or `true` when bare) to the
70
+ // type the leaf expects. Throws a precise error referencing the flag key when
71
+ // coercion fails; the caller surfaces the message to stderr.
72
+ export function coerceFlag(leaf: unknown, raw: string | true, key: string): unknown {
73
+ const info = describeLeaf(leaf)
74
+ if (info.kind === 'boolean') {
75
+ if (raw === true || raw === 'true') return true
76
+ if (raw === 'false') return false
77
+ throw new Error(`--${key}: expected true/false, got "${raw}"`)
78
+ }
79
+ if (info.kind === 'number') {
80
+ if (raw === true) throw new Error(`--${key} requires a numeric value`)
81
+ if (raw === '') throw new Error(`--${key}: empty value rejected; pass a number`)
82
+ const n = Number(raw)
83
+ if (Number.isNaN(n)) throw new Error(`--${key}: not a number: "${raw}"`)
84
+ return n
85
+ }
86
+ if (raw === true) throw new Error(`--${key} requires a value`)
87
+ return raw
88
+ }
89
+
90
+ // Returns true when `schema` is a Zod 4 z.object whose leaf properties are all
91
+ // primitive-shaped (string/number/boolean, with optional/default/nullable
92
+ // wrappers). Plugin command args schemas MUST satisfy this in v1.
93
+ export function isPrimitiveZodObject(schema: unknown): schema is z.ZodObject<z.ZodRawShape> {
94
+ if (!(schema instanceof z.ZodObject)) return false
95
+ const shape = (schema as z.ZodObject<z.ZodRawShape>).shape as Record<string, unknown>
96
+ for (const leaf of Object.values(shape)) {
97
+ if (describeLeaf(leaf).kind === 'unknown') return false
98
+ }
99
+ return true
100
+ }
@@ -21,9 +21,10 @@ export type PartialChannelOrigin = {
21
21
  authorId: string
22
22
  }
23
23
 
24
- const ADAPTER_TO_PLATFORM: Record<ChannelKey['adapter'], 'slack' | 'discord' | 'telegram' | 'kakao'> = {
24
+ const ADAPTER_TO_PLATFORM: Record<ChannelKey['adapter'], 'slack' | 'discord' | 'telegram' | 'kakao' | 'github'> = {
25
25
  'slack-bot': 'slack',
26
26
  'discord-bot': 'discord',
27
+ github: 'github',
27
28
  'telegram-bot': 'telegram',
28
29
  kakaotalk: 'kakao',
29
30
  }
package/src/run/index.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  } from '@/agent/subagents'
13
13
  import { resolveCapOptionsFromConfig } from '@/bundled-plugins/tool-result-cap'
14
14
  import { createChannelManager, createChannelsReloadable, type ChannelManager } from '@/channels'
15
+ import { createTunnelBridge, type TunnelBridge } from '@/channels/tunnel-bridge'
15
16
  import { createConfigReloadable, getConfig, loadConfigSync, loadPluginConfigsSync } from '@/config'
16
17
  import {
17
18
  type CronConsumer,
@@ -26,14 +27,24 @@ import {
26
27
  } from '@/cron'
27
28
  import { CLI_VERSION } from '@/init/cli-version'
28
29
  import { loadPlugins, type LoadPluginsResult, pluginCronJobs, type PluginRegistry, summarizeLoaded } from '@/plugin'
30
+ import { createPluginLogger } from '@/plugin/context'
31
+ import type { CronHandlerContext } from '@/plugin/types'
29
32
  import { createContainerBroker, publishForwardResult } from '@/portbroker'
30
33
  import { ReloadRegistry } from '@/reload'
31
34
  import { createClaimController } from '@/role-claim'
32
35
  import { hydrateChannelEnvFromSecrets } from '@/secrets'
33
36
  import { createServer, type Server } from '@/server'
37
+ import {
38
+ createCommandRunner,
39
+ type CommandRunner,
40
+ type CommandSpawnSubagent,
41
+ runExecForCommand,
42
+ runPromptForCommand,
43
+ } from '@/server/command-runner'
34
44
  import { createSessionFactory, type SessionFactory } from '@/sessions'
35
45
  import { createStream, type Stream } from '@/stream'
36
46
  import { createTui as createTuiDefault, type TuiOptions } from '@/tui'
47
+ import { createTunnelManager, type TunnelManager, type TunnelManagerOptions } from '@/tunnels'
37
48
 
38
49
  import { BUNDLED_PLUGINS } from './bundled-plugins'
39
50
  import { buildChannelSessionFactory } from './channel-session-factory'
@@ -45,6 +56,8 @@ export type TuiFactory = (options: TuiOptions) => { run: () => Promise<void> }
45
56
 
46
57
  export type LoadCronFn = (agentDir: string, options?: { subagents?: SubagentRegistry }) => Promise<LoadCronResult>
47
58
  export type SchedulerFactory = (options: { cwd: string; file: CronFile; onFire: (job: CronJob) => void }) => Scheduler
59
+ export type ChannelManagerFactory = typeof createChannelManager
60
+ export type TunnelManagerFactory = (options: TunnelManagerOptions) => TunnelManager
48
61
 
49
62
  export type StartAgentOptions = {
50
63
  port: number
@@ -56,6 +69,8 @@ export type StartAgentOptions = {
56
69
  createSchedulerFor?: SchedulerFactory
57
70
  sessionFactory?: SessionFactory
58
71
  stream?: Stream
72
+ createChannelManager?: ChannelManagerFactory
73
+ createTunnelManager?: TunnelManagerFactory
59
74
  }
60
75
 
61
76
  export type StartAgentResult = {
@@ -82,6 +97,8 @@ export async function startAgent({
82
97
  createSchedulerFor,
83
98
  sessionFactory = createSessionFactory({ agentDir: cwd }),
84
99
  stream = createStream(),
100
+ createChannelManager: createChannelManagerFor = createChannelManager,
101
+ createTunnelManager: createTunnelManagerFor = createTunnelManager,
85
102
  }: StartAgentOptions): Promise<StartAgentResult> {
86
103
  const reloadRegistry = new ReloadRegistry()
87
104
 
@@ -105,7 +122,13 @@ export async function startAgent({
105
122
  ...(cwdConfig.roles !== undefined ? { roles: cwdConfig.roles } : {}),
106
123
  })
107
124
 
108
- reloadRegistry.register(createConfigReloadable({ cwd, permissions: pluginsLoaded.permissions }))
125
+ reloadRegistry.register(
126
+ createConfigReloadable({
127
+ cwd,
128
+ permissions: pluginsLoaded.permissions,
129
+ skipMountValidation: containerName !== undefined,
130
+ }),
131
+ )
109
132
  const pluginRegistry = pluginsLoaded.registry
110
133
  const pluginHooks = pluginsLoaded.hooks
111
134
 
@@ -117,6 +140,7 @@ export async function startAgent({
117
140
  pluginRegistry.cronJobs.length > 0 ||
118
141
  pluginRegistry.skills.length > 0 ||
119
142
  pluginRegistry.skillsDirs.length > 0 ||
143
+ pluginRegistry.commands.length > 0 ||
120
144
  pluginsLoaded.loadedPlugins.length > 0
121
145
 
122
146
  const pluginRuntime = createPluginRuntime({
@@ -143,10 +167,21 @@ export async function startAgent({
143
167
  rolesProvider: () => getConfig().roles,
144
168
  })
145
169
 
146
- const channelManager = createChannelManager({
170
+ const tunnelManager: TunnelManager = createTunnelManagerFor({
171
+ tunnels: getConfig().tunnels,
172
+ stream,
173
+ resolveChannelUpstreamPort: (name) => {
174
+ if (name === 'github') return getConfig().channels.github?.webhookPort ?? null
175
+ return null
176
+ },
177
+ })
178
+
179
+ const channelManager = createChannelManagerFor({
147
180
  agentDir: cwd,
148
181
  channelsConfigRef: () => getConfig().channels,
149
182
  aliasesRef: () => getConfig().alias,
183
+ tunnelUrlForChannel: (name) => resolveTunnelUrlForChannel(name, tunnelManager),
184
+ tunnelConfiguredForChannel: (name) => isTunnelConfiguredForChannel(name),
150
185
  createSessionForChannel: buildChannelSessionFactory({
151
186
  cwd,
152
187
  sessionFactory,
@@ -238,6 +273,47 @@ export async function startAgent({
238
273
  const cronConsumer = createCronConsumer({
239
274
  stream,
240
275
  cwd,
276
+ invokeHandler: async (job) => {
277
+ const snap = pluginRuntime.get()
278
+ const registered = snap.registry.cronJobs.find((j) => j.globalId === job.id)
279
+ const pluginName = registered?.pluginName ?? '<unknown>'
280
+ const logger = createPluginLogger(pluginName)
281
+ const abortController = new AbortController()
282
+ const origin: SessionOrigin = {
283
+ kind: 'cron',
284
+ jobId: job.id,
285
+ jobKind: 'handler',
286
+ ...(job.scheduledByRole !== undefined ? { scheduledByRole: job.scheduledByRole } : {}),
287
+ scheduledByOrigin: (job.scheduledByOrigin as SessionOrigin | undefined) ?? { kind: 'config-file' },
288
+ }
289
+ const ctx: CronHandlerContext = {
290
+ jobId: job.id,
291
+ name: pluginName,
292
+ agentDir: cwd,
293
+ logger,
294
+ signal: abortController.signal,
295
+ permissions: pluginsLoaded.permissions,
296
+ origin,
297
+ prompt: (text: string) =>
298
+ runPromptForCommand({
299
+ text,
300
+ origin,
301
+ runtime: pluginRuntime,
302
+ agentDir: cwd,
303
+ permissions: pluginsLoaded.permissions,
304
+ signal: abortController.signal,
305
+ runtimeVersion: runtimeVersionOpt.runtimeVersion,
306
+ containerName: containerNameOpt.containerName,
307
+ }),
308
+ subagent: (subName: string, payload?: unknown) =>
309
+ dispatchSpawnSubagent(subName, payload, {
310
+ spawnedByOrigin: origin,
311
+ }),
312
+ exec: (strings: TemplateStringsArray, ...values: unknown[]) =>
313
+ runExecForCommand(strings, values, { cwd, signal: abortController.signal }),
314
+ }
315
+ await job.handler(ctx)
316
+ },
241
317
  createSessionForCron: async (job) => {
242
318
  const snap = pluginRuntime.get()
243
319
  const sessionManager = SessionManager.create(cwd, sessionFactory.sessionDir())
@@ -279,6 +355,7 @@ export async function startAgent({
279
355
  sessionId,
280
356
  agentDir: cwd,
281
357
  origin: cronOrigin,
358
+ session,
282
359
  ...(snap.hasAnyPluginContent ? { hooks: snap.hooks } : {}),
283
360
  getTranscriptPath: () => sessionManager.getSessionFile(),
284
361
  }
@@ -303,10 +380,15 @@ export async function startAgent({
303
380
  )
304
381
  }
305
382
 
383
+ const tunnelBridge: TunnelBridge = createTunnelBridge({ stream, channelManager })
384
+
306
385
  reloadRegistry.register(createChannelsReloadable({ manager: channelManager }))
307
386
  await channelManager.start()
308
387
 
309
- pluginsLoaded.setSpawnSubagent(async (name, payload, options) => {
388
+ // Captured separately from setSpawnSubagent so both the plugin context and
389
+ // the plugin-command runner can dispatch through the same path. The setter
390
+ // returns void, so without this local binding we couldn't reuse the fn.
391
+ const dispatchSpawnSubagent: CommandSpawnSubagent = async (name, payload, options) => {
310
392
  // Resolve the spawning session's role from its origin so the subagent
311
393
  // inherits it. Callers (hooks like session.idle) pass the parent origin
312
394
  // verbatim; we look up the role rather than letting the caller forge it,
@@ -321,11 +403,13 @@ export async function startAgent({
321
403
  agentDir: cwd,
322
404
  userPrompt: '',
323
405
  payload,
406
+ onProviderError: (message) => console.error(`[subagent] ${name}: LLM call failed: ${message}`),
324
407
  ...(options?.parentSessionId !== undefined ? { parentSessionId: options.parentSessionId } : {}),
325
408
  ...(spawnedByRole !== undefined ? { spawnedByRole } : {}),
326
409
  ...(options?.spawnedByOrigin !== undefined ? { spawnedByOrigin: options.spawnedByOrigin } : {}),
327
410
  })
328
- })
411
+ }
412
+ pluginsLoaded.setSpawnSubagent(dispatchSpawnSubagent)
329
413
  pluginsLoaded.markBooted()
330
414
 
331
415
  if (pluginsLoaded.loadedPlugins.length > 0) {
@@ -357,6 +441,17 @@ export async function startAgent({
357
441
  : undefined
358
442
  const containerBrokerOpt = containerBroker ? { containerBroker } : {}
359
443
 
444
+ const commandRunnerFactory = (outbound: import('@/server/command-runner').CommandOutbound): CommandRunner =>
445
+ createCommandRunner({
446
+ pluginRuntime,
447
+ permissions: pluginsLoaded.permissions,
448
+ spawnSubagent: dispatchSpawnSubagent,
449
+ agentDir: cwd,
450
+ runtimeVersion: CLI_VERSION,
451
+ containerName,
452
+ outbound,
453
+ })
454
+
360
455
  const server = createServer({
361
456
  port,
362
457
  reloadAll: () => reloadRegistry.reloadAll(),
@@ -367,12 +462,21 @@ export async function startAgent({
367
462
  agentDir: cwd,
368
463
  pluginRuntime,
369
464
  claimController,
465
+ commandRunnerFactory,
466
+ tunnelManager,
370
467
  ...containerNameOpt,
371
468
  ...runtimeVersionOpt,
372
469
  ...tuiTokenOpt,
373
470
  ...containerBrokerOpt,
374
471
  }).start()
375
472
 
473
+ // Tunnel manager starts AFTER the WS server is up so a slow/hanging
474
+ // provider (PR 2's cloudflared first-URL wait) cannot block TUI, reload,
475
+ // or channel adapter availability. External providers resolve URLs
476
+ // synchronously; future managed providers will resolve asynchronously
477
+ // and broadcast URL events when ready.
478
+ await tunnelManager.start()
479
+
376
480
  let stopped = false
377
481
  const stop = async () => {
378
482
  if (stopped) return
@@ -382,6 +486,8 @@ export async function startAgent({
382
486
  subagentConsumer.stop()
383
487
  server.stop(true)
384
488
  void disposeMaterializedSkills(pluginRuntime)
489
+ tunnelBridge.stop()
490
+ await tunnelManager.stop()
385
491
  await channelManager.stop()
386
492
  }
387
493
 
@@ -428,6 +534,15 @@ function buildLocalTuiUrl(port: number, token: string | null): string {
428
534
  return url.toString()
429
535
  }
430
536
 
537
+ function resolveTunnelUrlForChannel(channelName: string, tunnelManager: TunnelManager): string | null {
538
+ const tunnel = getConfig().tunnels.find((entry) => entry.for.kind === 'channel' && entry.for.name === channelName)
539
+ return tunnel ? tunnelManager.urlFor(tunnel.name) : null
540
+ }
541
+
542
+ function isTunnelConfiguredForChannel(channelName: string): boolean {
543
+ return getConfig().tunnels.some((entry) => entry.for.kind === 'channel' && entry.for.name === channelName)
544
+ }
545
+
431
546
  async function disposeMaterializedSkills(pluginRuntime: PluginRuntime): Promise<void> {
432
547
  const pending = pluginRuntime.drainPendingDisposal()
433
548
  const current = pluginRuntime.get().materializedSkills
@@ -1,4 +1,4 @@
1
- export { type Channels } from './schema'
1
+ export { type Channels, type GithubSecretsBlock } from './schema'
2
2
 
3
3
  export { createSecretsStoreForAgent, SecretsBackend } from './storage'
4
4
 
@@ -40,6 +40,23 @@ const telegramBotChannelSchema = z.object({
40
40
  token: secretFieldSchema.optional(),
41
41
  })
42
42
 
43
+ const githubPatAuthSchema = z.object({
44
+ type: z.literal('pat'),
45
+ token: secretFieldSchema,
46
+ })
47
+
48
+ const githubAppAuthSchema = z.object({
49
+ type: z.literal('app'),
50
+ appId: z.number().int().positive(),
51
+ privateKey: secretFieldSchema,
52
+ installationId: z.number().int().positive().optional(),
53
+ })
54
+
55
+ const githubChannelSchema = z.object({
56
+ auth: z.discriminatedUnion('type', [githubPatAuthSchema, githubAppAuthSchema]),
57
+ webhookSecret: secretFieldSchema,
58
+ })
59
+
43
60
  // Encrypted password envelope produced by src/secrets/encryption.ts. Optional
44
61
  // in the schema because legacy v2 accounts (pre-renewal feature) don't have
45
62
  // one; the renewal cron treats a missing envelope as "reauth required" and
@@ -92,6 +109,7 @@ export const channelsSchema = z
92
109
  .object({
93
110
  'slack-bot': slackBotChannelSchema.optional(),
94
111
  'discord-bot': discordBotChannelSchema.optional(),
112
+ github: githubChannelSchema.optional(),
95
113
  'telegram-bot': telegramBotChannelSchema.optional(),
96
114
  kakaotalk: kakaoChannelBlockSchema.optional(),
97
115
  })
@@ -113,6 +131,9 @@ export const secretsFileSchema = z.object({
113
131
  export type ProviderCredential = z.infer<typeof providerCredentialSchema>
114
132
  export type Providers = z.infer<typeof providersSchema>
115
133
  export type Channels = z.infer<typeof channelsSchema>
134
+ export type GithubPatAuthBlock = z.infer<typeof githubPatAuthSchema>
135
+ export type GithubAppAuthBlock = z.infer<typeof githubAppAuthSchema>
136
+ export type GithubSecretsBlock = z.infer<typeof githubChannelSchema>
116
137
  export type KakaoAccountRecord = z.infer<typeof kakaoAccountRecordSchema>
117
138
  export type PendingLoginRecord = z.infer<typeof kakaoPendingLoginRecordSchema>
118
139
  export type KakaoChannelBlock = z.infer<typeof kakaoChannelBlockSchema>