typeclaw 0.1.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.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/auth.schema.json +63 -0
- package/cron.schema.json +96 -0
- package/package.json +72 -0
- package/scripts/emit-base-dockerfile.ts +5 -0
- package/scripts/generate-schema.ts +34 -0
- package/secrets.schema.json +63 -0
- package/src/agent/auth.ts +119 -0
- package/src/agent/compaction.ts +35 -0
- package/src/agent/git-nudge.ts +95 -0
- package/src/agent/index.ts +451 -0
- package/src/agent/plugin-tools.ts +269 -0
- package/src/agent/reload-tool.ts +71 -0
- package/src/agent/self.ts +45 -0
- package/src/agent/session-origin.ts +288 -0
- package/src/agent/subagents.ts +253 -0
- package/src/agent/system-prompt.ts +68 -0
- package/src/agent/tools/channel-fetch-attachment.ts +118 -0
- package/src/agent/tools/channel-history.ts +119 -0
- package/src/agent/tools/channel-reply.ts +182 -0
- package/src/agent/tools/channel-send.ts +212 -0
- package/src/agent/tools/ddg.ts +218 -0
- package/src/agent/tools/restart.ts +122 -0
- package/src/agent/tools/stream-snapshot.ts +181 -0
- package/src/agent/tools/webfetch/fetch.ts +102 -0
- package/src/agent/tools/webfetch/index.ts +1 -0
- package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
- package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
- package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
- package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
- package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
- package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
- package/src/agent/tools/webfetch/tool.ts +281 -0
- package/src/agent/tools/webfetch/types.ts +33 -0
- package/src/agent/tools/websearch.ts +96 -0
- package/src/agent/tools/wikipedia.ts +52 -0
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
- package/src/bundled-plugins/agent-browser/index.ts +179 -0
- package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
- package/src/bundled-plugins/agent-browser/shim.ts +152 -0
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
- package/src/bundled-plugins/guard/index.ts +26 -0
- package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
- package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
- package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
- package/src/bundled-plugins/guard/policy.ts +18 -0
- package/src/bundled-plugins/memory/README.md +71 -0
- package/src/bundled-plugins/memory/append-tool.ts +84 -0
- package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
- package/src/bundled-plugins/memory/dreaming.ts +470 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
- package/src/bundled-plugins/memory/index.ts +238 -0
- package/src/bundled-plugins/memory/load-memory.ts +122 -0
- package/src/bundled-plugins/memory/memory-logger.ts +257 -0
- package/src/bundled-plugins/memory/secret-detector.ts +49 -0
- package/src/bundled-plugins/memory/watermark.ts +15 -0
- package/src/bundled-plugins/security/index.ts +35 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
- package/src/bundled-plugins/security/policy.ts +9 -0
- package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
- package/src/channels/adapters/discord-bot-classify.ts +148 -0
- package/src/channels/adapters/discord-bot.ts +640 -0
- package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
- package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
- package/src/channels/adapters/kakaotalk-classify.ts +77 -0
- package/src/channels/adapters/kakaotalk.ts +622 -0
- package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
- package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
- package/src/channels/adapters/slack-bot-classify.ts +213 -0
- package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
- package/src/channels/adapters/slack-bot-time.ts +10 -0
- package/src/channels/adapters/slack-bot.ts +881 -0
- package/src/channels/adapters/telegram-bot-classify.ts +155 -0
- package/src/channels/adapters/telegram-bot-format.ts +309 -0
- package/src/channels/adapters/telegram-bot.ts +604 -0
- package/src/channels/engagement.ts +227 -0
- package/src/channels/index.ts +21 -0
- package/src/channels/manager.ts +292 -0
- package/src/channels/membership-cache.ts +116 -0
- package/src/channels/membership-from-history.ts +53 -0
- package/src/channels/membership.ts +30 -0
- package/src/channels/participants.ts +47 -0
- package/src/channels/persistence.ts +209 -0
- package/src/channels/reloadable.ts +28 -0
- package/src/channels/router.ts +1570 -0
- package/src/channels/schema.ts +273 -0
- package/src/channels/types.ts +160 -0
- package/src/cli/channel.ts +403 -0
- package/src/cli/compose-status.ts +95 -0
- package/src/cli/compose.ts +240 -0
- package/src/cli/hostd.ts +163 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/init.ts +592 -0
- package/src/cli/logs.ts +38 -0
- package/src/cli/reload.ts +68 -0
- package/src/cli/restart.ts +66 -0
- package/src/cli/run.ts +77 -0
- package/src/cli/shell.ts +33 -0
- package/src/cli/start.ts +57 -0
- package/src/cli/status.ts +178 -0
- package/src/cli/stop.ts +31 -0
- package/src/cli/tui.ts +35 -0
- package/src/cli/ui.ts +110 -0
- package/src/commands/index.ts +74 -0
- package/src/compose/discover.ts +43 -0
- package/src/compose/index.ts +25 -0
- package/src/compose/logs.ts +162 -0
- package/src/compose/restart.ts +69 -0
- package/src/compose/start.ts +62 -0
- package/src/compose/status.ts +28 -0
- package/src/compose/stop.ts +43 -0
- package/src/config/config.ts +424 -0
- package/src/config/index.ts +25 -0
- package/src/config/providers.ts +234 -0
- package/src/config/reloadable.ts +47 -0
- package/src/container/index.ts +27 -0
- package/src/container/logs.ts +37 -0
- package/src/container/port.ts +137 -0
- package/src/container/shared.ts +290 -0
- package/src/container/shell.ts +58 -0
- package/src/container/start.ts +670 -0
- package/src/container/status.ts +76 -0
- package/src/container/stop.ts +120 -0
- package/src/container/verify-running.ts +149 -0
- package/src/cron/consumer.ts +138 -0
- package/src/cron/index.ts +54 -0
- package/src/cron/reloadable.ts +64 -0
- package/src/cron/scheduler.ts +200 -0
- package/src/cron/schema.ts +96 -0
- package/src/hostd/client.ts +113 -0
- package/src/hostd/daemon.ts +587 -0
- package/src/hostd/index.ts +25 -0
- package/src/hostd/paths.ts +82 -0
- package/src/hostd/portbroker-manager.ts +101 -0
- package/src/hostd/protocol.ts +48 -0
- package/src/hostd/spawn.ts +224 -0
- package/src/hostd/supervisor.ts +60 -0
- package/src/hostd/tailscale.ts +172 -0
- package/src/hostd/version.ts +115 -0
- package/src/init/dockerfile.ts +327 -0
- package/src/init/ensure-deps.ts +152 -0
- package/src/init/gitignore.ts +46 -0
- package/src/init/hatching.ts +60 -0
- package/src/init/index.ts +786 -0
- package/src/init/kakaotalk-auth.ts +114 -0
- package/src/init/models-dev.ts +130 -0
- package/src/init/oauth-login.ts +74 -0
- package/src/init/packagejson.ts +94 -0
- package/src/init/paths.ts +2 -0
- package/src/init/run-bun-install.ts +20 -0
- package/src/markdown/chunk.ts +299 -0
- package/src/markdown/index.ts +1 -0
- package/src/plugin/context.ts +40 -0
- package/src/plugin/define.ts +35 -0
- package/src/plugin/hooks.ts +204 -0
- package/src/plugin/index.ts +63 -0
- package/src/plugin/loader.ts +111 -0
- package/src/plugin/manager.ts +136 -0
- package/src/plugin/registry.ts +145 -0
- package/src/plugin/skills.ts +62 -0
- package/src/plugin/types.ts +172 -0
- package/src/portbroker/bind-with-forward.ts +102 -0
- package/src/portbroker/container-server.ts +305 -0
- package/src/portbroker/forward-result-bus.ts +36 -0
- package/src/portbroker/hostd-client.ts +443 -0
- package/src/portbroker/index.ts +33 -0
- package/src/portbroker/policy.ts +24 -0
- package/src/portbroker/proc-net-tcp.ts +72 -0
- package/src/portbroker/protocol.ts +39 -0
- package/src/reload/client.ts +59 -0
- package/src/reload/index.ts +3 -0
- package/src/reload/registry.ts +60 -0
- package/src/reload/types.ts +13 -0
- package/src/run/bundled-plugins.ts +24 -0
- package/src/run/channel-session-factory.ts +105 -0
- package/src/run/index.ts +432 -0
- package/src/run/plugin-runtime.ts +43 -0
- package/src/run/schema-with-plugins.ts +14 -0
- package/src/secrets/index.ts +13 -0
- package/src/secrets/migrate.ts +95 -0
- package/src/secrets/schema.ts +75 -0
- package/src/secrets/storage.ts +231 -0
- package/src/server/index.ts +436 -0
- package/src/sessions/index.ts +23 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/local-time.ts +21 -0
- package/src/shared/protocol.ts +25 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
- package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
- package/src/skills/typeclaw-config/SKILL.md +643 -0
- package/src/skills/typeclaw-cron/SKILL.md +159 -0
- package/src/skills/typeclaw-git/SKILL.md +89 -0
- package/src/skills/typeclaw-memory/SKILL.md +174 -0
- package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
- package/src/skills/typeclaw-plugins/SKILL.md +594 -0
- package/src/skills/typeclaw-skills/SKILL.md +246 -0
- package/src/stream/broker.ts +161 -0
- package/src/stream/index.ts +16 -0
- package/src/stream/types.ts +69 -0
- package/src/tui/client.ts +45 -0
- package/src/tui/format.ts +317 -0
- package/src/tui/index.ts +225 -0
- package/src/tui/theme.ts +41 -0
- package/typeclaw.schema.json +826 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const ADAPTER_IDS = ['discord-bot', 'kakaotalk', 'slack-bot', 'telegram-bot'] as const
|
|
4
|
+
|
|
5
|
+
export type AdapterId = (typeof ADAPTER_IDS)[number]
|
|
6
|
+
|
|
7
|
+
const allowRuleSchema = z.string().min(1).refine(isValidAllowRule, {
|
|
8
|
+
message:
|
|
9
|
+
'allow rule must be one of: *, guild:*, guild:<id>, guild:<id>/<channel>, team:*, team:<id>, team:<id>/<channel>, tg:*, tg:<chat_id>, channel:<id>, dm:*, dm:<id>, im:*, im:<id>, kakao:*, kakao:<chat>, kakao:dm/*, kakao:group/*, kakao:open/*',
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const engagementTriggerSchema = z.enum(['mention', 'reply', 'dm'])
|
|
13
|
+
|
|
14
|
+
const stickinessSchema = z.union([
|
|
15
|
+
z.literal('off'),
|
|
16
|
+
z.object({
|
|
17
|
+
perReply: z.object({
|
|
18
|
+
window: z
|
|
19
|
+
.number()
|
|
20
|
+
.int()
|
|
21
|
+
.min(1)
|
|
22
|
+
.max(24 * 60 * 60_000),
|
|
23
|
+
}),
|
|
24
|
+
}),
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
export const STICKY_DEFAULT_WINDOW_MS = 5 * 60 * 1000
|
|
28
|
+
|
|
29
|
+
const engagementSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
trigger: z.array(engagementTriggerSchema).default(['mention', 'reply', 'dm']),
|
|
32
|
+
stickiness: stickinessSchema.default({ perReply: { window: STICKY_DEFAULT_WINDOW_MS } }),
|
|
33
|
+
})
|
|
34
|
+
.default({
|
|
35
|
+
trigger: ['mention', 'reply', 'dm'],
|
|
36
|
+
stickiness: { perReply: { window: STICKY_DEFAULT_WINDOW_MS } },
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Cold-start prefetch windows. The router seeds `contextBuffer` once when a
|
|
40
|
+
// brand-new channel session is created (no persisted sessionId for the
|
|
41
|
+
// (workspace, chat, thread) tuple). Set any field to 0 to disable that side
|
|
42
|
+
// of the prefetch. Non-fatal: if the upstream history fetch fails (missing
|
|
43
|
+
// scopes, network error, adapter doesn't expose history), the session still
|
|
44
|
+
// starts and the agent can call `channel_history` on demand.
|
|
45
|
+
//
|
|
46
|
+
// Reload semantics: `channels` is `applied` in FIELD_EFFECTS, but prefetch
|
|
47
|
+
// only fires at session creation, so changes here only affect the *next*
|
|
48
|
+
// cold start; in-flight live sessions are unaffected.
|
|
49
|
+
export const PREFETCH_DEFAULTS = {
|
|
50
|
+
thread: { head: 3, tail: 10 },
|
|
51
|
+
channel: { tail: 10 },
|
|
52
|
+
} as const
|
|
53
|
+
|
|
54
|
+
export function defaultHistoryConfig(): {
|
|
55
|
+
prefetch: { thread: { head: number; tail: number }; channel: { tail: number } }
|
|
56
|
+
} {
|
|
57
|
+
return {
|
|
58
|
+
prefetch: {
|
|
59
|
+
thread: { head: PREFETCH_DEFAULTS.thread.head, tail: PREFETCH_DEFAULTS.thread.tail },
|
|
60
|
+
channel: { tail: PREFETCH_DEFAULTS.channel.tail },
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const prefetchWindowSchema = z.number().int().min(0).max(200)
|
|
66
|
+
|
|
67
|
+
const historySchema = z
|
|
68
|
+
.object({
|
|
69
|
+
prefetch: z
|
|
70
|
+
.object({
|
|
71
|
+
thread: z
|
|
72
|
+
.object({
|
|
73
|
+
head: prefetchWindowSchema.default(PREFETCH_DEFAULTS.thread.head),
|
|
74
|
+
tail: prefetchWindowSchema.default(PREFETCH_DEFAULTS.thread.tail),
|
|
75
|
+
})
|
|
76
|
+
.default({ head: PREFETCH_DEFAULTS.thread.head, tail: PREFETCH_DEFAULTS.thread.tail }),
|
|
77
|
+
channel: z
|
|
78
|
+
.object({
|
|
79
|
+
tail: prefetchWindowSchema.default(PREFETCH_DEFAULTS.channel.tail),
|
|
80
|
+
})
|
|
81
|
+
.default({ tail: PREFETCH_DEFAULTS.channel.tail }),
|
|
82
|
+
})
|
|
83
|
+
.default({
|
|
84
|
+
thread: { head: PREFETCH_DEFAULTS.thread.head, tail: PREFETCH_DEFAULTS.thread.tail },
|
|
85
|
+
channel: { tail: PREFETCH_DEFAULTS.channel.tail },
|
|
86
|
+
}),
|
|
87
|
+
})
|
|
88
|
+
.default({
|
|
89
|
+
prefetch: {
|
|
90
|
+
thread: { head: PREFETCH_DEFAULTS.thread.head, tail: PREFETCH_DEFAULTS.thread.tail },
|
|
91
|
+
channel: { tail: PREFETCH_DEFAULTS.channel.tail },
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const adapterSchema = z.object({
|
|
96
|
+
allow: z.array(allowRuleSchema).default([]),
|
|
97
|
+
engagement: engagementSchema,
|
|
98
|
+
history: historySchema,
|
|
99
|
+
enabled: z.boolean().default(true),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// KakaoTalk uses the same shape as every other adapter. There used to be an
|
|
103
|
+
// `autoMarkRead` opt-in here; the adapter now fires a LOCO NOTIREAD ack on
|
|
104
|
+
// every inbound MSG event unconditionally (see kakaotalk.ts) so the sender's
|
|
105
|
+
// unread "1" (노란숫자) clears as soon as the agent observes the message.
|
|
106
|
+
// Existing configs with `autoMarkRead: <bool>` continue to parse — Zod's
|
|
107
|
+
// default `.object()` strips unknown keys silently — but the field has no
|
|
108
|
+
// effect. Risk note: auto-acking every received message is a distinct
|
|
109
|
+
// behavioral fingerprint vs a human, so KakaoTalk's abuse detection may
|
|
110
|
+
// flag accounts that ack rapidly and unconditionally. Run typeclaw with the
|
|
111
|
+
// kakaotalk adapter only on dedicated agent accounts you can afford to lose.
|
|
112
|
+
export const channelsSchema = z
|
|
113
|
+
.object({
|
|
114
|
+
'discord-bot': adapterSchema.optional(),
|
|
115
|
+
kakaotalk: adapterSchema.optional(),
|
|
116
|
+
'slack-bot': adapterSchema.optional(),
|
|
117
|
+
'telegram-bot': adapterSchema.optional(),
|
|
118
|
+
})
|
|
119
|
+
.default({})
|
|
120
|
+
|
|
121
|
+
export type AllowRule = string
|
|
122
|
+
export type EngagementConfig = z.infer<typeof engagementSchema>
|
|
123
|
+
export type ChannelAdapterConfig = z.infer<typeof adapterSchema>
|
|
124
|
+
export type KakaotalkAdapterConfig = ChannelAdapterConfig
|
|
125
|
+
export type ChannelsConfig = z.infer<typeof channelsSchema>
|
|
126
|
+
|
|
127
|
+
// Discord IDs are numeric snowflakes; Slack IDs start with a single uppercase
|
|
128
|
+
// letter (T for teams, C/D/G for channels) followed by alphanumerics; Telegram
|
|
129
|
+
// chat IDs are signed integers (negative for groups, `-100…` for supergroups
|
|
130
|
+
// and channels); KakaoTalk chat IDs are LOCO-protocol decimal integers
|
|
131
|
+
// (large enough to need BigInt at the protocol layer, but rendered as plain
|
|
132
|
+
// decimal strings here). All shapes are accepted on every adapter so the
|
|
133
|
+
// allow list stays declarative — the runtime ensures only the right adapter
|
|
134
|
+
// ever sees its own IDs.
|
|
135
|
+
const RULE_PATTERNS = [
|
|
136
|
+
/^\*$/,
|
|
137
|
+
// Discord
|
|
138
|
+
/^guild:\*$/,
|
|
139
|
+
/^guild:[0-9]+$/,
|
|
140
|
+
/^guild:[0-9]+\/[0-9]+$/,
|
|
141
|
+
/^dm:\*$/,
|
|
142
|
+
/^dm:[0-9]+$/,
|
|
143
|
+
// Slack
|
|
144
|
+
/^team:\*$/,
|
|
145
|
+
/^team:[A-Z0-9]+$/,
|
|
146
|
+
/^team:[A-Z0-9]+\/[A-Z0-9]+$/,
|
|
147
|
+
/^im:\*$/,
|
|
148
|
+
/^im:[A-Z0-9]+$/,
|
|
149
|
+
// Telegram (`tg:*` admits all chats; `tg:<chat_id>` scopes to one chat —
|
|
150
|
+
// numeric, may be negative). There is no team/guild concept; every chat is
|
|
151
|
+
// identified by its absolute id.
|
|
152
|
+
/^tg:\*$/,
|
|
153
|
+
/^tg:-?[0-9]+$/,
|
|
154
|
+
// KakaoTalk: a single workspace per logged-in account, so the rules scope
|
|
155
|
+
// by chat-type (1:1 / group / open) rather than by workspace. `kakao:*`
|
|
156
|
+
// admits every chat the account can see; `kakao:dm/*`, `kakao:group/*`,
|
|
157
|
+
// `kakao:open/*` admit one chat-type bucket; `kakao:<chat-id>` admits a
|
|
158
|
+
// single chat. The runtime classifies each chat into a bucket based on
|
|
159
|
+
// KakaoChat.type at chat-resolver time and surfaces the bucket via the
|
|
160
|
+
// workspace coordinate.
|
|
161
|
+
/^kakao:\*$/,
|
|
162
|
+
/^kakao:dm\/\*$/,
|
|
163
|
+
/^kakao:group\/\*$/,
|
|
164
|
+
/^kakao:open\/\*$/,
|
|
165
|
+
/^kakao:[0-9]+$/,
|
|
166
|
+
// Shared (channel ids are unique on both platforms)
|
|
167
|
+
/^channel:[A-Z0-9]+$/,
|
|
168
|
+
/^channel:-?[0-9]+$/,
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
function isValidAllowRule(rule: string): boolean {
|
|
172
|
+
return RULE_PATTERNS.some((p) => p.test(rule))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function isAllowed(rules: readonly AllowRule[], workspace: string, chat: string): boolean {
|
|
176
|
+
for (const rule of rules) {
|
|
177
|
+
if (matchRule(rule, workspace, chat)) return true
|
|
178
|
+
}
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// `*` → every workspace channel + every DM (catch-all)
|
|
183
|
+
// `guild:*` → every Discord guild channel (no DMs)
|
|
184
|
+
// `guild:G` → every channel in guild G
|
|
185
|
+
// `guild:G/C` → channel C in guild G only
|
|
186
|
+
// `team:*` → every Slack team channel (no DMs)
|
|
187
|
+
// `team:T` → every channel in team T
|
|
188
|
+
// `team:T/C` → channel C in team T only
|
|
189
|
+
// `tg:*` → every Telegram chat (DMs, groups, supergroups, channels)
|
|
190
|
+
// `tg:C` → Telegram chat C only (signed numeric chat id)
|
|
191
|
+
// `channel:C` → channel C in any workspace (IDs are globally unique on
|
|
192
|
+
// Discord/Slack and Telegram chat ids are also globally
|
|
193
|
+
// unique numeric values)
|
|
194
|
+
// `dm:*` → every Discord DM
|
|
195
|
+
// `dm:C` → Discord DM channel C only
|
|
196
|
+
// `im:*` → every Slack DM (im channel)
|
|
197
|
+
// `im:D` → Slack DM channel D only
|
|
198
|
+
// `kakao:*` → every KakaoTalk chat the account is in
|
|
199
|
+
// `kakao:dm/*` → every KakaoTalk 1:1 chat
|
|
200
|
+
// `kakao:group/*` → every KakaoTalk group chat
|
|
201
|
+
// `kakao:open/*` → every KakaoTalk open chat
|
|
202
|
+
// `kakao:<id>` → KakaoTalk chat with the given numeric chat_id
|
|
203
|
+
//
|
|
204
|
+
// `guild:`/`dm:`, `team:`/`im:`, `tg:`, and `kakao:` identify which adapter
|
|
205
|
+
// the rule was written for, but the matcher applies any rule that the
|
|
206
|
+
// (workspace, chat) pair satisfies. That keeps the adapter-side coupling at
|
|
207
|
+
// the schema/UX layer (Slack users write `team:`, Discord users write
|
|
208
|
+
// `guild:`, Telegram users write `tg:`, KakaoTalk users write `kakao:`)
|
|
209
|
+
// without bloating the matching logic. Telegram has no workspace concept;
|
|
210
|
+
// the adapter pins workspace to `'telegram'` so `tg:*` only ever admits
|
|
211
|
+
// Telegram chats. KakaoTalk uses `@kakao-dm` / `@kakao-group` / `@kakao-open`
|
|
212
|
+
// as workspace coordinates so the bucket-* rules are pure prefix matches
|
|
213
|
+
// against `workspace`.
|
|
214
|
+
function matchRule(rule: string, workspace: string, chat: string): boolean {
|
|
215
|
+
// KakaoTalk workspaces accept the global `*` catch-all or any `kakao:`
|
|
216
|
+
// rule. Adapter-specific non-kakao rules (`team:*`, `guild:*`, `dm:*`,
|
|
217
|
+
// `im:*`, `tg:*`) never admit kakao workspaces — those are scoped to
|
|
218
|
+
// their own adapter's coordinate space and would be meaningless here.
|
|
219
|
+
// The init wizard still defaults kakaotalk to the narrower `kakao:dm/*`
|
|
220
|
+
// (group chats with personal accounts are sensitive — every member sees
|
|
221
|
+
// every reply), so opting into `*` is an explicit, per-adapter decision
|
|
222
|
+
// made in `channels.kakaotalk.allow`.
|
|
223
|
+
if (KAKAO_WORKSPACES.has(workspace)) {
|
|
224
|
+
if (rule === '*') return true
|
|
225
|
+
if (rule.startsWith('kakao:')) return matchKakaoRule(rule.slice(6), workspace, chat)
|
|
226
|
+
return false
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (rule === '*') return true
|
|
230
|
+
if (rule.startsWith('kakao:')) return false
|
|
231
|
+
|
|
232
|
+
if (workspace === '@dm') {
|
|
233
|
+
if (rule === 'dm:*' || rule === 'im:*') return true
|
|
234
|
+
if (rule.startsWith('dm:')) return rule.slice(3) === chat
|
|
235
|
+
if (rule.startsWith('im:')) return rule.slice(3) === chat
|
|
236
|
+
if (rule.startsWith('channel:')) return rule.slice(8) === chat
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (workspace === 'telegram') {
|
|
241
|
+
if (rule === 'tg:*') return true
|
|
242
|
+
if (rule.startsWith('tg:')) return rule.slice(3) === chat
|
|
243
|
+
if (rule.startsWith('channel:')) return rule.slice(8) === chat
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (rule === 'guild:*' || rule === 'team:*') return true
|
|
248
|
+
if (rule.startsWith('channel:')) return rule.slice(8) === chat
|
|
249
|
+
if (rule.startsWith('guild:')) {
|
|
250
|
+
const body = rule.slice(6)
|
|
251
|
+
const slash = body.indexOf('/')
|
|
252
|
+
if (slash === -1) return body === workspace
|
|
253
|
+
return body.slice(0, slash) === workspace && body.slice(slash + 1) === chat
|
|
254
|
+
}
|
|
255
|
+
if (rule.startsWith('team:')) {
|
|
256
|
+
const body = rule.slice(5)
|
|
257
|
+
const slash = body.indexOf('/')
|
|
258
|
+
if (slash === -1) return body === workspace
|
|
259
|
+
return body.slice(0, slash) === workspace && body.slice(slash + 1) === chat
|
|
260
|
+
}
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const KAKAO_WORKSPACES = new Set(['@kakao-dm', '@kakao-group', '@kakao-open'])
|
|
265
|
+
|
|
266
|
+
function matchKakaoRule(body: string, workspace: string, chat: string): boolean {
|
|
267
|
+
if (!KAKAO_WORKSPACES.has(workspace)) return false
|
|
268
|
+
if (body === '*') return true
|
|
269
|
+
if (body === 'dm/*') return workspace === '@kakao-dm'
|
|
270
|
+
if (body === 'group/*') return workspace === '@kakao-group'
|
|
271
|
+
if (body === 'open/*') return workspace === '@kakao-open'
|
|
272
|
+
return body === chat
|
|
273
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { AdapterId } from './schema'
|
|
2
|
+
|
|
3
|
+
export type ChannelKey = {
|
|
4
|
+
adapter: AdapterId
|
|
5
|
+
workspace: string
|
|
6
|
+
chat: string
|
|
7
|
+
thread: string | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type InboundMessage = {
|
|
11
|
+
adapter: AdapterId
|
|
12
|
+
workspace: string
|
|
13
|
+
chat: string
|
|
14
|
+
thread: string | null
|
|
15
|
+
text: string
|
|
16
|
+
externalMessageId: string
|
|
17
|
+
authorId: string
|
|
18
|
+
authorName: string
|
|
19
|
+
// Set true when the inbound is from another bot (NOT this typeclaw
|
|
20
|
+
// instance's own bot identity — the adapter still drops self-authored
|
|
21
|
+
// messages with `reason: 'self_author'`). The router treats peer bots
|
|
22
|
+
// identically to humans for engagement, but uses this flag to drive a
|
|
23
|
+
// bounded loop guard so two or more bots cannot ping-pong forever.
|
|
24
|
+
authorIsBot: boolean
|
|
25
|
+
isBotMention: boolean
|
|
26
|
+
replyToBotMessageId: string | null
|
|
27
|
+
// True when the message contains at least one user mention AND none of
|
|
28
|
+
// those mentions resolve to the bot. Used by the engagement layer to
|
|
29
|
+
// suppress the solo-human fallback: if the human is explicitly tagging
|
|
30
|
+
// someone else, the message almost certainly is not addressed to us.
|
|
31
|
+
// False when the message has no mentions at all (the fallback still
|
|
32
|
+
// applies in that case) or when one of the mentions IS the bot (which
|
|
33
|
+
// is already handled by `isBotMention`). Adapters that cannot reliably
|
|
34
|
+
// enumerate mentions MUST default this to false rather than true.
|
|
35
|
+
mentionsOthers: boolean
|
|
36
|
+
// Set to the parent message id when the inbound is a reply AND the
|
|
37
|
+
// parent was authored by someone other than the bot (or by an unknown
|
|
38
|
+
// author the adapter could not attribute). Mirrors `replyToBotMessageId`
|
|
39
|
+
// but for the inverse case. Used by the engagement layer to suppress
|
|
40
|
+
// the solo-human fallback on Discord-style replies that are clearly
|
|
41
|
+
// directed at another user. Null when the message is not a reply, or
|
|
42
|
+
// when the parent is the bot's own message (already covered by
|
|
43
|
+
// `replyToBotMessageId`). Adapters that cannot determine the parent's
|
|
44
|
+
// author MUST leave this null rather than guessing.
|
|
45
|
+
replyToOtherMessageId: string | null
|
|
46
|
+
isDm: boolean
|
|
47
|
+
// Original platform-side timestamp in milliseconds since epoch. Sourced
|
|
48
|
+
// from Slack's `event.ts` or Discord's `event.timestamp` (via the
|
|
49
|
+
// adapter classifier), NOT the local time the router observed it. Zero
|
|
50
|
+
// means "unknown" — the formatter renders such lines without a
|
|
51
|
+
// timestamp prefix instead of stamping them with the wrong clock.
|
|
52
|
+
ts: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// File on disk that the agent wants to attach to an outbound message. The
|
|
56
|
+
// agent runs inside a container with /agent bind-mounted from the host;
|
|
57
|
+
// `path` should be an absolute path the container can `readFile`. The
|
|
58
|
+
// optional `filename` overrides the basename of `path` when uploading
|
|
59
|
+
// (useful when the on-disk name carries a tempdir suffix the user
|
|
60
|
+
// shouldn't see in the chat). Adapters that cannot upload files MUST
|
|
61
|
+
// fail loudly via `SendResult.ok = false` rather than silently dropping
|
|
62
|
+
// the attachment.
|
|
63
|
+
export type OutboundAttachment = {
|
|
64
|
+
path: string
|
|
65
|
+
filename?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type OutboundMessage = {
|
|
69
|
+
adapter: AdapterId
|
|
70
|
+
workspace: string
|
|
71
|
+
chat: string
|
|
72
|
+
thread?: string | null
|
|
73
|
+
// Optional when `attachments` is non-empty (file-only post is allowed).
|
|
74
|
+
// Adapters that always need text (e.g. some webhook backends in the
|
|
75
|
+
// future) must validate this themselves.
|
|
76
|
+
text?: string
|
|
77
|
+
// Each attachment is uploaded once. Order is preserved. For Slack, the
|
|
78
|
+
// first attachment carries `text` as the file's `initial_comment` so
|
|
79
|
+
// both arrive in a single API call; subsequent attachments are uploaded
|
|
80
|
+
// bare. For Discord, attachments are uploaded first (no text) and then
|
|
81
|
+
// `text` is posted as a separate message — Discord's upstream
|
|
82
|
+
// `uploadFile` does not accept a content body or a thread id, see the
|
|
83
|
+
// adapter for the workaround details.
|
|
84
|
+
attachments?: OutboundAttachment[]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type SendResult = { ok: true } | { ok: false; error: string }
|
|
88
|
+
|
|
89
|
+
export type OutboundCallback = (msg: OutboundMessage) => Promise<SendResult>
|
|
90
|
+
|
|
91
|
+
export type TypingTarget = {
|
|
92
|
+
adapter: AdapterId
|
|
93
|
+
workspace: string
|
|
94
|
+
chat: string
|
|
95
|
+
thread?: string | null
|
|
96
|
+
// 'tick' is the heartbeat fired during debouncing/generation; adapters
|
|
97
|
+
// should set the indicator visible. 'stop' is fired exactly once when the
|
|
98
|
+
// router decides the turn is over (drain finally, /stop command, or
|
|
99
|
+
// teardown); adapters should explicitly clear the indicator if their
|
|
100
|
+
// platform doesn't auto-expire it. Without 'stop', a 'tick' that lands
|
|
101
|
+
// after the agent's final reply but before the drain returns will leave
|
|
102
|
+
// the indicator on for Slack's full 2-minute server timeout.
|
|
103
|
+
phase: 'tick' | 'stop'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type TypingCallback = (target: TypingTarget) => Promise<void>
|
|
107
|
+
|
|
108
|
+
export type ResolvedChannelNames = {
|
|
109
|
+
chatName?: string
|
|
110
|
+
workspaceName?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type ChannelNameResolver = (key: ChannelKey) => Promise<ResolvedChannelNames>
|
|
114
|
+
|
|
115
|
+
// History entries are intentionally distinct from InboundMessage:
|
|
116
|
+
// `InboundMessage` carries router-classification fields (`isBotMention`,
|
|
117
|
+
// `isDm`) that are turn-delivery concerns, not history concerns. History
|
|
118
|
+
// entries instead need `isBot` so the agent can tell its own past replies
|
|
119
|
+
// from user messages, and a sortable `ts` for chronological rendering.
|
|
120
|
+
export type ChannelHistoryMessage = {
|
|
121
|
+
externalMessageId: string
|
|
122
|
+
authorId: string
|
|
123
|
+
authorName: string
|
|
124
|
+
text: string
|
|
125
|
+
ts: number
|
|
126
|
+
isBot: boolean
|
|
127
|
+
replyToBotMessageId: string | null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type FetchHistoryArgs = {
|
|
131
|
+
chat: string
|
|
132
|
+
thread: string | null
|
|
133
|
+
limit: number
|
|
134
|
+
cursor?: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type FetchHistoryResult =
|
|
138
|
+
| { ok: true; messages: ChannelHistoryMessage[]; nextCursor?: string }
|
|
139
|
+
| { ok: false; error: string }
|
|
140
|
+
|
|
141
|
+
// Registered per-adapter on the ChannelRouter alongside outbound/typing
|
|
142
|
+
// callbacks. Adapters that cannot fetch history (e.g. webhook-only future
|
|
143
|
+
// adapters) simply do not register one; the router answers
|
|
144
|
+
// 'history-not-supported' for those.
|
|
145
|
+
export type HistoryCallback = (args: FetchHistoryArgs) => Promise<FetchHistoryResult>
|
|
146
|
+
|
|
147
|
+
export type FetchAttachmentArgs = {
|
|
148
|
+
ref: string
|
|
149
|
+
filename?: string
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type FetchAttachmentResult =
|
|
153
|
+
| { ok: true; buffer: Buffer; filename: string; mimetype?: string; size: number }
|
|
154
|
+
| { ok: false; error: string }
|
|
155
|
+
|
|
156
|
+
export type FetchAttachmentCallback = (args: FetchAttachmentArgs) => Promise<FetchAttachmentResult>
|
|
157
|
+
|
|
158
|
+
export function channelKeyId(key: ChannelKey): string {
|
|
159
|
+
return `${key.adapter}:${key.workspace}:${key.chat}:${key.thread ?? ''}`
|
|
160
|
+
}
|