typeclaw 0.36.3 → 0.36.5
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 +2 -2
- package/src/channels/adapters/slack-bot-classify.ts +9 -0
- package/src/channels/adapters/slack-bot.ts +3 -1
- package/src/config/config.ts +20 -0
- package/src/config/index.ts +4 -0
- package/src/container/start.ts +2 -2
- package/src/init/index.ts +14 -2
- package/src/init/line-auth.ts +49 -4
- package/src/run/index.ts +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typeclaw",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.5",
|
|
4
4
|
"homepage": "https://github.com/typeclaw/typeclaw#readme",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/typeclaw/typeclaw/issues"
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@mariozechner/pi-tui": "^0.67.3",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
50
|
"@mozilla/readability": "^0.6.0",
|
|
51
|
-
"agent-messenger": "2.19.
|
|
51
|
+
"agent-messenger": "2.19.5",
|
|
52
52
|
"cheerio": "^1.2.0",
|
|
53
53
|
"citty": "^0.2.2",
|
|
54
54
|
"cron-parser": "^5.5.0",
|
|
@@ -14,6 +14,7 @@ export type SlackInboundAppMentionEvent = SlackSocketModeAppMentionEvent
|
|
|
14
14
|
export type InboundDropReason =
|
|
15
15
|
| 'self_author' // event.user === botUserId; we never route our own messages back to ourselves
|
|
16
16
|
| 'no_user' // event has no `user` field (e.g. system messages: channel_join, message_changed)
|
|
17
|
+
| 'slack_system_message' // non-replyable Slack message subtype events (e.g. channel_topic)
|
|
17
18
|
| 'empty_text' // event has neither text nor files — nothing for the agent to act on
|
|
18
19
|
| 'pre_connect' // bot identity is not known yet, so mention/self/reply classification cannot be trusted
|
|
19
20
|
|
|
@@ -62,6 +63,10 @@ export function classifyInbound(
|
|
|
62
63
|
return { kind: 'drop', reason: 'no_user' }
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
if (!isRouteableSlackMessageSubtype(event.subtype)) {
|
|
67
|
+
return { kind: 'drop', reason: 'slack_system_message' }
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
const rawText = event.text ?? ''
|
|
66
71
|
const { text, attachments } = splitInbound(event)
|
|
67
72
|
const slackAttachments = Array.isArray(event.attachments) ? event.attachments : undefined
|
|
@@ -156,6 +161,10 @@ export function classifyInbound(
|
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
|
|
164
|
+
export function isRouteableSlackMessageSubtype(subtype: string | undefined): boolean {
|
|
165
|
+
return subtype === undefined || subtype === 'bot_message' || subtype === 'file_share' || subtype === 'me_message'
|
|
166
|
+
}
|
|
167
|
+
|
|
159
168
|
// Slack encodes user mentions inline as `<@U…>` (or `<@W…>` for some org
|
|
160
169
|
// accounts, and `<@U…|fallback>` when the client supplied a label). Pull
|
|
161
170
|
// every distinct id out of the text — duplicates collapse so the caller
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
classifyInbound,
|
|
39
39
|
describeSlackFile,
|
|
40
40
|
type InboundDropReason,
|
|
41
|
+
isRouteableSlackMessageSubtype,
|
|
41
42
|
renderPlaceholder,
|
|
42
43
|
type SlackInboundAppMentionEvent,
|
|
43
44
|
type SlackInboundMessageEvent,
|
|
@@ -694,7 +695,7 @@ export function createSlackHistoryCallback(deps: {
|
|
|
694
695
|
}
|
|
695
696
|
|
|
696
697
|
const botUserId = botUserIdRef()
|
|
697
|
-
const rawMessages = raw.messages ?? []
|
|
698
|
+
const rawMessages = (raw.messages ?? []).filter((message) => isRouteableSlackMessageSubtype(message.subtype))
|
|
698
699
|
const mapped = rawMessages.map((m) => mapSlackMessage(m, botUserId))
|
|
699
700
|
// History payloads carry no profile, so mapSlackMessage echoes the raw
|
|
700
701
|
// id into authorName; resolve it here so prompts show display names.
|
|
@@ -1316,6 +1317,7 @@ function dropHint(reason: InboundDropReason): string {
|
|
|
1316
1317
|
case 'no_user':
|
|
1317
1318
|
case 'pre_connect':
|
|
1318
1319
|
case 'self_author':
|
|
1320
|
+
case 'slack_system_message':
|
|
1319
1321
|
return ''
|
|
1320
1322
|
}
|
|
1321
1323
|
}
|
package/src/config/config.ts
CHANGED
|
@@ -25,6 +25,26 @@ const knownModelRefs = listKnownModelRefs() as [KnownModelRef, ...KnownModelRef[
|
|
|
25
25
|
// T9 keypad: T=8, Y=9, P=7, E=3
|
|
26
26
|
const DEFAULT_PORT = 8973
|
|
27
27
|
|
|
28
|
+
export const GWS_MULTI_ACCOUNT_PLUGIN_PACKAGE = 'typeclaw-gws-multi-account'
|
|
29
|
+
export const GWS_MULTI_ACCOUNT_PLUGIN_VERSION = '^0.3.4'
|
|
30
|
+
export const DEFAULT_PLUGINS = [`${GWS_MULTI_ACCOUNT_PLUGIN_PACKAGE}@${GWS_MULTI_ACCOUNT_PLUGIN_VERSION}`] as const
|
|
31
|
+
|
|
32
|
+
export function withDefaultPlugins(plugins: readonly string[]): string[] {
|
|
33
|
+
const configuredNames = new Set(plugins.map(pluginPackageName))
|
|
34
|
+
const defaults = DEFAULT_PLUGINS.filter((entry) => !configuredNames.has(pluginPackageName(entry)))
|
|
35
|
+
return [...defaults, ...plugins]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pluginPackageName(entry: string): string {
|
|
39
|
+
if (entry.startsWith('@')) {
|
|
40
|
+
const slash = entry.indexOf('/')
|
|
41
|
+
const at = slash === -1 ? -1 : entry.indexOf('@', slash + 1)
|
|
42
|
+
return at === -1 ? entry : entry.slice(0, at)
|
|
43
|
+
}
|
|
44
|
+
const at = entry.indexOf('@')
|
|
45
|
+
return at === -1 ? entry : entry.slice(0, at)
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
// Mount names land on disk as `mounts/<name>` inside the agent folder, so they
|
|
29
49
|
// share a namespace with regular filenames. Restricting to lowercase
|
|
30
50
|
// alphanumerics + `-`/`_` keeps them shell-safe and avoids accidental shadowing
|
package/src/config/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export {
|
|
|
2
2
|
buildConfigMigrationCommitMessage,
|
|
3
3
|
config,
|
|
4
4
|
configSchema,
|
|
5
|
+
DEFAULT_PLUGINS,
|
|
5
6
|
dockerSchema,
|
|
6
7
|
dockerfileSchema,
|
|
7
8
|
expandMountPath,
|
|
@@ -9,6 +10,8 @@ export {
|
|
|
9
10
|
getConfig,
|
|
10
11
|
gitSchema,
|
|
11
12
|
gitignoreSchema,
|
|
13
|
+
GWS_MULTI_ACCOUNT_PLUGIN_PACKAGE,
|
|
14
|
+
GWS_MULTI_ACCOUNT_PLUGIN_VERSION,
|
|
12
15
|
loadConfigSync,
|
|
13
16
|
loadConfigSyncOrDefaults,
|
|
14
17
|
loadPluginConfigsSync,
|
|
@@ -22,6 +25,7 @@ export {
|
|
|
22
25
|
resolveProfile,
|
|
23
26
|
validateConfig,
|
|
24
27
|
validateMount,
|
|
28
|
+
withDefaultPlugins,
|
|
25
29
|
type Config,
|
|
26
30
|
type ConfigChange,
|
|
27
31
|
type ConfigReloadDiff,
|
package/src/container/start.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync } from 'node:fs'
|
|
|
3
3
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
4
4
|
import { isAbsolute, join, resolve } from 'node:path'
|
|
5
5
|
|
|
6
|
-
import { expandMountPath, loadConfigSync, type Config } from '@/config'
|
|
6
|
+
import { expandMountPath, loadConfigSync, withDefaultPlugins, type Config } from '@/config'
|
|
7
7
|
import { commitGitignoreWithUntracks, untrackTrulyIgnoredFiles } from '@/git/reconcile-ignored'
|
|
8
8
|
import { commitSystemFile as commitSystemFileShared } from '@/git/system-commit'
|
|
9
9
|
import { send as sendToDaemon } from '@/hostd/client'
|
|
@@ -263,7 +263,7 @@ export async function start({
|
|
|
263
263
|
// field is trustworthy by construction.
|
|
264
264
|
const pluginReconcile = await reconcilePluginDeps({
|
|
265
265
|
cwd,
|
|
266
|
-
plugins: (await loadTypeclawConfig(cwd)).plugins,
|
|
266
|
+
plugins: withDefaultPlugins((await loadTypeclawConfig(cwd)).plugins),
|
|
267
267
|
}).catch((error: unknown) => ({ error: error instanceof Error ? error.message : String(error) }) as const)
|
|
268
268
|
if ('error' in pluginReconcile) {
|
|
269
269
|
return { ok: false, reason: `plugin dependency reconcile failed: ${pluginReconcile.error}` }
|
package/src/init/index.ts
CHANGED
|
@@ -3,7 +3,14 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
|
3
3
|
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
config,
|
|
8
|
+
configSchema,
|
|
9
|
+
GWS_MULTI_ACCOUNT_PLUGIN_PACKAGE,
|
|
10
|
+
GWS_MULTI_ACCOUNT_PLUGIN_VERSION,
|
|
11
|
+
migrateLegacyConfigShape,
|
|
12
|
+
type Config,
|
|
13
|
+
} from '@/config'
|
|
7
14
|
import {
|
|
8
15
|
DEFAULT_MODEL_REF,
|
|
9
16
|
KNOWN_PROVIDERS,
|
|
@@ -577,7 +584,6 @@ export async function scaffold(root: string, options: ScaffoldOptions = {}): Pro
|
|
|
577
584
|
// to function. The Dockerfile pre-downloads Chromium too, so the agent
|
|
578
585
|
// can drive a browser without any first-run setup.
|
|
579
586
|
const AGENT_BROWSER_VERSION = '^0.26.0'
|
|
580
|
-
|
|
581
587
|
function buildPackageJson(root: string, name: string): Record<string, unknown> {
|
|
582
588
|
return {
|
|
583
589
|
name,
|
|
@@ -587,6 +593,12 @@ function buildPackageJson(root: string, name: string): Record<string, unknown> {
|
|
|
587
593
|
dependencies: {
|
|
588
594
|
typeclaw: resolveTypeclawSpec(root),
|
|
589
595
|
'agent-browser': AGENT_BROWSER_VERSION,
|
|
596
|
+
[GWS_MULTI_ACCOUNT_PLUGIN_PACKAGE]: GWS_MULTI_ACCOUNT_PLUGIN_VERSION,
|
|
597
|
+
},
|
|
598
|
+
typeclaw: {
|
|
599
|
+
managedPlugins: {
|
|
600
|
+
[GWS_MULTI_ACCOUNT_PLUGIN_PACKAGE]: GWS_MULTI_ACCOUNT_PLUGIN_VERSION,
|
|
601
|
+
},
|
|
590
602
|
},
|
|
591
603
|
}
|
|
592
604
|
}
|
package/src/init/line-auth.ts
CHANGED
|
@@ -44,6 +44,8 @@ export type LineLoginClient = {
|
|
|
44
44
|
}): Promise<LineLoginResult>
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
let lineTokenInfoSuppressionQueue: Promise<void> = Promise.resolve()
|
|
48
|
+
|
|
47
49
|
export function lineSecretsPath(agentDir: string): string {
|
|
48
50
|
return join(agentDir, 'secrets.json')
|
|
49
51
|
}
|
|
@@ -58,19 +60,20 @@ export async function runLineBootstrap(input: LineLoginInput): Promise<LineBoots
|
|
|
58
60
|
// ~/.config/agent-messenger to keep in sync.
|
|
59
61
|
const client = input.client ?? buildLineClient(store)
|
|
60
62
|
|
|
61
|
-
const result =
|
|
63
|
+
const result = await suppressLineTokenInfoDump(() =>
|
|
62
64
|
input.method === 'qr'
|
|
63
|
-
?
|
|
65
|
+
? client.loginWithQR({
|
|
64
66
|
onQRUrl: async (url) => {
|
|
65
67
|
await input.callbacks.onQRUrl?.(url)
|
|
66
68
|
},
|
|
67
69
|
onPincode: input.callbacks.onPincode,
|
|
68
70
|
})
|
|
69
|
-
:
|
|
71
|
+
: client.loginWithEmail({
|
|
70
72
|
email: input.email,
|
|
71
73
|
password: input.password,
|
|
72
74
|
onPincode: input.callbacks.onPincode,
|
|
73
|
-
})
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
74
77
|
|
|
75
78
|
if (!result.authenticated || result.account_id === undefined) {
|
|
76
79
|
const reason = result.message ?? result.error ?? 'LINE login did not authenticate'
|
|
@@ -101,3 +104,45 @@ function buildLineClient(store: SecretsLineCredentialStore): LineLoginClient {
|
|
|
101
104
|
const credManager = store as unknown as LineCredentialManager
|
|
102
105
|
return new RealLineClient(credManager) as unknown as LineLoginClient
|
|
103
106
|
}
|
|
107
|
+
|
|
108
|
+
async function suppressLineTokenInfoDump<T>(fn: () => Promise<T>): Promise<T> {
|
|
109
|
+
const previous = lineTokenInfoSuppressionQueue
|
|
110
|
+
let release: () => void = () => {}
|
|
111
|
+
lineTokenInfoSuppressionQueue = new Promise((resolve) => {
|
|
112
|
+
release = resolve
|
|
113
|
+
})
|
|
114
|
+
await previous
|
|
115
|
+
|
|
116
|
+
const originalLog = console.log
|
|
117
|
+
console.log = (...args: unknown[]) => {
|
|
118
|
+
if (isLineTokenInfoDump(args)) return
|
|
119
|
+
originalLog(...args)
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return await fn()
|
|
123
|
+
} finally {
|
|
124
|
+
console.log = originalLog
|
|
125
|
+
release()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isLineTokenInfoDump(args: unknown[]): boolean {
|
|
130
|
+
if (args.length !== 1) return false
|
|
131
|
+
const value = args[0]
|
|
132
|
+
if (value === null || typeof value !== 'object') return false
|
|
133
|
+
|
|
134
|
+
const record = value as Record<string, unknown>
|
|
135
|
+
return (
|
|
136
|
+
looksLikeJwt(record['1']) &&
|
|
137
|
+
looksLikeJwt(record['2']) &&
|
|
138
|
+
typeof record['3'] === 'number' &&
|
|
139
|
+
typeof record['4'] === 'object' &&
|
|
140
|
+
typeof record['5'] === 'string' &&
|
|
141
|
+
typeof record['6'] === 'number'
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function looksLikeJwt(value: unknown): boolean {
|
|
146
|
+
if (typeof value !== 'string') return false
|
|
147
|
+
return /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value)
|
|
148
|
+
}
|
package/src/run/index.ts
CHANGED
|
@@ -28,7 +28,14 @@ import {
|
|
|
28
28
|
type SubagentCompletionBridge,
|
|
29
29
|
} from '@/channels'
|
|
30
30
|
import { createTunnelBridge, type TunnelBridge } from '@/channels/tunnel-bridge'
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
createConfigReloadable,
|
|
33
|
+
getConfig,
|
|
34
|
+
loadConfigSync,
|
|
35
|
+
loadPluginConfigsSync,
|
|
36
|
+
reloadConfig,
|
|
37
|
+
withDefaultPlugins,
|
|
38
|
+
} from '@/config'
|
|
32
39
|
import {
|
|
33
40
|
type CountStore,
|
|
34
41
|
type CronConsumer,
|
|
@@ -162,7 +169,7 @@ export async function startAgent({
|
|
|
162
169
|
}
|
|
163
170
|
const mcpManagerOpt = mcpManager !== null ? { mcpManager } : {}
|
|
164
171
|
const pluginsLoaded = await loadPlugins({
|
|
165
|
-
entries: cwdConfig.plugins,
|
|
172
|
+
entries: withDefaultPlugins(cwdConfig.plugins),
|
|
166
173
|
agentDir: cwd,
|
|
167
174
|
configsByName: pluginConfigsByName,
|
|
168
175
|
bundled: BUNDLED_PLUGINS,
|