typeclaw 0.10.0 → 0.11.1

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 (62) hide show
  1. package/README.md +5 -1
  2. package/package.json +1 -1
  3. package/src/agent/index.ts +37 -4
  4. package/src/agent/multimodal/look-at.ts +8 -0
  5. package/src/agent/restart-handoff/index.ts +91 -0
  6. package/src/agent/restart-handoff/paths.ts +11 -0
  7. package/src/agent/session-origin.ts +30 -10
  8. package/src/agent/subagent-completion-reminder.ts +4 -2
  9. package/src/agent/system-prompt.ts +3 -1
  10. package/src/agent/tools/restart.ts +42 -1
  11. package/src/agent/tools/skip-response.ts +157 -0
  12. package/src/bundled-plugins/memory/README.md +18 -2
  13. package/src/bundled-plugins/memory/index.ts +108 -6
  14. package/src/bundled-plugins/memory/memory-logger.ts +33 -24
  15. package/src/bundled-plugins/security/policies/prompt-injection.ts +1 -1
  16. package/src/channels/adapters/discord-bot-invite.ts +89 -0
  17. package/src/channels/adapters/github/auth-app.ts +53 -9
  18. package/src/channels/adapters/github/auth-pat.ts +4 -1
  19. package/src/channels/adapters/github/auth.ts +10 -0
  20. package/src/channels/adapters/github/event-permissions.ts +83 -0
  21. package/src/channels/adapters/github/inbound.ts +126 -1
  22. package/src/channels/adapters/github/index.ts +60 -66
  23. package/src/channels/adapters/github/outbound.ts +65 -17
  24. package/src/channels/adapters/github/permission-guidance.ts +169 -0
  25. package/src/channels/adapters/github/team-membership.ts +56 -0
  26. package/src/channels/adapters/kakaotalk-classify.ts +13 -1
  27. package/src/channels/adapters/kakaotalk.ts +2 -0
  28. package/src/channels/router.ts +269 -34
  29. package/src/channels/schema.ts +8 -7
  30. package/src/channels/types.ts +1 -1
  31. package/src/cli/channel.ts +138 -52
  32. package/src/cli/init.ts +139 -100
  33. package/src/cli/inspect-controller.ts +66 -0
  34. package/src/cli/inspect.ts +24 -32
  35. package/src/cli/prompt-pem.ts +113 -0
  36. package/src/cli/run.ts +24 -5
  37. package/src/cli/tui.ts +34 -10
  38. package/src/cli/tunnel.ts +453 -14
  39. package/src/cli/ui.ts +22 -0
  40. package/src/compose/discover.ts +5 -0
  41. package/src/config/config.ts +35 -7
  42. package/src/config/providers.ts +64 -56
  43. package/src/init/env-file.ts +66 -0
  44. package/src/init/hatching.ts +32 -5
  45. package/src/init/index.ts +131 -39
  46. package/src/init/validate-api-key.ts +31 -0
  47. package/src/inspect/index.ts +5 -1
  48. package/src/inspect/loop.ts +12 -1
  49. package/src/inspect/replay.ts +15 -1
  50. package/src/run/codex-fetch-observer.ts +377 -0
  51. package/src/run/index.ts +14 -2
  52. package/src/server/command-runner.ts +31 -2
  53. package/src/server/index.ts +59 -1
  54. package/src/shared/protocol.ts +1 -1
  55. package/src/skills/typeclaw-channel-github/SKILL.md +47 -1
  56. package/src/skills/typeclaw-tunnels/SKILL.md +33 -1
  57. package/src/tui/index.ts +17 -5
  58. package/src/tunnels/index.ts +1 -0
  59. package/src/tunnels/manager.ts +18 -0
  60. package/src/tunnels/providers/cloudflare-named.ts +224 -0
  61. package/src/tunnels/types.ts +17 -1
  62. package/typeclaw.schema.json +25 -7
@@ -1,5 +1,4 @@
1
1
  import { randomBytes } from 'node:crypto'
2
- import { readFile } from 'node:fs/promises'
3
2
 
4
3
  import { cancel, confirm, intro, isCancel, log, note, password, select, spinner, text } from '@clack/prompts'
5
4
  import { defineCommand } from 'citty'
@@ -8,8 +7,10 @@ import { config } from '@/config'
8
7
  import { start, status, stop } from '@/container'
9
8
  import {
10
9
  CHANNEL_KINDS,
10
+ appendOrReplaceEnvKey,
11
11
  findAgentDir,
12
12
  formatEagerGithubWebhookInstallResult,
13
+ hasEnvKey,
13
14
  isInitialized,
14
15
  readConfiguredChannels,
15
16
  readGithubAuthType,
@@ -25,7 +26,8 @@ import {
25
26
  import { runKakaotalkBootstrap } from '@/init/kakaotalk-auth'
26
27
  import { SecretsKakaoCredentialStore } from '@/secrets/kakao-store'
27
28
 
28
- import { c, done, errorLine, printSlackAppManifestSetup } from './ui'
29
+ import { CANCEL_SYMBOL, promptPrivateKeyPem } from './prompt-pem'
30
+ import { c, done, errorLine, printDiscordInviteHint, printSlackAppManifestSetup } from './ui'
29
31
 
30
32
  const CHANNEL_LABELS: Record<ChannelKind, string> = {
31
33
  'slack-bot': 'Slack',
@@ -61,7 +63,7 @@ const addSub = defineCommand({
61
63
 
62
64
  intro(`Adding channel: ${CHANNEL_LABELS[channel]}`)
63
65
 
64
- const credentials = await collectCredentials(channel)
66
+ const credentials = await collectCredentials(channel, cwd)
65
67
 
66
68
  const events: AddChannelStepEvent[] = []
67
69
  try {
@@ -518,14 +520,14 @@ async function runSetGithub(cwd: string): Promise<void> {
518
520
  }
519
521
  const authLabel =
520
522
  authType === 'pat'
521
- ? 'Personal access token (PAT) the authentication credential (recommended)'
522
- : 'GitHub App private key the authentication credential (recommended)'
523
+ ? 'Auth credential rotate the PAT, or switch to GitHub App auth (recommended)'
524
+ : 'Auth credential — rotate the App private key, or switch to PAT auth (recommended)'
523
525
  const choice = await select<GithubSetChoice>({
524
- message: 'Which GitHub secret do you want to rotate?',
526
+ message: 'Which GitHub secret do you want to update?',
525
527
  options: [
526
528
  { value: 'auth', label: authLabel },
527
529
  { value: 'webhook', label: 'Webhook secret — shared secret for verifying GitHub payloads' },
528
- { value: 'both', label: 'Both secrets — rotate the auth credential and the webhook secret' },
530
+ { value: 'both', label: 'Both secrets — update the auth credential and the webhook secret' },
529
531
  ],
530
532
  initialValue: 'auth',
531
533
  })
@@ -537,36 +539,7 @@ async function runSetGithub(cwd: string): Promise<void> {
537
539
  const patch: GithubCredentialPatch = {}
538
540
 
539
541
  if (choice === 'auth' || choice === 'both') {
540
- if (authType === 'pat') {
541
- note(
542
- [
543
- 'Rotate at https://github.com/settings/personal-access-tokens.',
544
- 'Required permissions: Issues read/write, Pull requests read/write, Discussions read/write (if used),',
545
- 'Metadata read, and Webhooks read/write.',
546
- ].join('\n'),
547
- 'Rotate the GitHub PAT',
548
- )
549
- const { pat } = await promptGithubPatAuth()
550
- patch.auth = { type: 'pat', pat }
551
- } else {
552
- note(
553
- [
554
- 'Rotate at https://github.com/settings/apps/<your-app> → Private keys → Generate a private key.',
555
- 'GitHub immediately downloads the new .pem. The previous key keeps working until you delete it,',
556
- 'so it is safe to rotate without downtime.',
557
- ].join('\n'),
558
- 'Rotate the GitHub App private key',
559
- )
560
- const privateKeyInput = await text({
561
- message: 'New GitHub App private key PEM, escaped PEM, or path to .pem file',
562
- validate: (value) => (value && value.length > 0 ? undefined : 'Private key is required'),
563
- })
564
- if (isCancel(privateKeyInput)) {
565
- cancel('Aborted.')
566
- process.exit(0)
567
- }
568
- patch.auth = { type: 'app', privateKey: await resolvePrivateKeyInput(privateKeyInput) }
569
- }
542
+ patch.auth = await promptGithubAuthUpdate(authType)
570
543
  }
571
544
 
572
545
  if (choice === 'webhook' || choice === 'both') {
@@ -604,7 +577,7 @@ type CollectedCredentials =
604
577
  auth: { type: 'pat'; pat: string } | { type: 'app'; appId: number; privateKey: string; installationId?: number }
605
578
  }
606
579
 
607
- async function collectCredentials(channel: ChannelKind): Promise<CollectedCredentials> {
580
+ async function collectCredentials(channel: ChannelKind, cwd: string): Promise<CollectedCredentials> {
608
581
  switch (channel) {
609
582
  case 'discord-bot':
610
583
  return { channel, discordBotToken: await promptDiscordToken() }
@@ -628,17 +601,19 @@ async function collectCredentials(channel: ChannelKind): Promise<CollectedCreden
628
601
  }
629
602
  }
630
603
  case 'github': {
631
- const creds = await promptGithubCredentials()
604
+ const creds = await promptGithubCredentials(cwd)
632
605
  return { channel, ...creds }
633
606
  }
634
607
  }
635
608
  }
636
609
 
637
- async function promptGithubCredentials(): Promise<{
610
+ async function promptGithubCredentials(cwd: string): Promise<{
638
611
  webhookSecret: string
639
612
  tunnelProvider: GithubTunnelProvider
640
613
  webhookUrl?: string
641
614
  webhookPort?: number
615
+ hostname?: string
616
+ tokenEnv?: string
642
617
  repos: string[]
643
618
  auth: { type: 'pat'; pat: string } | { type: 'app'; appId: number; privateKey: string; installationId?: number }
644
619
  }> {
@@ -670,6 +645,10 @@ async function promptGithubCredentials(): Promise<{
670
645
  value: 'cloudflare-quick',
671
646
  label: 'Cloudflare Quick Tunnel — no signup, URL rotates on restart (recommended)',
672
647
  },
648
+ {
649
+ value: 'cloudflare-named',
650
+ label: 'Cloudflare Named Tunnel — stable URL, needs Cloudflare account + domain',
651
+ },
673
652
  { value: 'external', label: 'External URL — I have my own reverse proxy / tunnel' },
674
653
  { value: 'none', label: 'None — configure later by hand-editing typeclaw.json' },
675
654
  ],
@@ -690,6 +669,7 @@ async function promptGithubCredentials(): Promise<{
690
669
  cancel('Aborted.')
691
670
  process.exit(0)
692
671
  }
672
+ const namedCreds = tunnelProvider === 'cloudflare-named' ? await promptCloudflareNamedTunnel(cwd) : undefined
693
673
  const port = await text({
694
674
  message: 'Local webhook port inside the agent container',
695
675
  initialValue: '8975',
@@ -727,11 +707,125 @@ async function promptGithubCredentials(): Promise<{
727
707
  tunnelProvider,
728
708
  ...(webhookUrl !== undefined ? { webhookUrl } : {}),
729
709
  webhookPort: Number(port),
710
+ ...(namedCreds !== undefined ? namedCreds : {}),
730
711
  repos: parseRepos(reposRaw),
731
712
  auth,
732
713
  }
733
714
  }
734
715
 
716
+ async function promptCloudflareNamedTunnel(cwd: string): Promise<{ hostname: string; tokenEnv: string }> {
717
+ const tokenEnv = 'CLOUDFLARE_TUNNEL_TOKEN'
718
+ note(
719
+ [
720
+ 'Cloudflare Named Tunnel needs a tunnel you created in the Zero Trust dashboard:',
721
+ ' 1. Networks → Tunnels → Create a tunnel → Cloudflared. Copy the token shown on the install screen.',
722
+ ' 2. Public Hostname tab → Add: subdomain + your-domain, service type HTTP, URL localhost:<webhook port>.',
723
+ ` 3. Paste the token below when prompted — TypeClaw will write it to .env as ${tokenEnv}.`,
724
+ 'A tunnel without a Public Hostname registers but routes nothing.',
725
+ ].join('\n'),
726
+ 'Cloudflare named tunnel',
727
+ )
728
+ const hostname = await text({
729
+ message: 'Public hostname configured in the dashboard (https://...)',
730
+ validate: (value) => validateUrl(value ?? '', 'Hostname is required'),
731
+ })
732
+ if (isCancel(hostname)) {
733
+ cancel('Aborted.')
734
+ process.exit(0)
735
+ }
736
+ if (!hasEnvKey(cwd, tokenEnv)) {
737
+ const token = await password({
738
+ message: `Cloudflare tunnel token (will be written to .env as ${tokenEnv})`,
739
+ validate: (value) => (value && value.length > 0 ? undefined : 'Token is required'),
740
+ })
741
+ if (isCancel(token)) {
742
+ cancel('Aborted.')
743
+ process.exit(0)
744
+ }
745
+ appendOrReplaceEnvKey(cwd, tokenEnv, token)
746
+ }
747
+ return { hostname, tokenEnv }
748
+ }
749
+
750
+ type GithubAuthUpdateAction = 'rotate' | 'switch'
751
+
752
+ async function promptGithubAuthUpdate(currentType: 'pat' | 'app'): Promise<GithubCredentialPatch['auth']> {
753
+ const rotateLabel =
754
+ currentType === 'pat'
755
+ ? 'Rotate the PAT (replace the current personal access token)'
756
+ : 'Rotate the App private key (replace the current GitHub App private key)'
757
+ const switchLabel =
758
+ currentType === 'pat'
759
+ ? 'Switch to GitHub App auth (replace the PAT with App credentials)'
760
+ : 'Switch to PAT auth (replace the App credentials with a personal access token)'
761
+ const action = await select<GithubAuthUpdateAction>({
762
+ message: 'Update the GitHub auth credential',
763
+ options: [
764
+ { value: 'rotate', label: rotateLabel },
765
+ { value: 'switch', label: switchLabel },
766
+ ],
767
+ initialValue: 'rotate',
768
+ })
769
+ if (isCancel(action)) {
770
+ cancel('Aborted.')
771
+ process.exit(0)
772
+ }
773
+
774
+ const nextType: 'pat' | 'app' = action === 'rotate' ? currentType : currentType === 'pat' ? 'app' : 'pat'
775
+
776
+ if (nextType === 'pat') {
777
+ if (action === 'rotate') {
778
+ note(
779
+ [
780
+ 'Rotate at https://github.com/settings/personal-access-tokens.',
781
+ 'Required permissions: Issues read/write, Pull requests read/write, Discussions read/write (if used),',
782
+ 'Metadata read, and Webhooks read/write.',
783
+ ].join('\n'),
784
+ 'Rotate the GitHub PAT',
785
+ )
786
+ } else {
787
+ note(
788
+ [
789
+ 'Create a fine-grained PAT at https://github.com/settings/personal-access-tokens.',
790
+ 'Required permissions: Issues read/write, Pull requests read/write, Discussions read/write (if used),',
791
+ 'Metadata read, and Webhooks read/write.',
792
+ ].join('\n'),
793
+ 'Switch to GitHub PAT auth',
794
+ )
795
+ }
796
+ return await promptGithubPatAuth()
797
+ }
798
+
799
+ if (action === 'rotate') {
800
+ note(
801
+ [
802
+ 'Rotate at https://github.com/settings/apps/<your-app> → Private keys → Generate a private key.',
803
+ 'GitHub immediately downloads the new .pem. The previous key keeps working until you delete it,',
804
+ 'so it is safe to rotate without downtime.',
805
+ ].join('\n'),
806
+ 'Rotate the GitHub App private key',
807
+ )
808
+ const privateKey = await promptPrivateKeyPem('New GitHub App private key PEM, escaped PEM, or path to .pem file')
809
+ if (privateKey === CANCEL_SYMBOL) {
810
+ cancel('Aborted.')
811
+ process.exit(0)
812
+ }
813
+ return { type: 'app', privateKey }
814
+ }
815
+
816
+ note(
817
+ [
818
+ 'Create a GitHub App at https://github.com/settings/apps/new and install it on your repositories.',
819
+ 'Required permissions: Issues read/write, Pull requests read/write, Discussions read/write (if used),',
820
+ 'Metadata read, and Webhooks read/write.',
821
+ 'Then collect the App ID, generate a private key (.pem), and grab the Installation ID from the URL',
822
+ 'of the installation page (https://github.com/settings/installations/<installation-id>).',
823
+ ].join('\n'),
824
+ 'Switch to GitHub App auth',
825
+ )
826
+ return await promptGithubAppAuth()
827
+ }
828
+
735
829
  async function promptGithubPatAuth(): Promise<{ type: 'pat'; pat: string }> {
736
830
  const pat = await password({
737
831
  message: 'GitHub fine-grained PAT',
@@ -758,11 +852,8 @@ async function promptGithubAppAuth(): Promise<{
758
852
  cancel('Aborted.')
759
853
  process.exit(0)
760
854
  }
761
- const privateKeyInput = await text({
762
- message: 'GitHub App private key PEM, escaped PEM, or path to .pem file',
763
- validate: (value) => (value && value.length > 0 ? undefined : 'Private key is required'),
764
- })
765
- if (isCancel(privateKeyInput)) {
855
+ const privateKey = await promptPrivateKeyPem('GitHub App private key PEM, escaped PEM, or path to .pem file')
856
+ if (privateKey === CANCEL_SYMBOL) {
766
857
  cancel('Aborted.')
767
858
  process.exit(0)
768
859
  }
@@ -779,17 +870,11 @@ async function promptGithubAppAuth(): Promise<{
779
870
  return {
780
871
  type: 'app',
781
872
  appId: Number(appId),
782
- privateKey: await resolvePrivateKeyInput(privateKeyInput),
873
+ privateKey,
783
874
  ...(parsedInstallationId !== undefined ? { installationId: parsedInstallationId } : {}),
784
875
  }
785
876
  }
786
877
 
787
- async function resolvePrivateKeyInput(input: string): Promise<string> {
788
- const normalized = input.replace(/\\n/g, '\n')
789
- if (normalized.includes('-----BEGIN') && normalized.includes('PRIVATE KEY-----')) return normalized
790
- return await readFile(input, 'utf8')
791
- }
792
-
793
878
  function parseRepos(input: string): string[] {
794
879
  return input
795
880
  .split(',')
@@ -830,6 +915,7 @@ async function promptDiscordToken(): Promise<string> {
830
915
  cancel('Aborted.')
831
916
  process.exit(0)
832
917
  }
918
+ printDiscordInviteHint(token)
833
919
  return token
834
920
  }
835
921