typeclaw 0.3.1 → 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.
- 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/session-meta.ts +1 -1
- package/src/agent/session-origin.ts +3 -2
- package/src/bundled-plugins/security/index.ts +3 -2
- 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 +286 -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/schema.ts +31 -1
- package/src/channels/tunnel-bridge.ts +51 -0
- 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 +256 -27
- package/src/cli/model.ts +4 -2
- 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/tui.ts +10 -2
- package/src/cli/tunnel.ts +533 -0
- package/src/cli/ui.ts +8 -3
- package/src/config/config.ts +75 -0
- package/src/container/start.ts +30 -3
- package/src/cron/bridge.ts +136 -0
- package/src/cron/consumer.ts +45 -5
- 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 +59 -13
- package/src/init/ensure-deps.ts +15 -4
- package/src/init/github-webhook-install.ts +109 -0
- package/src/init/index.ts +505 -9
- package/src/init/run-bun-install.ts +17 -3
- package/src/init/run-owner-claim.ts +11 -2
- package/src/permissions/builtins.ts +6 -1
- package/src/permissions/match-rule.ts +24 -2
- package/src/permissions/resolve.ts +1 -0
- package/src/plugin/define.ts +42 -1
- package/src/plugin/index.ts +18 -3
- package/src/plugin/manager.ts +2 -0
- package/src/plugin/registry.ts +85 -3
- package/src/plugin/types.ts +138 -1
- package/src/plugin/zod-introspect.ts +100 -0
- package/src/role-claim/match-rule.ts +2 -1
- package/src/run/index.ts +110 -3
- 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-monorepo/SKILL.md +2 -2
- package/src/skills/typeclaw-permissions/SKILL.md +5 -4
- 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 +35 -4
- 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/typeclaw.schema.json +254 -1
package/src/channels/manager.ts
CHANGED
|
@@ -2,15 +2,23 @@ import { createHash } from 'node:crypto'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import type { PermissionService } from '@/permissions'
|
|
5
|
+
import type { GithubSecretsBlock } from '@/secrets'
|
|
5
6
|
import { SecretsKakaoCredentialStore } from '@/secrets/kakao-store'
|
|
6
7
|
import { SecretsBackend } from '@/secrets/storage'
|
|
7
8
|
|
|
8
9
|
import { createDiscordBotAdapter, type DiscordBotAdapter } from './adapters/discord-bot'
|
|
10
|
+
import { createGithubAdapter, type GithubAdapter } from './adapters/github'
|
|
9
11
|
import { createKakaotalkAdapter, type KakaotalkAdapter } from './adapters/kakaotalk'
|
|
10
12
|
import { createSlackBotAdapter, type SlackBotAdapter } from './adapters/slack-bot'
|
|
11
13
|
import { createTelegramBotAdapter, type TelegramBotAdapter } from './adapters/telegram-bot'
|
|
12
14
|
import { createChannelRouter, type ChannelRouter, type ClaimHandler, type CreateSessionForChannel } from './router'
|
|
13
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
ADAPTER_IDS,
|
|
17
|
+
type AdapterId,
|
|
18
|
+
type ChannelAdapterConfig,
|
|
19
|
+
type ChannelsConfig,
|
|
20
|
+
type GithubAdapterConfig,
|
|
21
|
+
} from './schema'
|
|
14
22
|
|
|
15
23
|
export type ChannelManagerLogger = {
|
|
16
24
|
info: (msg: string) => void
|
|
@@ -48,6 +56,7 @@ export type ChannelManagerOptions = {
|
|
|
48
56
|
createSessionForChannel?: CreateSessionForChannel
|
|
49
57
|
// Test seams: let fake adapters replace the real adapter wiring per id.
|
|
50
58
|
createDiscordAdapter?: typeof createDiscordBotAdapter
|
|
59
|
+
createGithubAdapter?: typeof createGithubAdapter
|
|
51
60
|
createKakaotalkAdapter?: typeof createKakaotalkAdapter
|
|
52
61
|
createSlackAdapter?: typeof createSlackBotAdapter
|
|
53
62
|
createTelegramAdapter?: typeof createTelegramBotAdapter
|
|
@@ -62,16 +71,24 @@ export type ChannelManagerOptions = {
|
|
|
62
71
|
// code. Production wiring sets this from the role-claim subsystem (see
|
|
63
72
|
// src/run/index.ts). Tests typically omit it.
|
|
64
73
|
claimHandler?: ClaimHandler
|
|
74
|
+
tunnelUrlForChannel?: (channelName: string) => string | null
|
|
75
|
+
// Whether the user declared a `tunnels[]` entry bound to this channel.
|
|
76
|
+
// Lets channel-bound adapters distinguish "operator opted out of public
|
|
77
|
+
// webhook delivery" from "operator opted in but the tunnel never produced
|
|
78
|
+
// a URL" so error logs can be precise. Same shape as
|
|
79
|
+
// `tunnelUrlForChannel` for consistency. Optional for tests.
|
|
80
|
+
tunnelConfiguredForChannel?: (channelName: string) => boolean
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
export type ChannelManager = {
|
|
68
84
|
router: ChannelRouter
|
|
69
85
|
start: () => Promise<void>
|
|
70
86
|
stop: () => Promise<void>
|
|
87
|
+
restartAdapter: (name: AdapterId) => Promise<void>
|
|
71
88
|
reload: () => Promise<{ started: string[]; stopped: string[]; restartRequired: string[] }>
|
|
72
89
|
}
|
|
73
90
|
|
|
74
|
-
type AnyAdapter = DiscordBotAdapter | KakaotalkAdapter | SlackBotAdapter | TelegramBotAdapter
|
|
91
|
+
type AnyAdapter = DiscordBotAdapter | GithubAdapter | KakaotalkAdapter | SlackBotAdapter | TelegramBotAdapter
|
|
75
92
|
|
|
76
93
|
// Credential signature is the comparison key for credential-rotation
|
|
77
94
|
// detection on reload. Discord and Telegram each use a single bot token;
|
|
@@ -98,14 +115,27 @@ export function createChannelManager(options: ChannelManagerOptions): ChannelMan
|
|
|
98
115
|
...(options.claimHandler ? { claimHandler: options.claimHandler } : {}),
|
|
99
116
|
})
|
|
100
117
|
const createDiscordAdapter = options.createDiscordAdapter ?? createDiscordBotAdapter
|
|
118
|
+
const createGithub = options.createGithubAdapter ?? createGithubAdapter
|
|
101
119
|
const createKakaotalk = options.createKakaotalkAdapter ?? createKakaotalkAdapter
|
|
102
120
|
const createSlackAdapter = options.createSlackAdapter ?? createSlackBotAdapter
|
|
103
121
|
const createTelegramAdapter = options.createTelegramAdapter ?? createTelegramBotAdapter
|
|
104
122
|
|
|
105
123
|
const live = new Map<AdapterId, AdapterEntry>()
|
|
124
|
+
const perAdapterSerial = new Map<AdapterId, Promise<unknown>>()
|
|
125
|
+
|
|
126
|
+
const runSerially = <T>(name: AdapterId, op: () => Promise<T>): Promise<T> => {
|
|
127
|
+
const prev = perAdapterSerial.get(name) ?? Promise.resolve()
|
|
128
|
+
const next = prev.then(op, op)
|
|
129
|
+
perAdapterSerial.set(
|
|
130
|
+
name,
|
|
131
|
+
next.catch(() => {}),
|
|
132
|
+
)
|
|
133
|
+
return next
|
|
134
|
+
}
|
|
106
135
|
|
|
107
136
|
const buildCredentialSignature = (name: AdapterId): { signature: string; missing: string[] } => {
|
|
108
137
|
if (name === 'kakaotalk') return buildKakaotalkSignature(options.agentDir)
|
|
138
|
+
if (name === 'github') return buildGithubSignature(options.agentDir)
|
|
109
139
|
const requiredEnvs = TOKEN_ENV[name]
|
|
110
140
|
const parts: string[] = []
|
|
111
141
|
const missing: string[] = []
|
|
@@ -151,6 +181,19 @@ export function createChannelManager(options: ChannelManagerOptions): ChannelMan
|
|
|
151
181
|
credentialsStore: createContainerKakaoCredentialStore(options.agentDir, env),
|
|
152
182
|
})
|
|
153
183
|
}
|
|
184
|
+
if (name === 'github') {
|
|
185
|
+
const secrets = readGithubSecrets(options.agentDir)
|
|
186
|
+
if (secrets === null) return null
|
|
187
|
+
return createGithub({
|
|
188
|
+
router,
|
|
189
|
+
configRef: () => (options.channelsConfigRef()[name] ?? cfg) as ChannelAdapterConfig & GithubAdapterConfig,
|
|
190
|
+
secrets,
|
|
191
|
+
agentDir: options.agentDir,
|
|
192
|
+
logger,
|
|
193
|
+
tunnelUrl: () => options.tunnelUrlForChannel?.('github') ?? null,
|
|
194
|
+
tunnelConfiguredForChannel: () => options.tunnelConfiguredForChannel?.('github') ?? false,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
154
197
|
if (name === 'telegram-bot') {
|
|
155
198
|
const token = env.TELEGRAM_BOT_TOKEN
|
|
156
199
|
if (token === undefined || token.trim() === '') return null
|
|
@@ -193,9 +236,9 @@ export function createChannelManager(options: ChannelManagerOptions): ChannelMan
|
|
|
193
236
|
const stopAdapter = async (name: AdapterId): Promise<void> => {
|
|
194
237
|
const entry = live.get(name)
|
|
195
238
|
if (!entry) return
|
|
196
|
-
live.delete(name)
|
|
197
239
|
try {
|
|
198
240
|
await entry.adapter.stop()
|
|
241
|
+
live.delete(name)
|
|
199
242
|
logger.info(`[channels] adapter "${name}" stopped`)
|
|
200
243
|
} catch (err) {
|
|
201
244
|
logger.error(`[channels] adapter "${name}" failed to stop: ${describe(err)}`)
|
|
@@ -209,15 +252,31 @@ export function createChannelManager(options: ChannelManagerOptions): ChannelMan
|
|
|
209
252
|
const cfg = options.channelsConfigRef()
|
|
210
253
|
for (const name of ADAPTER_IDS) {
|
|
211
254
|
const adapterCfg = cfg[name]
|
|
212
|
-
if (adapterCfg !== undefined) await startAdapter(name, adapterCfg)
|
|
255
|
+
if (adapterCfg !== undefined) await runSerially(name, () => startAdapter(name, adapterCfg))
|
|
213
256
|
}
|
|
214
257
|
},
|
|
215
258
|
|
|
216
259
|
async stop(): Promise<void> {
|
|
217
|
-
for (const name of Array.from(live.keys())) await stopAdapter(name)
|
|
260
|
+
for (const name of Array.from(live.keys())) await runSerially(name, () => stopAdapter(name))
|
|
218
261
|
await router.stop()
|
|
219
262
|
},
|
|
220
263
|
|
|
264
|
+
async restartAdapter(name: AdapterId): Promise<void> {
|
|
265
|
+
await runSerially(name, async () => {
|
|
266
|
+
if (!live.has(name)) {
|
|
267
|
+
logger.info(`[channels] restartAdapter('${name}'): adapter not live, skipping`)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
const currentCfg = options.channelsConfigRef()[name]
|
|
271
|
+
if (currentCfg === undefined) {
|
|
272
|
+
logger.info(`[channels] restartAdapter('${name}'): adapter config missing, skipping`)
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
await stopAdapter(name)
|
|
276
|
+
await startAdapter(name, currentCfg)
|
|
277
|
+
})
|
|
278
|
+
},
|
|
279
|
+
|
|
221
280
|
async reload(): Promise<{ started: string[]; stopped: string[]; restartRequired: string[] }> {
|
|
222
281
|
const cfg = options.channelsConfigRef()
|
|
223
282
|
const started: string[] = []
|
|
@@ -229,11 +288,11 @@ export function createChannelManager(options: ChannelManagerOptions): ChannelMan
|
|
|
229
288
|
const current = live.get(name)
|
|
230
289
|
if (desired === undefined || desired.enabled === false) {
|
|
231
290
|
if (current) {
|
|
232
|
-
await stopAdapter(name)
|
|
291
|
+
await runSerially(name, () => stopAdapter(name))
|
|
233
292
|
stopped.push(name)
|
|
234
293
|
}
|
|
235
294
|
} else if (!current) {
|
|
236
|
-
const ok = await startAdapter(name, desired)
|
|
295
|
+
const ok = await runSerially(name, () => startAdapter(name, desired))
|
|
237
296
|
if (ok) started.push(name)
|
|
238
297
|
} else {
|
|
239
298
|
const { signature, missing } = buildCredentialSignature(name)
|
|
@@ -246,7 +305,7 @@ export function createChannelManager(options: ChannelManagerOptions): ChannelMan
|
|
|
246
305
|
logger.warn(
|
|
247
306
|
`[channels] adapter "${name}" missing credentials after reload (${missing.join(', ')}); stopping`,
|
|
248
307
|
)
|
|
249
|
-
await stopAdapter(name)
|
|
308
|
+
await runSerially(name, () => stopAdapter(name))
|
|
250
309
|
stopped.push(name)
|
|
251
310
|
} else if (signature !== current.credentialSignature) {
|
|
252
311
|
const reason = name === 'kakaotalk' ? 'credential rotation' : 'token rotation'
|
|
@@ -263,7 +322,7 @@ export function createChannelManager(options: ChannelManagerOptions): ChannelMan
|
|
|
263
322
|
// Token-based adapters only. KakaoTalk's credentials live in
|
|
264
323
|
// secrets.json#channels.kakaotalk, not in env, so it goes through
|
|
265
324
|
// buildKakaotalkSignature instead.
|
|
266
|
-
const TOKEN_ENV: Record<Exclude<AdapterId, 'kakaotalk'>, readonly string[]> = {
|
|
325
|
+
const TOKEN_ENV: Record<Exclude<AdapterId, 'kakaotalk' | 'github'>, readonly string[]> = {
|
|
267
326
|
'discord-bot': ['DISCORD_BOT_TOKEN'],
|
|
268
327
|
'slack-bot': ['SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN'],
|
|
269
328
|
'telegram-bot': ['TELEGRAM_BOT_TOKEN'],
|
|
@@ -301,6 +360,32 @@ function buildKakaotalkSignature(agentDir: string): { signature: string; missing
|
|
|
301
360
|
}
|
|
302
361
|
}
|
|
303
362
|
|
|
363
|
+
function buildGithubSignature(agentDir: string): { signature: string; missing: string[] } {
|
|
364
|
+
const block = readGithubSecrets(agentDir)
|
|
365
|
+
if (block === null) return { signature: '', missing: ['secrets.json#channels.github'] }
|
|
366
|
+
const digest = createHash('sha256').update(JSON.stringify(block)).digest('hex')
|
|
367
|
+
return { signature: `secrets.json#channels.github@sha256:${digest}`, missing: [] }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function readGithubSecrets(agentDir: string): GithubSecretsBlock | null {
|
|
371
|
+
const path = join(agentDir, 'secrets.json')
|
|
372
|
+
try {
|
|
373
|
+
const block = new SecretsBackend(path).tryReadChannelsSync()?.github
|
|
374
|
+
return isGithubSecretsBlock(block) ? block : null
|
|
375
|
+
} catch {
|
|
376
|
+
return null
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isGithubSecretsBlock(value: unknown): value is GithubSecretsBlock {
|
|
381
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
382
|
+
const record = value as Record<string, unknown>
|
|
383
|
+
const auth = record.auth
|
|
384
|
+
if (typeof auth !== 'object' || auth === null || Array.isArray(auth)) return false
|
|
385
|
+
const authType = (auth as Record<string, unknown>).type
|
|
386
|
+
return authType === 'pat' || authType === 'app'
|
|
387
|
+
}
|
|
388
|
+
|
|
304
389
|
function isKakaoCredentialBlock(value: unknown): value is { accounts: Record<string, unknown> } {
|
|
305
390
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
306
391
|
if (!('accounts' in value)) return false
|
package/src/channels/schema.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
-
export const ADAPTER_IDS = ['discord-bot', 'kakaotalk', 'slack-bot', 'telegram-bot'] as const
|
|
3
|
+
export const ADAPTER_IDS = ['discord-bot', 'github', 'kakaotalk', 'slack-bot', 'telegram-bot'] as const
|
|
4
4
|
|
|
5
5
|
export type AdapterId = (typeof ADAPTER_IDS)[number]
|
|
6
6
|
|
|
@@ -99,6 +99,33 @@ const adapterSchema = z.object({
|
|
|
99
99
|
enabled: z.boolean().default(true),
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
+
export const DEFAULT_GITHUB_EVENT_ALLOWLIST = [
|
|
103
|
+
'issue_comment.created',
|
|
104
|
+
'pull_request_review_comment.created',
|
|
105
|
+
'discussion_comment.created',
|
|
106
|
+
'issues.opened',
|
|
107
|
+
'pull_request.opened',
|
|
108
|
+
'discussion.created',
|
|
109
|
+
'pull_request_review.submitted',
|
|
110
|
+
] as const
|
|
111
|
+
|
|
112
|
+
const githubChannelSchema = adapterSchema.extend({
|
|
113
|
+
// Optional now (PR 2): when omitted and a `tunnels[]` entry with
|
|
114
|
+
// `for: { kind: 'channel', name: 'github' }` exists, the runtime resolves
|
|
115
|
+
// the URL from the tunnel manager via the adapter's `tunnelUrl` callback.
|
|
116
|
+
// The github adapter skips webhook registration when no effective URL is available.
|
|
117
|
+
webhookUrl: z.string().url().optional(),
|
|
118
|
+
webhookPort: z.number().int().positive().default(8975),
|
|
119
|
+
eventAllowlist: z.array(z.string()).default([...DEFAULT_GITHUB_EVENT_ALLOWLIST]),
|
|
120
|
+
// Repositories whose webhooks the adapter manages. Each entry is an
|
|
121
|
+
// `owner/name` slug. On adapter start(), TypeClaw registers a webhook
|
|
122
|
+
// pointing at webhookUrl for every repo here (idempotent: existing hooks
|
|
123
|
+
// at the same URL are updated). On stop(), every hook TypeClaw created
|
|
124
|
+
// this session is deleted so a restart with a different webhookUrl (e.g.
|
|
125
|
+
// a tunnel reassigning a URL) doesn't leave orphaned hooks on GitHub.
|
|
126
|
+
repos: z.array(z.string()).default([]),
|
|
127
|
+
})
|
|
128
|
+
|
|
102
129
|
// KakaoTalk uses the same shape as every other adapter. There used to be an
|
|
103
130
|
// `autoMarkRead` opt-in here; the adapter now fires a LOCO NOTIREAD ack on
|
|
104
131
|
// every inbound MSG event unconditionally (see kakaotalk.ts) so the sender's
|
|
@@ -112,6 +139,7 @@ const adapterSchema = z.object({
|
|
|
112
139
|
export const channelsSchema = z
|
|
113
140
|
.object({
|
|
114
141
|
'discord-bot': adapterSchema.optional(),
|
|
142
|
+
github: githubChannelSchema.optional(),
|
|
115
143
|
kakaotalk: adapterSchema.optional(),
|
|
116
144
|
'slack-bot': adapterSchema.optional(),
|
|
117
145
|
'telegram-bot': adapterSchema.optional(),
|
|
@@ -120,4 +148,6 @@ export const channelsSchema = z
|
|
|
120
148
|
|
|
121
149
|
export type EngagementConfig = z.infer<typeof engagementSchema>
|
|
122
150
|
export type ChannelAdapterConfig = z.infer<typeof adapterSchema>
|
|
151
|
+
type ParsedGithubAdapterConfig = z.infer<typeof githubChannelSchema>
|
|
152
|
+
export type GithubAdapterConfig = ParsedGithubAdapterConfig
|
|
123
153
|
export type ChannelsConfig = z.infer<typeof channelsSchema>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Stream } from '@/stream'
|
|
2
|
+
import { isTunnelUrlChangedPayload } from '@/tunnels'
|
|
3
|
+
|
|
4
|
+
import type { AdapterId } from './schema'
|
|
5
|
+
|
|
6
|
+
export type TunnelBridgeLogger = {
|
|
7
|
+
info: (msg: string) => void
|
|
8
|
+
warn: (msg: string) => void
|
|
9
|
+
error: (msg: string) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type TunnelBridgeChannelManager = {
|
|
13
|
+
restartAdapter: (name: AdapterId) => Promise<void>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type TunnelBridgeOptions = {
|
|
17
|
+
stream: Stream
|
|
18
|
+
channelManager: TunnelBridgeChannelManager
|
|
19
|
+
logger?: TunnelBridgeLogger
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type TunnelBridge = {
|
|
23
|
+
stop: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const consoleLogger: TunnelBridgeLogger = {
|
|
27
|
+
info: (msg) => console.log(msg),
|
|
28
|
+
warn: (msg) => console.warn(msg),
|
|
29
|
+
error: (msg) => console.error(msg),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createTunnelBridge(options: TunnelBridgeOptions): TunnelBridge {
|
|
33
|
+
const logger = options.logger ?? consoleLogger
|
|
34
|
+
// Subscribe synchronously; run/index.ts must create this bridge before
|
|
35
|
+
// tunnelManager.start() so an initial provider URL broadcast cannot be missed.
|
|
36
|
+
const unsubscribe = options.stream.subscribe({ target: { kind: 'broadcast' } }, (msg) => {
|
|
37
|
+
const payload = msg.payload
|
|
38
|
+
if (!isTunnelUrlChangedPayload(payload)) return
|
|
39
|
+
if (payload.for.kind !== 'channel') return
|
|
40
|
+
const name = (payload.for as { name?: unknown }).name
|
|
41
|
+
if (typeof name !== 'string') return
|
|
42
|
+
logger.info(`[tunnels] ${name} URL → restarting adapter`)
|
|
43
|
+
void options.channelManager.restartAdapter(name as AdapterId).catch((err: unknown) => {
|
|
44
|
+
logger.error(`[tunnels] failed to restart ${name} adapter: ${err instanceof Error ? err.message : String(err)}`)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
stop: unsubscribe,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Single source of truth for the top-level `typeclaw` subcommands that the
|
|
2
|
+
// CLI dispatches via citty. Plugin commands MUST NOT shadow these names.
|
|
3
|
+
// `src/cli/index.ts` consumes this for argv interception; `src/plugin/registry.ts`
|
|
4
|
+
// consumes it to reject plugin commands that collide.
|
|
5
|
+
export const BUILTIN_COMMAND_NAMES = [
|
|
6
|
+
'init',
|
|
7
|
+
'run',
|
|
8
|
+
'tui',
|
|
9
|
+
'start',
|
|
10
|
+
'stop',
|
|
11
|
+
'restart',
|
|
12
|
+
'status',
|
|
13
|
+
'reload',
|
|
14
|
+
'logs',
|
|
15
|
+
'shell',
|
|
16
|
+
'compose',
|
|
17
|
+
'channel',
|
|
18
|
+
'cron',
|
|
19
|
+
'tunnel',
|
|
20
|
+
'role',
|
|
21
|
+
'provider',
|
|
22
|
+
'model',
|
|
23
|
+
'doctor',
|
|
24
|
+
'usage',
|
|
25
|
+
'_hostd',
|
|
26
|
+
] as const
|
|
27
|
+
|
|
28
|
+
export type BuiltinCommandName = (typeof BUILTIN_COMMAND_NAMES)[number]
|