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.
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/agent/index.ts +37 -4
- package/src/agent/multimodal/look-at.ts +8 -0
- package/src/agent/restart-handoff/index.ts +91 -0
- package/src/agent/restart-handoff/paths.ts +11 -0
- package/src/agent/session-origin.ts +30 -10
- package/src/agent/subagent-completion-reminder.ts +4 -2
- package/src/agent/system-prompt.ts +3 -1
- package/src/agent/tools/restart.ts +42 -1
- package/src/agent/tools/skip-response.ts +157 -0
- package/src/bundled-plugins/memory/README.md +18 -2
- package/src/bundled-plugins/memory/index.ts +108 -6
- package/src/bundled-plugins/memory/memory-logger.ts +33 -24
- package/src/bundled-plugins/security/policies/prompt-injection.ts +1 -1
- package/src/channels/adapters/discord-bot-invite.ts +89 -0
- package/src/channels/adapters/github/auth-app.ts +53 -9
- package/src/channels/adapters/github/auth-pat.ts +4 -1
- package/src/channels/adapters/github/auth.ts +10 -0
- package/src/channels/adapters/github/event-permissions.ts +83 -0
- package/src/channels/adapters/github/inbound.ts +126 -1
- package/src/channels/adapters/github/index.ts +60 -66
- package/src/channels/adapters/github/outbound.ts +65 -17
- package/src/channels/adapters/github/permission-guidance.ts +169 -0
- package/src/channels/adapters/github/team-membership.ts +56 -0
- package/src/channels/adapters/kakaotalk-classify.ts +13 -1
- package/src/channels/adapters/kakaotalk.ts +2 -0
- package/src/channels/router.ts +269 -34
- package/src/channels/schema.ts +8 -7
- package/src/channels/types.ts +1 -1
- package/src/cli/channel.ts +138 -52
- package/src/cli/init.ts +139 -100
- package/src/cli/inspect-controller.ts +66 -0
- package/src/cli/inspect.ts +24 -32
- package/src/cli/prompt-pem.ts +113 -0
- package/src/cli/run.ts +24 -5
- package/src/cli/tui.ts +34 -10
- package/src/cli/tunnel.ts +453 -14
- package/src/cli/ui.ts +22 -0
- package/src/compose/discover.ts +5 -0
- package/src/config/config.ts +35 -7
- package/src/config/providers.ts +64 -56
- package/src/init/env-file.ts +66 -0
- package/src/init/hatching.ts +32 -5
- package/src/init/index.ts +131 -39
- package/src/init/validate-api-key.ts +31 -0
- package/src/inspect/index.ts +5 -1
- package/src/inspect/loop.ts +12 -1
- package/src/inspect/replay.ts +15 -1
- package/src/run/codex-fetch-observer.ts +377 -0
- package/src/run/index.ts +14 -2
- package/src/server/command-runner.ts +31 -2
- package/src/server/index.ts +59 -1
- package/src/shared/protocol.ts +1 -1
- package/src/skills/typeclaw-channel-github/SKILL.md +47 -1
- package/src/skills/typeclaw-tunnels/SKILL.md +33 -1
- package/src/tui/index.ts +17 -5
- package/src/tunnels/index.ts +1 -0
- package/src/tunnels/manager.ts +18 -0
- package/src/tunnels/providers/cloudflare-named.ts +224 -0
- package/src/tunnels/types.ts +17 -1
- package/typeclaw.schema.json +25 -7
package/src/cli/channel.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
? '
|
|
522
|
-
: '
|
|
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
|
|
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 —
|
|
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
|
-
|
|
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
|
|
762
|
-
|
|
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
|
|
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
|
|