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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typeclaw",
3
- "version": "0.36.3",
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.3",
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
  }
@@ -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
@@ -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,
@@ -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 { config, configSchema, migrateLegacyConfigShape, type Config } from '@/config'
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
  }
@@ -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
- ? await client.loginWithQR({
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
- : await client.loginWithEmail({
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 { createConfigReloadable, getConfig, loadConfigSync, loadPluginConfigsSync, reloadConfig } from '@/config'
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,