typeclaw 0.32.1 → 0.34.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 (65) hide show
  1. package/auth.schema.json +66 -0
  2. package/cron.schema.json +26 -2
  3. package/package.json +1 -1
  4. package/secrets.schema.json +66 -0
  5. package/src/agent/index.ts +7 -3
  6. package/src/agent/session-origin.ts +17 -0
  7. package/src/agent/subagent-completion-reminder.ts +14 -1
  8. package/src/agent/subagent-drain.ts +2 -0
  9. package/src/agent/subagents.ts +21 -7
  10. package/src/agent/tools/channel-disengage.ts +66 -0
  11. package/src/agent/tools/channel-log.ts +3 -2
  12. package/src/agent/tools/spawn-subagent.ts +25 -5
  13. package/src/agent/tools/subagent-output.ts +13 -1
  14. package/src/bundled-plugins/guard/policies/managed-config.ts +1 -1
  15. package/src/bundled-plugins/memory/memory-logger.ts +7 -0
  16. package/src/bundled-plugins/researcher/researcher.ts +14 -11
  17. package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +2 -0
  18. package/src/channels/adapters/line-channel-resolver.ts +129 -0
  19. package/src/channels/adapters/line-classify.ts +80 -0
  20. package/src/channels/adapters/line-format.ts +11 -0
  21. package/src/channels/adapters/line.ts +350 -0
  22. package/src/channels/engagement.ts +4 -2
  23. package/src/channels/manager.ts +65 -6
  24. package/src/channels/router.ts +186 -41
  25. package/src/channels/schema.ts +6 -1
  26. package/src/cli/channel.ts +112 -1
  27. package/src/cli/cron.ts +22 -4
  28. package/src/cli/init.ts +267 -82
  29. package/src/cli/model.ts +5 -1
  30. package/src/cli/oauth-callbacks.ts +5 -4
  31. package/src/cli/provider.ts +41 -10
  32. package/src/config/providers.ts +366 -7
  33. package/src/cron/consumer.ts +33 -0
  34. package/src/cron/count-state.ts +208 -0
  35. package/src/cron/index.ts +4 -17
  36. package/src/cron/list.ts +24 -6
  37. package/src/cron/scheduler.ts +84 -9
  38. package/src/cron/schema.ts +100 -13
  39. package/src/doctor/channel-checks.ts +28 -0
  40. package/src/hostd/daemon.ts +14 -6
  41. package/src/hostd/protocol.ts +6 -2
  42. package/src/init/gitignore.ts +1 -1
  43. package/src/init/index.ts +36 -3
  44. package/src/init/line-auth.ts +98 -0
  45. package/src/init/models-dev.ts +3 -0
  46. package/src/init/run-owner-claim.ts +1 -0
  47. package/src/init/validate-api-key.ts +15 -0
  48. package/src/inspect/label.ts +1 -0
  49. package/src/permissions/match-rule.ts +28 -12
  50. package/src/permissions/resolve.ts +8 -1
  51. package/src/role-claim/match-rule.ts +5 -1
  52. package/src/run/index.ts +41 -4
  53. package/src/secrets/line-store.ts +112 -0
  54. package/src/secrets/oauth-xai.ts +342 -0
  55. package/src/secrets/schema.ts +25 -0
  56. package/src/secrets/storage.ts +2 -0
  57. package/src/server/index.ts +17 -4
  58. package/src/shared/protocol.ts +4 -1
  59. package/src/skills/typeclaw-channel-line/SKILL.md +46 -0
  60. package/src/skills/typeclaw-channels/SKILL.md +153 -0
  61. package/src/skills/typeclaw-config/SKILL.md +54 -184
  62. package/src/skills/typeclaw-config/references/dockerfile.md +66 -0
  63. package/src/skills/typeclaw-cron/SKILL.md +68 -14
  64. package/src/skills/typeclaw-permissions/SKILL.md +3 -3
  65. package/typeclaw.schema.json +185 -3
@@ -22,8 +22,10 @@ import {
22
22
  type GithubCredentialPatch,
23
23
  type GithubTunnelProvider,
24
24
  type KakaotalkAuthResult,
25
+ type LineAuthResult,
25
26
  } from '@/init'
26
27
  import { runKakaotalkBootstrap } from '@/init/kakaotalk-auth'
28
+ import { runLineBootstrap } from '@/init/line-auth'
27
29
  import { SecretsKakaoCredentialStore } from '@/secrets/kakao-store'
28
30
 
29
31
  import { CANCEL_SYMBOL, promptPrivateKeyPem } from './prompt-pem'
@@ -33,6 +35,7 @@ const CHANNEL_LABELS: Record<ChannelKind, string> = {
33
35
  'slack-bot': 'Slack',
34
36
  'discord-bot': 'Discord',
35
37
  'telegram-bot': 'Telegram',
38
+ line: 'LINE',
36
39
  kakaotalk: 'KakaoTalk',
37
40
  github: 'GitHub',
38
41
  }
@@ -127,6 +130,15 @@ const setSub = defineCommand({
127
130
  process.exit(1)
128
131
  }
129
132
 
133
+ if (args.adapter === 'line') {
134
+ console.error(
135
+ errorLine(
136
+ 'LINE uses an interactive auth flow (QR or email + PIN). Use `typeclaw channel reauth line` to rotate its credentials.',
137
+ ),
138
+ )
139
+ process.exit(1)
140
+ }
141
+
130
142
  const adapter =
131
143
  args.adapter === undefined
132
144
  ? await pickSettableAdapter(configured)
@@ -138,7 +150,7 @@ const setSub = defineCommand({
138
150
  },
139
151
  })
140
152
 
141
- const REAUTHABLE_ADAPTERS = ['kakaotalk'] as const
153
+ const REAUTHABLE_ADAPTERS = ['line', 'kakaotalk'] as const
142
154
  type ReauthableAdapter = (typeof REAUTHABLE_ADAPTERS)[number]
143
155
 
144
156
  const reauthSub = defineCommand({
@@ -234,12 +246,29 @@ function isReauthableAdapter(value: string): value is ReauthableAdapter {
234
246
 
235
247
  async function runReauth(cwd: string, adapter: ReauthableAdapter): Promise<void> {
236
248
  switch (adapter) {
249
+ case 'line':
250
+ await runLineReauth(cwd)
251
+ return
237
252
  case 'kakaotalk':
238
253
  await runKakaotalkReauth(cwd)
239
254
  return
240
255
  }
241
256
  }
242
257
 
258
+ async function runLineReauth(cwd: string): Promise<void> {
259
+ const login = await promptLineLogin()
260
+ const s = spinner()
261
+ s.start('Logging in to LINE...')
262
+ const result = await runLineBootstrap({ ...login, agentDir: cwd })
263
+ if (!result.ok) {
264
+ s.stop(`LINE login failed: ${result.reason}`)
265
+ process.exit(1)
266
+ }
267
+ s.stop('LINE credentials refreshed in secrets.json.')
268
+
269
+ await maybePromptReauthRefresh(cwd, 'line')
270
+ }
271
+
243
272
  async function runKakaotalkReauth(cwd: string): Promise<void> {
244
273
  const existingEmail = await readExistingKakaotalkEmail(cwd)
245
274
  const creds = await promptKakaotalkCredentials({ defaultEmail: existingEmail })
@@ -566,6 +595,7 @@ type CollectedCredentials =
566
595
  | { channel: 'discord-bot'; discordBotToken: string }
567
596
  | { channel: 'slack-bot'; slackBotToken: string; slackAppToken: string }
568
597
  | { channel: 'telegram-bot'; telegramBotToken: string }
598
+ | { channel: 'line'; runLineAuth: (options: { cwd: string }) => Promise<LineAuthResult> }
569
599
  | { channel: 'kakaotalk'; runKakaotalkAuth: (options: { cwd: string }) => Promise<KakaotalkAuthResult> }
570
600
  | {
571
601
  channel: 'github'
@@ -587,6 +617,13 @@ async function collectCredentials(channel: ChannelKind, cwd: string): Promise<Co
587
617
  }
588
618
  case 'telegram-bot':
589
619
  return { channel, telegramBotToken: await promptTelegramToken() }
620
+ case 'line': {
621
+ const login = await promptLineLogin()
622
+ return {
623
+ channel,
624
+ runLineAuth: ({ cwd: agentDir }) => runLineBootstrap({ ...login, agentDir }),
625
+ }
626
+ }
590
627
  case 'kakaotalk': {
591
628
  const creds = await promptKakaotalkCredentials()
592
629
  return {
@@ -1023,6 +1060,71 @@ async function promptKakaotalkCredentials(
1023
1060
  return { email, password: pwd }
1024
1061
  }
1025
1062
 
1063
+ type LinePromptResult =
1064
+ | { method: 'qr'; callbacks: { onQRUrl: (url: string) => void; onPincode: (pin: string) => void } }
1065
+ | {
1066
+ method: 'email'
1067
+ email: string
1068
+ password: string
1069
+ callbacks: { onPincode: (pin: string) => void }
1070
+ }
1071
+
1072
+ async function promptLineLogin(): Promise<LinePromptResult> {
1073
+ note(
1074
+ [
1075
+ 'LINE authentication uses a personal account registered as a sub-device.',
1076
+ 'Messages will be sent and received under this account — use a',
1077
+ 'non-primary account if possible.',
1078
+ '',
1079
+ 'QR login is recommended: it works even when the account has no',
1080
+ 'email/password set (social-login accounts).',
1081
+ ].join('\n'),
1082
+ 'About to log in to LINE',
1083
+ )
1084
+ const method = await select<'qr' | 'email'>({
1085
+ message: 'How do you want to log in to LINE?',
1086
+ options: [
1087
+ { value: 'qr', label: 'QR code — scan with the LINE app on your phone (recommended)' },
1088
+ { value: 'email', label: 'Email + password — for accounts with email login enabled' },
1089
+ ],
1090
+ initialValue: 'qr',
1091
+ })
1092
+ if (isCancel(method)) {
1093
+ cancel('Aborted.')
1094
+ process.exit(0)
1095
+ }
1096
+
1097
+ const onPincode = (pin: string): void => log.info(`Enter this PIN in the LINE app to confirm: ${pin}`)
1098
+
1099
+ if (method === 'qr') {
1100
+ return {
1101
+ method: 'qr',
1102
+ callbacks: {
1103
+ onQRUrl: (url) => note(url, 'Open this URL on your phone (or scan the QR it renders)'),
1104
+ onPincode,
1105
+ },
1106
+ }
1107
+ }
1108
+
1109
+ const email = await text({
1110
+ message: 'LINE email',
1111
+ validate: (value) => (value && value.length > 0 ? undefined : 'Email is required'),
1112
+ })
1113
+ if (isCancel(email)) {
1114
+ cancel('Aborted.')
1115
+ process.exit(0)
1116
+ }
1117
+ const pwd = await password({
1118
+ message: 'LINE password',
1119
+ validate: (value) => (value && value.length > 0 ? undefined : 'Password is required'),
1120
+ })
1121
+ if (isCancel(pwd)) {
1122
+ cancel('Aborted.')
1123
+ process.exit(0)
1124
+ }
1125
+ return { method: 'email', email, password: pwd, callbacks: { onPincode } }
1126
+ }
1127
+
1026
1128
  function reportProgress(events: AddChannelStepEvent[]): (event: AddChannelStepEvent) => void {
1027
1129
  const spinners: Partial<Record<AddChannelStepEvent['step'], ReturnType<typeof spinner>>> = {}
1028
1130
 
@@ -1039,6 +1141,9 @@ function reportProgress(events: AddChannelStepEvent[]): (event: AddChannelStepEv
1039
1141
  if (!s) return
1040
1142
 
1041
1143
  switch (event.step) {
1144
+ case 'line-auth':
1145
+ s.stop(reportLineAuth(event.result))
1146
+ break
1042
1147
  case 'kakaotalk-auth':
1043
1148
  s.stop(reportKakaotalkAuth(event.result))
1044
1149
  break
@@ -1056,6 +1161,7 @@ function reportProgress(events: AddChannelStepEvent[]): (event: AddChannelStepEv
1056
1161
  }
1057
1162
 
1058
1163
  const START_MESSAGES: Record<AddChannelStepEvent['step'], string> = {
1164
+ 'line-auth': 'Logging in to LINE...',
1059
1165
  'kakaotalk-auth': 'Logging in to KakaoTalk...',
1060
1166
  config: 'Updating typeclaw.json...',
1061
1167
  secrets: 'Saving credentials to secrets.json...',
@@ -1067,6 +1173,11 @@ function reportKakaotalkAuth(result: KakaotalkAuthResult): string {
1067
1173
  return `KakaoTalk login failed: ${result.reason}`
1068
1174
  }
1069
1175
 
1176
+ function reportLineAuth(result: LineAuthResult): string {
1177
+ if (result.ok) return 'LINE credentials saved to secrets.json.'
1178
+ return `LINE login failed: ${result.reason}`
1179
+ }
1180
+
1070
1181
  async function maybePromptRestart(cwd: string, channel: ChannelKind): Promise<void> {
1071
1182
  const label = CHANNEL_LABELS[channel]
1072
1183
  const current = await status({ cwd }).catch(() => null)
package/src/cli/cron.ts CHANGED
@@ -114,12 +114,30 @@ function formatEntry(job: CronListEntryPayload, nowMs: number): string {
114
114
  const kindBadge = c.dim(`[${job.kind}]`)
115
115
  lines.push(`${c.bold(displayId(job))} ${kindBadge} ${sourceLabel}${enabledBadge}`)
116
116
 
117
- const tz = job.timezone !== undefined ? ` ${c.dim(`(${job.timezone})`)}` : ''
118
- lines.push(` ${c.dim('schedule')} ${job.schedule}${tz}`)
117
+ if (job.at !== undefined) {
118
+ lines.push(` ${c.dim('at ')} ${job.at}`)
119
+ } else {
120
+ const tz = job.timezone !== undefined ? ` ${c.dim(`(${job.timezone})`)}` : ''
121
+ lines.push(` ${c.dim('schedule')} ${job.schedule}${tz}`)
122
+ }
123
+
124
+ const stops: string[] = []
125
+ if (job.until !== undefined) stops.push(`until ${job.until}`)
126
+ if (job.count !== undefined) stops.push(`count ${job.count}`)
127
+ if (stops.length > 0) {
128
+ lines.push(` ${c.dim('stops ')} ${stops.join(', ')}`)
129
+ }
119
130
 
120
131
  if (job.nextFireMs === null) {
121
- const why = job.scheduleError !== undefined ? `: ${job.scheduleError}` : ''
122
- lines.push(` ${c.dim('next ')} ${c.red('invalid schedule')}${why}`)
132
+ // A null next-fire means one of two very different things. A parse error
133
+ // carries `scheduleError` and is a problem to surface in red. No error
134
+ // means the job hit its count/until boundary and is simply retired — not
135
+ // broken — so it must not be mislabeled as an invalid schedule.
136
+ if (job.scheduleError !== undefined) {
137
+ lines.push(` ${c.dim('next ')} ${c.red('invalid schedule')}: ${job.scheduleError}`)
138
+ } else {
139
+ lines.push(` ${c.dim('next ')} ${c.dim('retired (boundary reached)')}`)
140
+ }
123
141
  } else {
124
142
  lines.push(` ${c.dim('next ')} ${formatNextFire(job.nextFireMs, nowMs)}`)
125
143
  }