typeclaw 0.22.0 → 0.24.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/package.json +1 -1
- package/src/agent/index.ts +91 -22
- package/src/agent/plugin-tools.ts +38 -2
- package/src/agent/restart/index.ts +15 -3
- package/src/agent/restart-handoff/index.ts +110 -12
- package/src/agent/session-origin.ts +41 -2
- package/src/agent/subagent-completion-reminder.ts +3 -1
- package/src/agent/subagents.ts +44 -1
- package/src/agent/system-prompt.ts +4 -0
- package/src/agent/todo/continuation-policy.ts +242 -0
- package/src/agent/todo/continuation-state.ts +87 -0
- package/src/agent/todo/continuation-wiring.ts +113 -0
- package/src/agent/todo/continuation.ts +71 -0
- package/src/agent/todo/scope.ts +77 -0
- package/src/agent/todo/store.ts +98 -0
- package/src/agent/tool-not-found-nudge.ts +119 -0
- package/src/agent/tools/channel-reply.ts +51 -0
- package/src/agent/tools/restart.ts +11 -4
- package/src/agent/tools/todo/index.ts +119 -0
- package/src/bundled-plugins/backup/runner.ts +1 -1
- package/src/bundled-plugins/memory/memory-logger.ts +28 -10
- package/src/bundled-plugins/reviewer/reviewer.ts +14 -0
- package/src/channels/adapters/discord-bot-reference.ts +78 -0
- package/src/channels/adapters/discord-bot.ts +31 -3
- package/src/channels/adapters/github/inbound.ts +161 -10
- package/src/channels/adapters/github/index.ts +18 -0
- package/src/channels/adapters/github/review-thread-resolver.ts +246 -0
- package/src/channels/adapters/kakaotalk-classify.ts +67 -6
- package/src/channels/adapters/slack-bot-classify.ts +9 -1
- package/src/channels/adapters/slack-bot-reference.ts +129 -0
- package/src/channels/adapters/slack-bot.ts +75 -8
- package/src/channels/adapters/telegram-bot.ts +11 -0
- package/src/channels/manager.ts +8 -2
- package/src/channels/router.ts +477 -22
- package/src/channels/schema.ts +20 -4
- package/src/channels/types.ts +95 -0
- package/src/cli/inspect-controller.ts +99 -0
- package/src/cli/inspect.ts +21 -123
- package/src/commands/index.ts +9 -0
- package/src/init/gitignore.ts +5 -2
- package/src/inspect/index.ts +30 -26
- package/src/inspect/live.ts +17 -3
- package/src/inspect/loop.ts +23 -17
- package/src/run/index.ts +60 -5
- package/src/sandbox/build.ts +10 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/policy.ts +10 -0
- package/src/sandbox/writable-zones.ts +78 -0
- package/src/server/index.ts +118 -4
- package/src/skills/typeclaw-channel-github/SKILL.md +34 -7
- package/src/skills/typeclaw-config/SKILL.md +1 -1
- package/src/skills/typeclaw-git/SKILL.md +1 -1
- package/typeclaw.schema.json +10 -0
|
@@ -18,6 +18,7 @@ import type { ChannelRouter } from '@/channels/router'
|
|
|
18
18
|
import type { ChannelAdapterConfig } from '@/channels/schema'
|
|
19
19
|
import type {
|
|
20
20
|
ChannelHistoryMessage,
|
|
21
|
+
ChannelSelfIdentityResolver,
|
|
21
22
|
FetchAttachmentCallback,
|
|
22
23
|
FetchHistoryArgs,
|
|
23
24
|
FetchHistoryResult,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
type SlackInboundMessageEvent,
|
|
43
44
|
} from './slack-bot-classify'
|
|
44
45
|
import { createSlackDedupe } from './slack-bot-dedupe'
|
|
46
|
+
import { enrichSlackReferenceContext } from './slack-bot-reference'
|
|
45
47
|
import {
|
|
46
48
|
buildSlashAckPayload,
|
|
47
49
|
commandResultReply,
|
|
@@ -260,6 +262,7 @@ export type SlackBotAdapterOptions = {
|
|
|
260
262
|
// classifier behaves as before (no alias-driven thread anchoring), so
|
|
261
263
|
// tests and ad-hoc adapter constructions stay backwards-compatible.
|
|
262
264
|
selfAliasesRef?: () => readonly string[]
|
|
265
|
+
fetchImpl?: typeof fetch
|
|
263
266
|
}
|
|
264
267
|
|
|
265
268
|
export type SlackBotAdapter = {
|
|
@@ -350,18 +353,23 @@ export function createTypingCallback(deps: {
|
|
|
350
353
|
const { typingTracker, logger, formatChannelTag } = deps
|
|
351
354
|
return async (target: TypingTarget): Promise<void> => {
|
|
352
355
|
if (target.adapter !== 'slack-bot') return
|
|
356
|
+
// DMs are flat (thread null) but setStatus still needs a real message ts;
|
|
357
|
+
// `typingThread` carries the inbound ts for exactly that case. Real channel
|
|
358
|
+
// threads keep using `thread`. Either way the status is keyed on one ts.
|
|
359
|
+
const statusThread =
|
|
360
|
+
target.typingThread !== undefined && target.typingThread !== '' ? target.typingThread : target.thread
|
|
353
361
|
const tag = formatChannelTag
|
|
354
|
-
? await formatChannelTag(target.workspace,
|
|
355
|
-
: `channel=${
|
|
356
|
-
if (
|
|
362
|
+
? await formatChannelTag(target.workspace, statusThread ?? target.chat)
|
|
363
|
+
: `channel=${statusThread ?? target.chat}`
|
|
364
|
+
if (statusThread === undefined || statusThread === null || statusThread === '') {
|
|
357
365
|
if (target.phase === 'tick') logger.info(`[slack-bot] typing (no-op, top-level chat) ${tag}`)
|
|
358
366
|
return
|
|
359
367
|
}
|
|
360
368
|
if (target.phase === 'stop') {
|
|
361
|
-
await typingTracker.clearAfterSend(target.chat,
|
|
369
|
+
await typingTracker.clearAfterSend(target.chat, statusThread)
|
|
362
370
|
return
|
|
363
371
|
}
|
|
364
|
-
await typingTracker.setStatus(target.chat,
|
|
372
|
+
await typingTracker.setStatus(target.chat, statusThread, 'is typing...')
|
|
365
373
|
}
|
|
366
374
|
}
|
|
367
375
|
|
|
@@ -822,7 +830,7 @@ export function createOutboundCallback(deps: {
|
|
|
822
830
|
// top-level posts.
|
|
823
831
|
if (threadTs === null && chunks.length > 1) threadTs = sent.ts
|
|
824
832
|
}
|
|
825
|
-
if (typingTracker) await typingTracker.clearAfterSend(msg.chat, msg.thread)
|
|
833
|
+
if (typingTracker) await typingTracker.clearAfterSend(msg.chat, msg.typingThread ?? msg.thread)
|
|
826
834
|
return { ok: true }
|
|
827
835
|
} catch (err) {
|
|
828
836
|
const message = err instanceof Error ? err.message : String(err)
|
|
@@ -855,7 +863,7 @@ export function createOutboundCallback(deps: {
|
|
|
855
863
|
return { ok: false, error: `uploadFile failed: ${message}` }
|
|
856
864
|
}
|
|
857
865
|
}
|
|
858
|
-
if (typingTracker) await typingTracker.clearAfterSend(msg.chat, msg.thread)
|
|
866
|
+
if (typingTracker) await typingTracker.clearAfterSend(msg.chat, msg.typingThread ?? msg.thread)
|
|
859
867
|
return { ok: true }
|
|
860
868
|
}
|
|
861
869
|
}
|
|
@@ -896,9 +904,46 @@ export function createFetchAttachmentCallback(deps: {
|
|
|
896
904
|
}
|
|
897
905
|
}
|
|
898
906
|
|
|
907
|
+
function createSlackReferenceFetch(deps: { token: string; fetchImpl: typeof fetch }) {
|
|
908
|
+
return async (channelId: string, messageTs: string) => {
|
|
909
|
+
const url = new URL('https://slack.com/api/conversations.replies')
|
|
910
|
+
url.searchParams.set('channel', channelId)
|
|
911
|
+
url.searchParams.set('ts', messageTs)
|
|
912
|
+
url.searchParams.set('limit', '1')
|
|
913
|
+
const response = await deps.fetchImpl(url, { headers: { Authorization: `Bearer ${deps.token}` } })
|
|
914
|
+
if (!response.ok) return null
|
|
915
|
+
const body = recordValue(await response.json())
|
|
916
|
+
if (body === null || body.ok !== true) return null
|
|
917
|
+
const messages = arrayField(body, 'messages')
|
|
918
|
+
const first = recordValue(messages[0])
|
|
919
|
+
if (first === null) return null
|
|
920
|
+
const authorId = stringField(first, 'user') ?? stringField(first, 'bot_id')
|
|
921
|
+
const text = stringField(first, 'text')
|
|
922
|
+
if (authorId === null || text === null) return null
|
|
923
|
+
return { authorId, authorName: authorId, text }
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function recordValue(value: unknown): Record<string, unknown> | null {
|
|
928
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
929
|
+
? (value as Record<string, unknown>)
|
|
930
|
+
: null
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function arrayField(record: Record<string, unknown>, key: string): readonly unknown[] {
|
|
934
|
+
const value = record[key]
|
|
935
|
+
return Array.isArray(value) ? value : []
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function stringField(record: Record<string, unknown>, key: string): string | null {
|
|
939
|
+
const value = record[key]
|
|
940
|
+
return typeof value === 'string' && value.length > 0 ? value : null
|
|
941
|
+
}
|
|
942
|
+
|
|
899
943
|
export function createSlackBotAdapter(options: SlackBotAdapterOptions): SlackBotAdapter {
|
|
900
944
|
const logger = options.logger ?? consoleLogger
|
|
901
945
|
const client = new SlackBotClient()
|
|
946
|
+
const fetchImpl = options.fetchImpl ?? fetch
|
|
902
947
|
let listener: SlackBotListener | null = null
|
|
903
948
|
let botUserId: string | null = null
|
|
904
949
|
let teamId: string | null = null
|
|
@@ -909,6 +954,11 @@ export function createSlackBotAdapter(options: SlackBotAdapterOptions): SlackBot
|
|
|
909
954
|
const authorResolver = createSlackAuthorResolver({ token: options.token })
|
|
910
955
|
const channelResolver = createSlackChannelResolver({ token: options.token })
|
|
911
956
|
|
|
957
|
+
// Slack mentions by id (`<@U…>`), so no username form. Read live off the
|
|
958
|
+
// closure so a reconnect re-running auth.test stays reflected; one team
|
|
959
|
+
// per token in practice, so `workspace` is ignored.
|
|
960
|
+
const selfIdentityResolver: ChannelSelfIdentityResolver = () => (botUserId !== null ? { id: botUserId } : null)
|
|
961
|
+
|
|
912
962
|
const formatChannelTag = async (workspace: string, chat: string): Promise<string> => {
|
|
913
963
|
const names = await channelResolver({ adapter: 'slack-bot', workspace, chat, thread: null }).catch(
|
|
914
964
|
() => ({}) as ResolvedChannelNames,
|
|
@@ -1047,7 +1097,22 @@ export function createSlackBotAdapter(options: SlackBotAdapterOptions): SlackBot
|
|
|
1047
1097
|
}
|
|
1048
1098
|
|
|
1049
1099
|
dedupe.mark(event)
|
|
1050
|
-
const
|
|
1100
|
+
const slackAttachments = Array.isArray(event.attachments) ? event.attachments : undefined
|
|
1101
|
+
const referenceResult = await enrichSlackReferenceContext({
|
|
1102
|
+
text: verdict.payload.text,
|
|
1103
|
+
channelId: event.channel,
|
|
1104
|
+
...(event.thread_ts !== undefined ? { threadTs: event.thread_ts } : {}),
|
|
1105
|
+
messageTs: event.ts,
|
|
1106
|
+
...(slackAttachments !== undefined ? { attachments: slackAttachments } : {}),
|
|
1107
|
+
fetchMessage: createSlackReferenceFetch({ token: options.token, fetchImpl }),
|
|
1108
|
+
})
|
|
1109
|
+
const enriched = {
|
|
1110
|
+
...verdict.payload,
|
|
1111
|
+
authorName: resolvedUserName,
|
|
1112
|
+
...(referenceResult.referenceContext !== undefined
|
|
1113
|
+
? { referenceContext: referenceResult.referenceContext }
|
|
1114
|
+
: {}),
|
|
1115
|
+
}
|
|
1051
1116
|
const routedTag = await formatChannelTag(enriched.workspace, enriched.chat)
|
|
1052
1117
|
logger.info(
|
|
1053
1118
|
`[slack-bot] routed ts=${event.ts} ${routedTag} mention=${enriched.isBotMention} reply=${enriched.replyToBotMessageId !== null}`,
|
|
@@ -1143,6 +1208,7 @@ export function createSlackBotAdapter(options: SlackBotAdapterOptions): SlackBot
|
|
|
1143
1208
|
options.router.registerOutbound('slack-bot', outboundCallback)
|
|
1144
1209
|
options.router.registerTyping('slack-bot', typingCallback)
|
|
1145
1210
|
options.router.registerChannelNameResolver('slack-bot', channelResolver)
|
|
1211
|
+
options.router.registerSelfIdentity('slack-bot', selfIdentityResolver)
|
|
1146
1212
|
options.router.registerHistory('slack-bot', historyCallback)
|
|
1147
1213
|
options.router.registerFetchAttachment('slack-bot', fetchAttachmentCallback)
|
|
1148
1214
|
options.router.registerMembership('slack-bot', membershipResolver)
|
|
@@ -1162,6 +1228,7 @@ export function createSlackBotAdapter(options: SlackBotAdapterOptions): SlackBot
|
|
|
1162
1228
|
options.router.unregisterOutbound('slack-bot', outboundCallback)
|
|
1163
1229
|
options.router.unregisterTyping('slack-bot', typingCallback)
|
|
1164
1230
|
options.router.unregisterChannelNameResolver('slack-bot', channelResolver)
|
|
1231
|
+
options.router.unregisterSelfIdentity('slack-bot', selfIdentityResolver)
|
|
1165
1232
|
options.router.unregisterHistory('slack-bot', historyCallback)
|
|
1166
1233
|
options.router.unregisterFetchAttachment('slack-bot', fetchAttachmentCallback)
|
|
1167
1234
|
options.router.unregisterMembership('slack-bot', membershipResolver)
|
|
@@ -6,6 +6,7 @@ import type { ChannelRouter } from '@/channels/router'
|
|
|
6
6
|
import type { ChannelAdapterConfig } from '@/channels/schema'
|
|
7
7
|
import type {
|
|
8
8
|
ChannelNameResolver,
|
|
9
|
+
ChannelSelfIdentityResolver,
|
|
9
10
|
FetchAttachmentCallback,
|
|
10
11
|
OutboundCallback,
|
|
11
12
|
OutboundMessage,
|
|
@@ -384,6 +385,13 @@ export function createTelegramBotAdapter(options: TelegramBotAdapterOptions): Te
|
|
|
384
385
|
|
|
385
386
|
const channelResolver = createChannelNameResolver({ client })
|
|
386
387
|
|
|
388
|
+
// Telegram addresses by `@username`, not by the numeric id, so surface
|
|
389
|
+
// `username` when the bot has one; the id is kept for completeness.
|
|
390
|
+
const selfIdentityResolver: ChannelSelfIdentityResolver = () =>
|
|
391
|
+
botUser !== null
|
|
392
|
+
? { id: String(botUser.id), ...(botUser.username !== undefined ? { username: botUser.username } : {}) }
|
|
393
|
+
: null
|
|
394
|
+
|
|
387
395
|
const formatChannelTag = async (chat: string): Promise<string> => {
|
|
388
396
|
const names = await channelResolver({
|
|
389
397
|
adapter: 'telegram-bot',
|
|
@@ -522,6 +530,7 @@ export function createTelegramBotAdapter(options: TelegramBotAdapterOptions): Te
|
|
|
522
530
|
options.router.registerOutbound('telegram-bot', outboundCallback)
|
|
523
531
|
options.router.registerTyping('telegram-bot', typingCallback)
|
|
524
532
|
options.router.registerChannelNameResolver('telegram-bot', channelResolver)
|
|
533
|
+
options.router.registerSelfIdentity('telegram-bot', selfIdentityResolver)
|
|
525
534
|
options.router.registerFetchAttachment('telegram-bot', fetchAttachmentCallback)
|
|
526
535
|
options.router.registerMembership('telegram-bot', membershipResolver)
|
|
527
536
|
|
|
@@ -529,6 +538,7 @@ export function createTelegramBotAdapter(options: TelegramBotAdapterOptions): Te
|
|
|
529
538
|
options.router.unregisterOutbound('telegram-bot', outboundCallback)
|
|
530
539
|
options.router.unregisterTyping('telegram-bot', typingCallback)
|
|
531
540
|
options.router.unregisterChannelNameResolver('telegram-bot', channelResolver)
|
|
541
|
+
options.router.unregisterSelfIdentity('telegram-bot', selfIdentityResolver)
|
|
532
542
|
options.router.unregisterFetchAttachment('telegram-bot', fetchAttachmentCallback)
|
|
533
543
|
options.router.unregisterMembership('telegram-bot', membershipResolver)
|
|
534
544
|
listener?.stop()
|
|
@@ -556,6 +566,7 @@ export function createTelegramBotAdapter(options: TelegramBotAdapterOptions): Te
|
|
|
556
566
|
options.router.unregisterOutbound('telegram-bot', outboundCallback)
|
|
557
567
|
options.router.unregisterTyping('telegram-bot', typingCallback)
|
|
558
568
|
options.router.unregisterChannelNameResolver('telegram-bot', channelResolver)
|
|
569
|
+
options.router.unregisterSelfIdentity('telegram-bot', selfIdentityResolver)
|
|
559
570
|
options.router.unregisterFetchAttachment('telegram-bot', fetchAttachmentCallback)
|
|
560
571
|
options.router.unregisterMembership('telegram-bot', membershipResolver)
|
|
561
572
|
// Stop the listener BEFORE waiting for inflight handlers. The SDK's
|
package/src/channels/manager.ts
CHANGED
|
@@ -13,7 +13,13 @@ import { createKakaotalkAdapter, type KakaotalkAdapter } from './adapters/kakaot
|
|
|
13
13
|
import { createSlackBotAdapter, type SlackBotAdapter } from './adapters/slack-bot'
|
|
14
14
|
import { createTelegramBotAdapter, type TelegramBotAdapter } from './adapters/telegram-bot'
|
|
15
15
|
import type { GithubTokenBridge } from './github-token-bridge'
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
createChannelRouter,
|
|
18
|
+
type ChannelRouter,
|
|
19
|
+
type ClaimHandler,
|
|
20
|
+
type CreateSessionForChannel,
|
|
21
|
+
type RestartCommandContext,
|
|
22
|
+
} from './router'
|
|
17
23
|
import {
|
|
18
24
|
ADAPTER_IDS,
|
|
19
25
|
type AdapterId,
|
|
@@ -94,7 +100,7 @@ export type ChannelManagerOptions = {
|
|
|
94
100
|
// container-restart bindings; tests omit them so the commands stay
|
|
95
101
|
// unregistered. See CreateChannelRouterOptions.onReload/onRestart.
|
|
96
102
|
onReload?: () => Promise<string>
|
|
97
|
-
onRestart?: () => Promise<string>
|
|
103
|
+
onRestart?: (ctx?: RestartCommandContext) => Promise<string>
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
export type ChannelManager = {
|