typeclaw 0.5.0 → 0.5.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/package.json +1 -1
- package/src/cli/channel.ts +2 -45
- package/src/cli/init.ts +2 -45
- package/src/cli/model.ts +2 -1
- package/src/cli/ui.ts +95 -0
- package/src/init/dockerfile.ts +112 -0
- package/src/init/index.ts +33 -0
package/package.json
CHANGED
package/src/cli/channel.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
import { runKakaotalkBootstrap } from '@/init/kakaotalk-auth'
|
|
26
26
|
import { SecretsKakaoCredentialStore } from '@/secrets/kakao-store'
|
|
27
27
|
|
|
28
|
-
import { c, done, errorLine } from './ui'
|
|
28
|
+
import { c, done, errorLine, printSlackAppManifestSetup } from './ui'
|
|
29
29
|
|
|
30
30
|
const CHANNEL_LABELS: Record<ChannelKind, string> = {
|
|
31
31
|
'slack-bot': 'Slack',
|
|
@@ -834,50 +834,7 @@ async function promptDiscordToken(): Promise<string> {
|
|
|
834
834
|
}
|
|
835
835
|
|
|
836
836
|
async function promptSlackTokens(): Promise<{ bot: string; app: string }> {
|
|
837
|
-
|
|
838
|
-
[
|
|
839
|
-
'1. https://api.slack.com/apps → Create New App → From a manifest.',
|
|
840
|
-
' Pick your workspace, then paste this JSON manifest:',
|
|
841
|
-
'',
|
|
842
|
-
' {',
|
|
843
|
-
' "display_information": { "name": "TypeClaw" },',
|
|
844
|
-
' "features": {',
|
|
845
|
-
' "bot_user": { "display_name": "TypeClaw", "always_online": true }',
|
|
846
|
-
' },',
|
|
847
|
-
' "oauth_config": {',
|
|
848
|
-
' "scopes": {',
|
|
849
|
-
' "bot": [',
|
|
850
|
-
' "app_mentions:read", "chat:write", "users:read", "files:read",',
|
|
851
|
-
' "channels:history", "channels:read",',
|
|
852
|
-
' "groups:history", "groups:read",',
|
|
853
|
-
' "im:history", "im:read",',
|
|
854
|
-
' "mpim:history", "mpim:read"',
|
|
855
|
-
' ]',
|
|
856
|
-
' }',
|
|
857
|
-
' },',
|
|
858
|
-
' "settings": {',
|
|
859
|
-
' "event_subscriptions": {',
|
|
860
|
-
' "bot_events": [',
|
|
861
|
-
' "app_mention",',
|
|
862
|
-
' "message.channels", "message.groups",',
|
|
863
|
-
' "message.im", "message.mpim"',
|
|
864
|
-
' ]',
|
|
865
|
-
' },',
|
|
866
|
-
' "socket_mode_enabled": true',
|
|
867
|
-
' }',
|
|
868
|
-
' }',
|
|
869
|
-
'',
|
|
870
|
-
'2. Install to Workspace, then OAuth & Permissions →',
|
|
871
|
-
' copy the Bot User OAuth Token (xoxb-...).',
|
|
872
|
-
'3. Basic Information → App-Level Tokens → Generate Token and',
|
|
873
|
-
' Scopes, add the connections:write scope, and copy the',
|
|
874
|
-
' token (xapp-...). Socket Mode needs this; the manifest',
|
|
875
|
-
' cannot grant it.',
|
|
876
|
-
'4. Invite the bot to any private channel or DM you want it in:',
|
|
877
|
-
' /invite @TypeClaw',
|
|
878
|
-
].join('\n'),
|
|
879
|
-
'Get a Slack bot',
|
|
880
|
-
)
|
|
837
|
+
printSlackAppManifestSetup()
|
|
881
838
|
const bot = await promptSlackBotToken()
|
|
882
839
|
note(
|
|
883
840
|
[
|
package/src/cli/init.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { fetchModelOptions, type ModelOption } from '@/init/models-dev'
|
|
|
32
32
|
import { makeOAuthLoginRunner, type OAuthLoginResult } from '@/init/oauth-login'
|
|
33
33
|
|
|
34
34
|
import { buildOAuthCallbacks } from './oauth-callbacks'
|
|
35
|
-
import { c, done, errorLine } from './ui'
|
|
35
|
+
import { c, done, errorLine, printSlackAppManifestSetup } from './ui'
|
|
36
36
|
|
|
37
37
|
// ESC and Ctrl+C both produce clack's cancel symbol (the keypress layer
|
|
38
38
|
// aliases both to the same "cancel" action — there's no way to tell them
|
|
@@ -1005,50 +1005,7 @@ async function runSlackFlow(): Promise<StepResult<CollectedInputs['channelSecret
|
|
|
1005
1005
|
let sub: SubStep = 'bot'
|
|
1006
1006
|
let botToken: string | undefined
|
|
1007
1007
|
|
|
1008
|
-
|
|
1009
|
-
[
|
|
1010
|
-
'1. https://api.slack.com/apps → Create New App → From a manifest.',
|
|
1011
|
-
' Pick your workspace, then paste this JSON manifest:',
|
|
1012
|
-
'',
|
|
1013
|
-
' {',
|
|
1014
|
-
' "display_information": { "name": "TypeClaw" },',
|
|
1015
|
-
' "features": {',
|
|
1016
|
-
' "bot_user": { "display_name": "TypeClaw", "always_online": true }',
|
|
1017
|
-
' },',
|
|
1018
|
-
' "oauth_config": {',
|
|
1019
|
-
' "scopes": {',
|
|
1020
|
-
' "bot": [',
|
|
1021
|
-
' "app_mentions:read", "chat:write", "users:read", "files:read",',
|
|
1022
|
-
' "channels:history", "channels:read",',
|
|
1023
|
-
' "groups:history", "groups:read",',
|
|
1024
|
-
' "im:history", "im:read",',
|
|
1025
|
-
' "mpim:history", "mpim:read"',
|
|
1026
|
-
' ]',
|
|
1027
|
-
' }',
|
|
1028
|
-
' },',
|
|
1029
|
-
' "settings": {',
|
|
1030
|
-
' "event_subscriptions": {',
|
|
1031
|
-
' "bot_events": [',
|
|
1032
|
-
' "app_mention",',
|
|
1033
|
-
' "message.channels", "message.groups",',
|
|
1034
|
-
' "message.im", "message.mpim"',
|
|
1035
|
-
' ]',
|
|
1036
|
-
' },',
|
|
1037
|
-
' "socket_mode_enabled": true',
|
|
1038
|
-
' }',
|
|
1039
|
-
' }',
|
|
1040
|
-
'',
|
|
1041
|
-
'2. Install to Workspace, then OAuth & Permissions →',
|
|
1042
|
-
' copy the Bot User OAuth Token (xoxb-...).',
|
|
1043
|
-
'3. Basic Information → App-Level Tokens → Generate Token and',
|
|
1044
|
-
' Scopes, add the connections:write scope, and copy the',
|
|
1045
|
-
' token (xapp-...). Socket Mode needs this; the manifest',
|
|
1046
|
-
' cannot grant it.',
|
|
1047
|
-
'4. Invite the bot to any private channel or DM you want it in:',
|
|
1048
|
-
' /invite @TypeClaw',
|
|
1049
|
-
].join('\n'),
|
|
1050
|
-
'Get a Slack bot',
|
|
1051
|
-
)
|
|
1008
|
+
printSlackAppManifestSetup()
|
|
1052
1009
|
|
|
1053
1010
|
while (true) {
|
|
1054
1011
|
if (sub === 'bot') {
|
package/src/cli/model.ts
CHANGED
|
@@ -25,7 +25,7 @@ const ADD_PROVIDER_SENTINEL = '__add-provider__'
|
|
|
25
25
|
const setSub = defineCommand({
|
|
26
26
|
meta: {
|
|
27
27
|
name: 'set',
|
|
28
|
-
description: 'set or update a model profile (default | fast | vision | <custom>)',
|
|
28
|
+
description: 'set or update a model profile (default | fast | deep | vision | <custom>)',
|
|
29
29
|
},
|
|
30
30
|
args: {
|
|
31
31
|
profile: {
|
|
@@ -206,6 +206,7 @@ async function pickProfileName(): Promise<string> {
|
|
|
206
206
|
options: [
|
|
207
207
|
{ value: 'default', label: 'default', hint: 'active model for new sessions' },
|
|
208
208
|
{ value: 'fast', label: 'fast', hint: 'optional alias used by some subagents' },
|
|
209
|
+
{ value: 'deep', label: 'deep', hint: 'optional alias used by some subagents' },
|
|
209
210
|
{ value: 'vision', label: 'vision', hint: 'optional alias used by some subagents' },
|
|
210
211
|
],
|
|
211
212
|
initialValue: 'default',
|
package/src/cli/ui.ts
CHANGED
|
@@ -124,3 +124,98 @@ export function errorLine(reason: string): string {
|
|
|
124
124
|
export function successLine(message: string): string {
|
|
125
125
|
return `${c.green('●')} ${message}`
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
// The exact JSON manifest a user pastes into
|
|
129
|
+
// https://api.slack.com/apps → From a manifest. Kept as a typed object so
|
|
130
|
+
// the file stays a single source of truth and `JSON.stringify` guarantees
|
|
131
|
+
// the rendered text is always valid JSON — no risk of a stray comma or
|
|
132
|
+
// quote slipping in through hand-formatting.
|
|
133
|
+
export const SLACK_APP_MANIFEST = {
|
|
134
|
+
display_information: { name: 'TypeClaw' },
|
|
135
|
+
features: {
|
|
136
|
+
bot_user: { display_name: 'TypeClaw', always_online: true },
|
|
137
|
+
// Enable the Messages tab so users can DM the bot from its app profile,
|
|
138
|
+
// and disable the Home tab — TypeClaw does not publish a custom App Home
|
|
139
|
+
// view, and leaving it enabled would surface an empty default tab.
|
|
140
|
+
app_home: {
|
|
141
|
+
home_tab_enabled: false,
|
|
142
|
+
messages_tab_enabled: true,
|
|
143
|
+
messages_tab_read_only_enabled: false,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
oauth_config: {
|
|
147
|
+
scopes: {
|
|
148
|
+
// Ordered alphabetically so the manifest stays a stable diff target.
|
|
149
|
+
// Read scopes cover every conversation type the agent might observe;
|
|
150
|
+
// write scopes (chat, files, im/mpim/groups, pins, reactions) let the
|
|
151
|
+
// agent post replies, upload attachments, open DMs, pin messages, and
|
|
152
|
+
// react to messages. `channels:join` lets the bot self-join public
|
|
153
|
+
// channels it's invited to discuss in.
|
|
154
|
+
bot: [
|
|
155
|
+
'app_mentions:read',
|
|
156
|
+
'channels:history',
|
|
157
|
+
'channels:join',
|
|
158
|
+
'channels:read',
|
|
159
|
+
'chat:write',
|
|
160
|
+
'emoji:read',
|
|
161
|
+
'files:read',
|
|
162
|
+
'files:write',
|
|
163
|
+
'groups:history',
|
|
164
|
+
'groups:read',
|
|
165
|
+
'groups:write',
|
|
166
|
+
'im:history',
|
|
167
|
+
'im:read',
|
|
168
|
+
'im:write',
|
|
169
|
+
'mpim:history',
|
|
170
|
+
'mpim:read',
|
|
171
|
+
'mpim:write',
|
|
172
|
+
'pins:read',
|
|
173
|
+
'pins:write',
|
|
174
|
+
'reactions:read',
|
|
175
|
+
'reactions:write',
|
|
176
|
+
'users:read',
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
settings: {
|
|
181
|
+
event_subscriptions: {
|
|
182
|
+
bot_events: ['app_mention', 'message.channels', 'message.groups', 'message.im', 'message.mpim'],
|
|
183
|
+
},
|
|
184
|
+
socket_mode_enabled: true,
|
|
185
|
+
},
|
|
186
|
+
} as const
|
|
187
|
+
|
|
188
|
+
// Prints the "create a Slack app from a manifest" walkthrough so the JSON
|
|
189
|
+
// payload is **flush-left and copy-pasteable**. Clack's `note()` wraps
|
|
190
|
+
// content inside a box with `│` borders on both sides, and `log.message()`
|
|
191
|
+
// still prefixes every line with a `│ ` guide column — neither survives a
|
|
192
|
+
// click-and-drag copy. This helper splits the walkthrough into three
|
|
193
|
+
// segments: a boxed prose intro, a raw-stdout JSON block, and a boxed
|
|
194
|
+
// follow-up. The JSON block is emitted via `process.stdout.write` so it
|
|
195
|
+
// carries zero terminal decoration.
|
|
196
|
+
export function printSlackAppManifestSetup(output: NodeJS.WritableStream = process.stdout): void {
|
|
197
|
+
note(
|
|
198
|
+
[
|
|
199
|
+
'1. https://api.slack.com/apps → Create New App → From a manifest.',
|
|
200
|
+
' Pick your workspace, then paste the JSON manifest printed below',
|
|
201
|
+
` (it is rendered flush-left so you can ${c.bold('click-drag and copy')} cleanly).`,
|
|
202
|
+
].join('\n'),
|
|
203
|
+
'Get a Slack bot',
|
|
204
|
+
)
|
|
205
|
+
output.write('\n')
|
|
206
|
+
output.write(`${JSON.stringify(SLACK_APP_MANIFEST, null, 2)}\n`)
|
|
207
|
+
output.write('\n')
|
|
208
|
+
note(
|
|
209
|
+
[
|
|
210
|
+
'2. Install to Workspace, then OAuth & Permissions →',
|
|
211
|
+
' copy the Bot User OAuth Token (xoxb-...).',
|
|
212
|
+
'3. Basic Information → App-Level Tokens → Generate Token and',
|
|
213
|
+
' Scopes, add the connections:write scope, and copy the',
|
|
214
|
+
' token (xapp-...). Socket Mode needs this; the manifest',
|
|
215
|
+
' cannot grant it.',
|
|
216
|
+
'4. Invite the bot to any private channel or DM you want it in:',
|
|
217
|
+
' /invite @TypeClaw',
|
|
218
|
+
].join('\n'),
|
|
219
|
+
'Finish Slack setup',
|
|
220
|
+
)
|
|
221
|
+
}
|
package/src/init/dockerfile.ts
CHANGED
|
@@ -561,6 +561,8 @@ ${LAYER_3_AGENT_BROWSER_ARM64_CONFIG}
|
|
|
561
561
|
|
|
562
562
|
${LAYER_4_AGENT_BROWSER_INSTALL}
|
|
563
563
|
|
|
564
|
+
${LAYER_4_5_AGENT_BROWSER_HEADED_WRAPPER}
|
|
565
|
+
|
|
564
566
|
${LAYER_5_CHROME_FOR_TESTING}
|
|
565
567
|
|
|
566
568
|
${cloudflaredLayer}${renderEntrypointShimLayer()}
|
|
@@ -638,6 +640,8 @@ ${LAYER_3_AGENT_BROWSER_ARM64_CONFIG}
|
|
|
638
640
|
|
|
639
641
|
${LAYER_4_AGENT_BROWSER_INSTALL}
|
|
640
642
|
|
|
643
|
+
${LAYER_4_5_AGENT_BROWSER_HEADED_WRAPPER}
|
|
644
|
+
|
|
641
645
|
${LAYER_5_CHROME_FOR_TESTING}
|
|
642
646
|
|
|
643
647
|
${renderEntrypointShimLayer()}
|
|
@@ -699,6 +703,114 @@ const LAYER_4_AGENT_BROWSER_INSTALL = `# Layer 4 (volatile): install agent-brows
|
|
|
699
703
|
RUN --mount=type=cache,target=/root/.bun/install/cache,sharing=locked \\
|
|
700
704
|
bun install -g agent-browser`
|
|
701
705
|
|
|
706
|
+
// Layer 4.5: shim the agent-browser binary with a wrapper that calls
|
|
707
|
+
// \`agent-browser close\` before \`open\`/\`goto\`/\`navigate\` when headed
|
|
708
|
+
// mode is requested. Works around vercel-labs/agent-browser issue #1083
|
|
709
|
+
// ("headed silently ignored on existing session"): when a daemon is
|
|
710
|
+
// already running with a headless browser, subsequent commands with
|
|
711
|
+
// --headed / AGENT_BROWSER_HEADED reuse the existing headless browser
|
|
712
|
+
// regardless of the requested mode. Three upstream fix PRs (#660, #370,
|
|
713
|
+
// #387) have been open and unmerged for months as of 2026-05, so we
|
|
714
|
+
// patch this locally rather than block on upstream.
|
|
715
|
+
//
|
|
716
|
+
// Allowlist, not denylist. The wrapper only pre-closes on the three
|
|
717
|
+
// commands that explicitly start a new browsing session (\`open\`,
|
|
718
|
+
// \`goto\`, \`navigate\`). Every other agent-browser subcommand — \`click\`,
|
|
719
|
+
// \`snapshot\`, \`chat\`, \`connect\`, \`batch\`, \`tab\`, \`record\`, \`trace\`,
|
|
720
|
+
// \`stream\`, \`cookies\`, \`network\`, ... — passes through untouched.
|
|
721
|
+
// Rationale: those subcommands may operate on the live browser/page
|
|
722
|
+
// state (cookies, in-progress recording, attached external CDP, etc.),
|
|
723
|
+
// and a pre-close from us would silently destroy it. The user-reported
|
|
724
|
+
// scenario for #1083 (\"\`agent-browser open <url> --headed\` after a
|
|
725
|
+
// previous headless invocation\") is fully covered because the
|
|
726
|
+
// follow-up commands inherit the now-headed browser the \`open\`
|
|
727
|
+
// pre-close forced. An earlier draft used a deny-list approach that
|
|
728
|
+
// pre-closed on every non-skip subcommand under headed env; oracle
|
|
729
|
+
// self-review flagged the state-destruction risk for stateful commands,
|
|
730
|
+
// and the allowlist fix is the resulting narrower contract.
|
|
731
|
+
//
|
|
732
|
+
// Truthy contract mirrors upstream's \`env_var_is_truthy\`
|
|
733
|
+
// (cli/src/flags.rs:183): any non-empty value EXCEPT case-insensitive
|
|
734
|
+
// "0" / "false" / "no" counts as truthy. So
|
|
735
|
+
// \`AGENT_BROWSER_HEADED=yes\`, \`=y\`, \`=on\`, \`=anything-non-falsy\` all
|
|
736
|
+
// trigger the workaround — matching what upstream's CLI parser would
|
|
737
|
+
// see — instead of the original narrower 1|true match that left the
|
|
738
|
+
// bug present for legitimate truthy values.
|
|
739
|
+
//
|
|
740
|
+
// Re-entrancy is defended at two layers. (1) The pre-close path is
|
|
741
|
+
// \`open\`/\`goto\`/\`navigate\` only, and the close subcommand isn't in the
|
|
742
|
+
// allowlist, so the pre-close never recurses through the wrapper into
|
|
743
|
+
// another pre-close. (2) \`_TYPECLAW_AGENT_BROWSER_HEADED_HANDLED=1\` is
|
|
744
|
+
// set on the env passed to both the pre-close and the final exec; if a
|
|
745
|
+
// future subcommand we don't recognize shells out to \`agent-browser\` as
|
|
746
|
+
// a subprocess while headed env is still set, the child sees the guard
|
|
747
|
+
// and bypasses straight to .real without recursing.
|
|
748
|
+
const LAYER_4_5_AGENT_BROWSER_HEADED_WRAPPER = `# Layer 4.5 (cheap): wrap agent-browser to work around upstream issue
|
|
749
|
+
# #1083 (--headed / AGENT_BROWSER_HEADED ignored on existing session).
|
|
750
|
+
# See src/init/dockerfile.ts for the full rationale.
|
|
751
|
+
RUN mv /usr/local/bin/agent-browser /usr/local/bin/agent-browser.real \\
|
|
752
|
+
&& cat > /usr/local/bin/agent-browser <<'TYPECLAW_AGENT_BROWSER_WRAPPER_EOF' \\
|
|
753
|
+
&& chmod +x /usr/local/bin/agent-browser
|
|
754
|
+
#!/bin/sh
|
|
755
|
+
# typeclaw wrapper for agent-browser — see src/init/dockerfile.ts.
|
|
756
|
+
set -e
|
|
757
|
+
real="\${TYPECLAW_AGENT_BROWSER_REAL:-/usr/local/bin/agent-browser.real}"
|
|
758
|
+
# Re-entrancy guard: if the wrapper invoked us, skip straight to the real
|
|
759
|
+
# binary. Prevents infinite recursion if a subcommand shells out to
|
|
760
|
+
# agent-browser while AGENT_BROWSER_HEADED is still set.
|
|
761
|
+
if [ "\${_TYPECLAW_AGENT_BROWSER_HEADED_HANDLED:-}" = "1" ]; then
|
|
762
|
+
exec "$real" "$@"
|
|
763
|
+
fi
|
|
764
|
+
# Pre-close is only needed when the caller is requesting headed mode.
|
|
765
|
+
# Match upstream's env_var_is_truthy contract (cli/src/flags.rs:183):
|
|
766
|
+
# truthy = any non-empty value except case-insensitive "0", "false", "no".
|
|
767
|
+
# Argv triggers: bare --headed, --headed=true, --headed=1. (A bare
|
|
768
|
+
# --headed followed by a separate "false" argument is upstream-supported
|
|
769
|
+
# to FORCE headless; the wrapper still pre-closes on the --headed match
|
|
770
|
+
# and the real binary launches headless — wasted close, correct end
|
|
771
|
+
# state. The narrower argv match keeps the wrapper from triggering on
|
|
772
|
+
# unrelated --headed-prefixed flags that may exist in future upstream
|
|
773
|
+
# versions.)
|
|
774
|
+
headed=0
|
|
775
|
+
val=\${AGENT_BROWSER_HEADED:-}
|
|
776
|
+
lower=$(printf '%s' "$val" | tr '[:upper:]' '[:lower:]')
|
|
777
|
+
case "$lower" in
|
|
778
|
+
''|'0'|'false'|'no') ;;
|
|
779
|
+
*) headed=1 ;;
|
|
780
|
+
esac
|
|
781
|
+
for arg in "$@"; do
|
|
782
|
+
case "$arg" in
|
|
783
|
+
--headed|--headed=true|--headed=1) headed=1; break ;;
|
|
784
|
+
esac
|
|
785
|
+
done
|
|
786
|
+
if [ "$headed" != "1" ]; then
|
|
787
|
+
exec "$real" "$@"
|
|
788
|
+
fi
|
|
789
|
+
# Allowlist of commands where pre-close is safe and necessary. Only
|
|
790
|
+
# user-visible "start a new browsing session" verbs go here. Everything
|
|
791
|
+
# else (click, snapshot, chat, connect, batch, tab, record, trace,
|
|
792
|
+
# stream, cookies, ...) may depend on live browser/page state and must
|
|
793
|
+
# not be pre-closed by us.
|
|
794
|
+
first=""
|
|
795
|
+
for arg in "$@"; do
|
|
796
|
+
case "$arg" in
|
|
797
|
+
-*) continue ;;
|
|
798
|
+
*) first="$arg"; break ;;
|
|
799
|
+
esac
|
|
800
|
+
done
|
|
801
|
+
case "$first" in
|
|
802
|
+
open|goto|navigate) ;;
|
|
803
|
+
*) exec "$real" "$@" ;;
|
|
804
|
+
esac
|
|
805
|
+
# Best-effort pre-close. If the daemon is already gone, the real binary
|
|
806
|
+
# prints "No active sessions" and exits 0 — safe to call unconditionally.
|
|
807
|
+
# We discard its output so it never pollutes the caller's stdout/stderr,
|
|
808
|
+
# and we tolerate failures (network blip, stale socket) by falling
|
|
809
|
+
# through to the real command anyway.
|
|
810
|
+
_TYPECLAW_AGENT_BROWSER_HEADED_HANDLED=1 "$real" close >/dev/null 2>&1 || true
|
|
811
|
+
exec env _TYPECLAW_AGENT_BROWSER_HEADED_HANDLED=1 "$real" "$@"
|
|
812
|
+
TYPECLAW_AGENT_BROWSER_WRAPPER_EOF`
|
|
813
|
+
|
|
702
814
|
// Layer 5: download the pinned Chrome for Testing build into
|
|
703
815
|
// ~/.agent-browser/browsers/. NO cache mount on that path because the
|
|
704
816
|
// runtime needs the binary in the image. System shared libraries are
|
package/src/init/index.ts
CHANGED
|
@@ -37,6 +37,14 @@ const CONFIG_FILE = 'typeclaw.json'
|
|
|
37
37
|
const CRON_FILE = 'cron.json'
|
|
38
38
|
const PACKAGE_FILE = 'package.json'
|
|
39
39
|
|
|
40
|
+
// Seeded into `typeclaw.json#roles.member.match[]` whenever a chat adapter
|
|
41
|
+
// (slack-bot, discord-bot, telegram-bot, kakaotalk) is wired. The "*" rule
|
|
42
|
+
// matches every channel session on every platform, so the built-in `member`
|
|
43
|
+
// role (which already carries `channel.respond`) covers any inbound the
|
|
44
|
+
// router sees. Without this, freshly-hatched agents silently drop every
|
|
45
|
+
// chat message — see scaffold() and ensureDefaultChatMemberMatch() below.
|
|
46
|
+
const DEFAULT_CHAT_MEMBER_MATCH_RULE = '*'
|
|
47
|
+
|
|
40
48
|
const MARKDOWN_FILES = ['AGENTS.md', 'IDENTITY.md', 'SOUL.md', 'USER.md'] as const
|
|
41
49
|
|
|
42
50
|
// `packages/` is a bun workspace root (see `workspaces` in buildPackageJson).
|
|
@@ -543,6 +551,11 @@ export async function scaffold(root: string, options: ScaffoldOptions = {}): Pro
|
|
|
543
551
|
if (options.withTelegram) channels['telegram-bot'] = {}
|
|
544
552
|
if (options.withKakaotalk) channels.kakaotalk = {}
|
|
545
553
|
if (Object.keys(channels).length > 0) config.channels = channels
|
|
554
|
+
// See DEFAULT_CHAT_MEMBER_MATCH_RULE for why this is here. GitHub is wired
|
|
555
|
+
// separately (writeGithubChannelForInit) and seeds per-repo member.match
|
|
556
|
+
// entries instead of the wildcard, so a github-only init stays scoped to
|
|
557
|
+
// the repos the operator opted in to.
|
|
558
|
+
if (Object.keys(channels).length > 0) config.roles = { member: { match: [DEFAULT_CHAT_MEMBER_MATCH_RULE] } }
|
|
546
559
|
await writeFile(join(root, CONFIG_FILE), `${JSON.stringify(config, null, 2)}\n`)
|
|
547
560
|
|
|
548
561
|
const cron = {
|
|
@@ -965,6 +978,8 @@ export async function runAddChannel(options: AddChannelOptions): Promise<void> {
|
|
|
965
978
|
if (options.channel === 'github') {
|
|
966
979
|
await appendGithubMatchRules(options.cwd, options.repos)
|
|
967
980
|
await maybeInstallGithubWebhooks(options, emit)
|
|
981
|
+
} else {
|
|
982
|
+
await ensureDefaultChatMemberMatch(options.cwd)
|
|
968
983
|
}
|
|
969
984
|
|
|
970
985
|
// Commit the typeclaw.json change so the agent folder isn't silently
|
|
@@ -1209,6 +1224,24 @@ async function appendGithubMatchRules(cwd: string, repos: readonly string[]): Pr
|
|
|
1209
1224
|
await writeFile(path, `${JSON.stringify(parsed, null, 2)}\n`)
|
|
1210
1225
|
}
|
|
1211
1226
|
|
|
1227
|
+
// Chat-adapter counterpart of appendGithubMatchRules. See
|
|
1228
|
+
// DEFAULT_CHAT_MEMBER_MATCH_RULE for the rationale. Set-union semantics: re-
|
|
1229
|
+
// running `typeclaw channel add` for additional chat adapters is a no-op on
|
|
1230
|
+
// the match list, and any pre-existing rules the operator hand-authored
|
|
1231
|
+
// (e.g. owner-claim's per-author entry on `owner`) are left intact.
|
|
1232
|
+
async function ensureDefaultChatMemberMatch(cwd: string): Promise<void> {
|
|
1233
|
+
const path = join(cwd, CONFIG_FILE)
|
|
1234
|
+
const parsed = JSON.parse(await readFile(path, 'utf8')) as Record<string, unknown>
|
|
1235
|
+
const roles = isObjectRecord(parsed.roles) ? { ...parsed.roles } : {}
|
|
1236
|
+
const member = isObjectRecord(roles.member) ? { ...roles.member } : {}
|
|
1237
|
+
const existing = Array.isArray(member.match) ? member.match.filter((v): v is string => typeof v === 'string') : []
|
|
1238
|
+
if (existing.includes(DEFAULT_CHAT_MEMBER_MATCH_RULE)) return
|
|
1239
|
+
member.match = [...existing, DEFAULT_CHAT_MEMBER_MATCH_RULE]
|
|
1240
|
+
roles.member = member
|
|
1241
|
+
parsed.roles = roles
|
|
1242
|
+
await writeFile(path, `${JSON.stringify(parsed, null, 2)}\n`)
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1212
1245
|
// Writes per-adapter field values into `secrets.json#channels.<adapter>`.
|
|
1213
1246
|
// Refuses to overwrite existing fields: if the user already has e.g.
|
|
1214
1247
|
// `botToken` recorded (from a prior `channel add` whose follow-up steps
|