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
@@ -1,12 +1,13 @@
1
1
  import { styleText } from 'node:util'
2
2
 
3
- import type { ModelUsage, UsageTotals } from './aggregate'
3
+ import type { ModelUsage, OriginUsage, UsageTotals } from './aggregate'
4
4
  import { formatCacheHitRate, formatCost, formatTokens, isoDay } from './format'
5
5
  import type { UsageReport } from './index'
6
+ import type { OriginKind } from './scan'
6
7
 
7
8
  export type FormatOptions = {
8
9
  useColor?: boolean
9
- view?: 'summary' | 'daily' | 'session' | 'models'
10
+ view?: 'summary' | 'daily' | 'session' | 'models' | 'origin'
10
11
  limit?: number
11
12
  // Terminal width hint used to size the elastic Item column. Omit to render
12
13
  // without truncation (tests, piped output where columns is undefined).
@@ -26,6 +27,8 @@ export function formatReport(report: UsageReport, opts: FormatOptions = {}): str
26
27
  return renderSessions(report, ctx, opts.limit ?? 20)
27
28
  case 'models':
28
29
  return renderModels(report, ctx, opts.limit)
30
+ case 'origin':
31
+ return renderOrigin(report, ctx)
29
32
  }
30
33
  }
31
34
 
@@ -58,12 +61,20 @@ function renderSummary(report: UsageReport, ctx: RenderCtx): string {
58
61
 
59
62
  if (aggregation.byDay.length > 0) {
60
63
  sections.push('')
64
+ const trend = renderDailyTrend(aggregation.byDay, ctx)
65
+ if (trend !== null) sections.push(trend, '')
61
66
  sections.push(header('By day (most recent first)', ctx))
62
67
  const recent = aggregation.byDay.slice(-7).reverse()
63
68
  const dayRows = recent.map((d) => ({ label: d.date, totals: d as UsageTotals }))
64
69
  sections.push(renderTotalsTable(dayRows, ctx, { total: totalOfRows(dayRows) }))
65
70
  }
66
71
 
72
+ if (aggregation.byOrigin.length > 0) {
73
+ sections.push('')
74
+ sections.push(header('By origin', ctx))
75
+ sections.push(renderOriginTable(aggregation.byOrigin, ctx))
76
+ }
77
+
67
78
  if (aggregation.byModel.length > 0) {
68
79
  sections.push('')
69
80
  sections.push(header('By model', ctx))
@@ -84,6 +95,79 @@ function renderSummary(report: UsageReport, ctx: RenderCtx): string {
84
95
  return sections.join('\n')
85
96
  }
86
97
 
98
+ function renderOrigin(report: UsageReport, ctx: RenderCtx): string {
99
+ const sections: string[] = [sectionTitle('USAGE BY ORIGIN', report.agentDir, ctx)]
100
+ if (report.aggregation.byOrigin.length === 0) {
101
+ sections.push(dim('No assistant turns recorded yet.', ctx))
102
+ return sections.join('\n')
103
+ }
104
+ sections.push(renderOriginTable(report.aggregation.byOrigin, ctx))
105
+ return sections.join('\n')
106
+ }
107
+
108
+ // Single source of truth for the origin breakdown table used by both the
109
+ // summary view and the dedicated `usage origin` subcommand. Matches the
110
+ // column shape of renderTotalsTable's other callers (byDay, byModel) plus
111
+ // one extra column for session count — deliberately plain numbers rather
112
+ // than a proportional bar, since the Cost column already lets the eye sort
113
+ // rows by spend and an extra "share of max" bar adds no information that
114
+ // isn't already in the report.
115
+ function renderOriginTable(byOrigin: readonly OriginUsage[], ctx: RenderCtx): string {
116
+ const rows = byOrigin.map((o) => ({
117
+ label: renderOriginLabel(o.originKind, ctx),
118
+ totals: o as UsageTotals,
119
+ extra: String(o.sessionCount),
120
+ extraTruncatable: false,
121
+ }))
122
+ return renderTotalsTable(rows, ctx, {
123
+ extraHeader: 'Sessions',
124
+ total: totalOfRows(rows),
125
+ })
126
+ }
127
+
128
+ // Glyph + colored label for each origin kind. The glyphs are chosen to be
129
+ // instantly readable in a dense table: ▶ for TUI (interactive), ⏱ for cron
130
+ // (time-triggered), # for channel (chat-room sigil), ↳ for subagent (child),
131
+ // ? for unknown. ASCII fallbacks are not provided — the codebase already
132
+ // requires unicode for `…` `●` `✓` etc. on other commands.
133
+ function renderOriginLabel(kind: OriginKind, ctx: RenderCtx): string {
134
+ switch (kind) {
135
+ case 'tui':
136
+ return `${color('cyan', '▶', ctx)} ${'tui'}`
137
+ case 'cron':
138
+ return `${color('magenta', '⏱', ctx)} ${'cron'}`
139
+ case 'channel':
140
+ return `${color('green', '#', ctx)} ${'channel'}`
141
+ case 'subagent':
142
+ return `${color('yellow', '↳', ctx)} ${'subagent'}`
143
+ case 'unknown':
144
+ return `${dim('?', ctx)} ${dim('unknown', ctx)}`
145
+ }
146
+ }
147
+
148
+ // Sparkline trend across the full byDay range, scaled to the row's max cost.
149
+ // Returns null when there are fewer than 2 days (a single point conveys no
150
+ // trend information). The 8-level Unicode block scale `▁▂▃▄▅▆▇█` lets us
151
+ // pack ~80 days into a one-line glance — wider than any table-based view
152
+ // could fit at terminal widths under ~160 columns.
153
+ const SPARK_GLYPHS = '▁▂▃▄▅▆▇█'
154
+
155
+ function renderDailyTrend(byDay: readonly { date: string; cost: number }[], ctx: RenderCtx): string | null {
156
+ if (byDay.length < 2) return null
157
+ const costs = byDay.map((d) => d.cost)
158
+ const max = costs.reduce((m, c) => Math.max(m, c), 0)
159
+ if (max <= 0) return null
160
+ const spark = costs
161
+ .map((c) => {
162
+ const idx = Math.min(SPARK_GLYPHS.length - 1, Math.max(0, Math.round((c / max) * (SPARK_GLYPHS.length - 1))))
163
+ return SPARK_GLYPHS[idx]!
164
+ })
165
+ .join('')
166
+ const first = byDay[0]!.date
167
+ const last = byDay[byDay.length - 1]!.date
168
+ return `${dim('Trend (cost):', ctx)} ${color('cyan', spark, ctx)} ${dim(`${first} → ${last}`, ctx)}`
169
+ }
170
+
87
171
  function renderDaily(report: UsageReport, ctx: RenderCtx, limit: number | undefined): string {
88
172
  const days = limit !== undefined ? report.aggregation.byDay.slice(-limit) : report.aggregation.byDay
89
173
  if (days.length === 0) return dim('No usage in range.', ctx)
@@ -100,8 +184,9 @@ function renderSessions(report: UsageReport, ctx: RenderCtx, limit: number): str
100
184
  const rows = sessions.map((s) => {
101
185
  const firstModel = s.models[0]
102
186
  const extra = s.models.length > 1 ? `${s.models.length} models` : modelIdFromKey(firstModel)
187
+ const originGlyph = originGlyphOnly(s.originKind, ctx)
103
188
  return {
104
- label: `${color('magenta', s.sessionId.slice(0, 12), ctx)} ${dim(isoDay(s.firstAt), ctx)}`,
189
+ label: `${originGlyph} ${color('magenta', s.sessionId.slice(0, 12), ctx)} ${dim(isoDay(s.firstAt), ctx)}`,
105
190
  totals: s as UsageTotals,
106
191
  extra,
107
192
  extraTruncatable: s.models.length === 1,
@@ -113,6 +198,21 @@ function renderSessions(report: UsageReport, ctx: RenderCtx, limit: number): str
113
198
  ].join('\n')
114
199
  }
115
200
 
201
+ function originGlyphOnly(kind: OriginKind, ctx: RenderCtx): string {
202
+ switch (kind) {
203
+ case 'tui':
204
+ return color('cyan', '▶', ctx)
205
+ case 'cron':
206
+ return color('magenta', '⏱', ctx)
207
+ case 'channel':
208
+ return color('green', '#', ctx)
209
+ case 'subagent':
210
+ return color('yellow', '↳', ctx)
211
+ case 'unknown':
212
+ return dim('?', ctx)
213
+ }
214
+ }
215
+
116
216
  function renderModels(report: UsageReport, ctx: RenderCtx, limit: number | undefined): string {
117
217
  const models = limit !== undefined ? report.aggregation.byModel.slice(0, limit) : report.aggregation.byModel
118
218
  if (models.length === 0) return dim('No models in range.', ctx)
package/src/usage/scan.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import { readdir } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
3
 
4
+ // Recognised origin kinds. Keep aligned with SessionOrigin's discriminator in
5
+ // src/agent/session-origin.ts. The 'unknown' bucket catches sessions written
6
+ // before origin stamping landed AND sessions whose session-meta line is
7
+ // malformed or missing — surfacing them under one explicit label is more
8
+ // honest than silently dropping them.
9
+ export const ORIGIN_KINDS = ['tui', 'cron', 'channel', 'subagent', 'unknown'] as const
10
+ export type OriginKind = (typeof ORIGIN_KINDS)[number]
11
+
4
12
  // Narrow projection: session files can grow into tens of MB on long-lived
5
13
  // agents, so we deliberately drop content/tool blocks before aggregation.
6
14
  export type AssistantRow = {
@@ -15,6 +23,7 @@ export type AssistantRow = {
15
23
  cacheWrite: number
16
24
  totalTokens: number
17
25
  cost: number
26
+ originKind: OriginKind
18
27
  }
19
28
 
20
29
  export type ScanOptions = {
@@ -66,6 +75,13 @@ async function* readSessionFile(file: string, opts: ScanOptions): AsyncGenerator
66
75
  }
67
76
  const decoder = new TextDecoder()
68
77
  let buf = ''
78
+ // First-stamp-wins per file. Once a `typeclaw.session-meta` custom entry
79
+ // pins the origin, later entries with the same customType are ignored —
80
+ // session-resume code paths may legitimately re-stamp on reopen, and the
81
+ // earliest one is the authoritative one for the session's first turn.
82
+ // Stays 'unknown' for legacy files (no stamp at all) so usage attribution
83
+ // surfaces them as a distinct bucket rather than dropping the rows.
84
+ const ctx: ParseCtx = { originKind: 'unknown', originPinned: false }
69
85
  try {
70
86
  for await (const chunk of stream) {
71
87
  buf += decoder.decode(chunk, { stream: true })
@@ -73,7 +89,7 @@ async function* readSessionFile(file: string, opts: ScanOptions): AsyncGenerator
73
89
  while (nl !== -1) {
74
90
  const line = buf.slice(0, nl)
75
91
  buf = buf.slice(nl + 1)
76
- const row = parseLine(line, file, basename, opts)
92
+ const row = parseLine(line, file, basename, opts, ctx)
77
93
  if (row !== null) yield row
78
94
  nl = buf.indexOf('\n')
79
95
  }
@@ -86,17 +102,20 @@ async function* readSessionFile(file: string, opts: ScanOptions): AsyncGenerator
86
102
  // half-written record from a live writer is silently skipped (parseLine
87
103
  // returns null and does NOT warn for the tail).
88
104
  if (buf.length > 0) {
89
- const row = parseLine(buf, file, basename, opts, { isTail: true })
105
+ const row = parseLine(buf, file, basename, opts, ctx, { isTail: true })
90
106
  if (row !== null) yield row
91
107
  }
92
108
  }
93
109
 
110
+ type ParseCtx = { originKind: OriginKind; originPinned: boolean }
111
+
94
112
  function parseLine(
95
113
  line: string,
96
114
  file: string,
97
115
  basename: string,
98
116
  opts: ScanOptions,
99
- ctx: { isTail?: boolean } = {},
117
+ ctx: ParseCtx,
118
+ flags: { isTail?: boolean } = {},
100
119
  ): AssistantRow | null {
101
120
  const trimmed = line.trim()
102
121
  if (trimmed === '') return null
@@ -106,7 +125,18 @@ function parseLine(
106
125
  entry = JSON.parse(trimmed)
107
126
  } catch {
108
127
  // Silently skip the trailing tail: a live writer may be mid-append.
109
- if (ctx.isTail !== true) opts.onWarn?.(`skipping malformed JSONL line in ${basename}`)
128
+ if (flags.isTail !== true) opts.onWarn?.(`skipping malformed JSONL line in ${basename}`)
129
+ return null
130
+ }
131
+
132
+ if (isSessionMetaCustomEntry(entry)) {
133
+ if (!ctx.originPinned) {
134
+ const kind = entry.data.origin.kind
135
+ if ((ORIGIN_KINDS as readonly string[]).includes(kind)) {
136
+ ctx.originKind = kind as OriginKind
137
+ ctx.originPinned = true
138
+ }
139
+ }
110
140
  return null
111
141
  }
112
142
 
@@ -134,9 +164,34 @@ function parseLine(
134
164
  cacheWrite: numberOrZero(u.cacheWrite),
135
165
  totalTokens: numberOrZero(u.totalTokens),
136
166
  cost: numberOrZero(u.cost?.total),
167
+ originKind: ctx.originKind,
137
168
  }
138
169
  }
139
170
 
171
+ // Pi-coding-agent persists `appendCustomEntry(customType, data)` calls as
172
+ // `{type:"custom", customType, data, id, parentId, timestamp}` lines. We
173
+ // stamp our origin block with customType `typeclaw.session-meta` (constant
174
+ // kept in src/agent/session-meta.ts; duplicated as a literal here to keep
175
+ // the usage subsystem free of agent-stack imports — a Grep across the repo
176
+ // is the chosen drift guard).
177
+ type SessionMetaCustomEntry = {
178
+ type: 'custom'
179
+ customType: 'typeclaw.session-meta'
180
+ data: { origin: { kind: string } }
181
+ }
182
+
183
+ function isSessionMetaCustomEntry(value: unknown): value is SessionMetaCustomEntry {
184
+ if (typeof value !== 'object' || value === null) return false
185
+ const v = value as Record<string, unknown>
186
+ if (v.type !== 'custom') return false
187
+ if (v.customType !== 'typeclaw.session-meta') return false
188
+ if (typeof v.data !== 'object' || v.data === null) return false
189
+ const d = v.data as Record<string, unknown>
190
+ if (typeof d.origin !== 'object' || d.origin === null) return false
191
+ const o = d.origin as Record<string, unknown>
192
+ return typeof o.kind === 'string'
193
+ }
194
+
140
195
  type MessageEntry = { type: 'message'; message: { role: string; [k: string]: unknown } }
141
196
  type AssistantMessageShape = {
142
197
  role: 'assistant'
@@ -231,6 +231,179 @@
231
231
  }
232
232
  }
233
233
  },
234
+ "github": {
235
+ "type": "object",
236
+ "properties": {
237
+ "engagement": {
238
+ "default": {
239
+ "trigger": [
240
+ "mention",
241
+ "reply",
242
+ "dm"
243
+ ],
244
+ "stickiness": {
245
+ "perReply": {
246
+ "window": 300000
247
+ }
248
+ }
249
+ },
250
+ "type": "object",
251
+ "properties": {
252
+ "trigger": {
253
+ "default": [
254
+ "mention",
255
+ "reply",
256
+ "dm"
257
+ ],
258
+ "type": "array",
259
+ "items": {
260
+ "type": "string",
261
+ "enum": [
262
+ "mention",
263
+ "reply",
264
+ "dm"
265
+ ]
266
+ }
267
+ },
268
+ "stickiness": {
269
+ "default": {
270
+ "perReply": {
271
+ "window": 300000
272
+ }
273
+ },
274
+ "anyOf": [
275
+ {
276
+ "type": "string",
277
+ "const": "off"
278
+ },
279
+ {
280
+ "type": "object",
281
+ "properties": {
282
+ "perReply": {
283
+ "type": "object",
284
+ "properties": {
285
+ "window": {
286
+ "type": "integer",
287
+ "minimum": 1,
288
+ "maximum": 86400000
289
+ }
290
+ },
291
+ "required": [
292
+ "window"
293
+ ]
294
+ }
295
+ },
296
+ "required": [
297
+ "perReply"
298
+ ]
299
+ }
300
+ ]
301
+ }
302
+ }
303
+ },
304
+ "history": {
305
+ "default": {
306
+ "prefetch": {
307
+ "thread": {
308
+ "head": 3,
309
+ "tail": 10
310
+ },
311
+ "channel": {
312
+ "tail": 10
313
+ }
314
+ }
315
+ },
316
+ "type": "object",
317
+ "properties": {
318
+ "prefetch": {
319
+ "default": {
320
+ "thread": {
321
+ "head": 3,
322
+ "tail": 10
323
+ },
324
+ "channel": {
325
+ "tail": 10
326
+ }
327
+ },
328
+ "type": "object",
329
+ "properties": {
330
+ "thread": {
331
+ "default": {
332
+ "head": 3,
333
+ "tail": 10
334
+ },
335
+ "type": "object",
336
+ "properties": {
337
+ "head": {
338
+ "default": 3,
339
+ "type": "integer",
340
+ "minimum": 0,
341
+ "maximum": 200
342
+ },
343
+ "tail": {
344
+ "default": 10,
345
+ "type": "integer",
346
+ "minimum": 0,
347
+ "maximum": 200
348
+ }
349
+ }
350
+ },
351
+ "channel": {
352
+ "default": {
353
+ "tail": 10
354
+ },
355
+ "type": "object",
356
+ "properties": {
357
+ "tail": {
358
+ "default": 10,
359
+ "type": "integer",
360
+ "minimum": 0,
361
+ "maximum": 200
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }
367
+ }
368
+ },
369
+ "enabled": {
370
+ "default": true,
371
+ "type": "boolean"
372
+ },
373
+ "webhookUrl": {
374
+ "type": "string",
375
+ "format": "uri"
376
+ },
377
+ "webhookPort": {
378
+ "default": 8975,
379
+ "type": "integer",
380
+ "exclusiveMinimum": 0,
381
+ "maximum": 9007199254740991
382
+ },
383
+ "eventAllowlist": {
384
+ "default": [
385
+ "issue_comment.created",
386
+ "pull_request_review_comment.created",
387
+ "discussion_comment.created",
388
+ "issues.opened",
389
+ "pull_request.opened",
390
+ "discussion.created",
391
+ "pull_request_review.submitted"
392
+ ],
393
+ "type": "array",
394
+ "items": {
395
+ "type": "string"
396
+ }
397
+ },
398
+ "repos": {
399
+ "default": [],
400
+ "type": "array",
401
+ "items": {
402
+ "type": "string"
403
+ }
404
+ }
405
+ }
406
+ },
234
407
  "kakaotalk": {
235
408
  "type": "object",
236
409
  "properties": {
@@ -723,6 +896,8 @@
723
896
  "gh": true,
724
897
  "python": true,
725
898
  "tmux": true,
899
+ "cjkFonts": true,
900
+ "cloudflared": true,
726
901
  "append": []
727
902
  }
728
903
  },
@@ -734,6 +909,8 @@
734
909
  "gh": true,
735
910
  "python": true,
736
911
  "tmux": true,
912
+ "cjkFonts": true,
913
+ "cloudflared": true,
737
914
  "append": []
738
915
  },
739
916
  "type": "object",
@@ -778,6 +955,14 @@
778
955
  }
779
956
  ]
780
957
  },
958
+ "cjkFonts": {
959
+ "default": true,
960
+ "type": "boolean"
961
+ },
962
+ "cloudflared": {
963
+ "default": true,
964
+ "type": "boolean"
965
+ },
781
966
  "append": {
782
967
  "default": [],
783
968
  "type": "array",
@@ -826,7 +1011,7 @@
826
1011
  "type": "array",
827
1012
  "items": {
828
1013
  "type": "string",
829
- "pattern": "^(tui|cron|subagent(:[a-z][a-z0-9-]*)?|\\*|(slack|discord|telegram|kakao):[^\\s]+)(\\s+[a-zA-Z][a-zA-Z0-9_]*:[^\\s]+)*$"
1014
+ "pattern": "^(tui|cron|subagent(:[a-z][a-z0-9-]*)?|\\*|(slack|discord|telegram|kakao|github):[^\\s]+)(\\s+[a-zA-Z][a-zA-Z0-9_]*:[^\\s]+)*$"
830
1015
  }
831
1016
  },
832
1017
  "permissions": {
@@ -841,6 +1026,74 @@
841
1026
  "additionalProperties": false
842
1027
  }
843
1028
  },
1029
+ "tunnels": {
1030
+ "default": [],
1031
+ "type": "array",
1032
+ "items": {
1033
+ "type": "object",
1034
+ "properties": {
1035
+ "name": {
1036
+ "type": "string",
1037
+ "minLength": 1,
1038
+ "pattern": "^[a-z0-9][a-z0-9-_]*$"
1039
+ },
1040
+ "provider": {
1041
+ "type": "string",
1042
+ "enum": [
1043
+ "external",
1044
+ "cloudflare-quick"
1045
+ ]
1046
+ },
1047
+ "for": {
1048
+ "oneOf": [
1049
+ {
1050
+ "type": "object",
1051
+ "properties": {
1052
+ "kind": {
1053
+ "type": "string",
1054
+ "const": "channel"
1055
+ },
1056
+ "name": {
1057
+ "type": "string",
1058
+ "minLength": 1
1059
+ }
1060
+ },
1061
+ "required": [
1062
+ "kind",
1063
+ "name"
1064
+ ]
1065
+ },
1066
+ {
1067
+ "type": "object",
1068
+ "properties": {
1069
+ "kind": {
1070
+ "type": "string",
1071
+ "const": "manual"
1072
+ }
1073
+ },
1074
+ "required": [
1075
+ "kind"
1076
+ ]
1077
+ }
1078
+ ]
1079
+ },
1080
+ "externalUrl": {
1081
+ "type": "string",
1082
+ "format": "uri"
1083
+ },
1084
+ "upstreamPort": {
1085
+ "type": "integer",
1086
+ "minimum": 1,
1087
+ "maximum": 65535
1088
+ }
1089
+ },
1090
+ "required": [
1091
+ "name",
1092
+ "provider",
1093
+ "for"
1094
+ ]
1095
+ }
1096
+ },
844
1097
  "tool-result-cap": {
845
1098
  "default": {
846
1099
  "enabled": true,