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,403 @@
|
|
|
1
|
+
import { cancel, confirm, intro, isCancel, log, note, password, select, spinner, text } from '@clack/prompts'
|
|
2
|
+
import { defineCommand } from 'citty'
|
|
3
|
+
|
|
4
|
+
import { config } from '@/config'
|
|
5
|
+
import { start, status, stop } from '@/container'
|
|
6
|
+
import {
|
|
7
|
+
CHANNEL_KINDS,
|
|
8
|
+
findAgentDir,
|
|
9
|
+
isInitialized,
|
|
10
|
+
readConfiguredChannels,
|
|
11
|
+
runAddChannel,
|
|
12
|
+
type AddChannelStepEvent,
|
|
13
|
+
type ChannelKind,
|
|
14
|
+
type KakaotalkAuthResult,
|
|
15
|
+
} from '@/init'
|
|
16
|
+
import { runKakaotalkBootstrap } from '@/init/kakaotalk-auth'
|
|
17
|
+
|
|
18
|
+
import { c, done, errorLine } from './ui'
|
|
19
|
+
|
|
20
|
+
const CHANNEL_LABELS: Record<ChannelKind, string> = {
|
|
21
|
+
'slack-bot': 'Slack',
|
|
22
|
+
'discord-bot': 'Discord',
|
|
23
|
+
'telegram-bot': 'Telegram',
|
|
24
|
+
kakaotalk: 'KakaoTalk',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const addSub = defineCommand({
|
|
28
|
+
meta: {
|
|
29
|
+
name: 'add',
|
|
30
|
+
description: 'add a new channel adapter to an existing agent (run this from inside the agent folder)',
|
|
31
|
+
},
|
|
32
|
+
args: {
|
|
33
|
+
adapter: {
|
|
34
|
+
type: 'positional',
|
|
35
|
+
description: `which adapter to add (${CHANNEL_KINDS.join(' | ')}); omit to pick interactively`,
|
|
36
|
+
required: false,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
async run({ args }) {
|
|
40
|
+
const cwd = findAgentDir(process.cwd()) ?? process.cwd()
|
|
41
|
+
|
|
42
|
+
if (!isInitialized(cwd)) {
|
|
43
|
+
console.error(errorLine('TypeClaw config file not found. Run `typeclaw init` first, or cd into an agent folder.'))
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const configured = await readConfiguredChannels(cwd)
|
|
48
|
+
const requested = args.adapter
|
|
49
|
+
const channel = requested === undefined ? await pickChannel(configured) : validateAdapterArg(requested, configured)
|
|
50
|
+
|
|
51
|
+
intro(`Adding channel: ${CHANNEL_LABELS[channel]}`)
|
|
52
|
+
|
|
53
|
+
const credentials = await collectCredentials(channel)
|
|
54
|
+
|
|
55
|
+
const events: AddChannelStepEvent[] = []
|
|
56
|
+
try {
|
|
57
|
+
await runAddChannel({
|
|
58
|
+
cwd,
|
|
59
|
+
...credentials,
|
|
60
|
+
onProgress: reportProgress(events),
|
|
61
|
+
})
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(errorLine(error instanceof Error ? error.message : String(error)))
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await maybePromptRestart(cwd, channel)
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
export const channelCommand = defineCommand({
|
|
72
|
+
meta: {
|
|
73
|
+
name: 'channel',
|
|
74
|
+
description: 'manage channel adapters wired into the agent',
|
|
75
|
+
},
|
|
76
|
+
subCommands: {
|
|
77
|
+
add: addSub,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
function validateAdapterArg(adapter: string, configured: Set<ChannelKind>): ChannelKind {
|
|
82
|
+
if (!isChannelKind(adapter)) {
|
|
83
|
+
console.error(errorLine(`Unknown adapter "${adapter}". Expected one of: ${CHANNEL_KINDS.join(', ')}.`))
|
|
84
|
+
process.exit(1)
|
|
85
|
+
}
|
|
86
|
+
if (configured.has(adapter)) {
|
|
87
|
+
console.error(
|
|
88
|
+
errorLine(
|
|
89
|
+
`${CHANNEL_LABELS[adapter]} ("${adapter}") is already configured in typeclaw.json. Edit the file directly to change its allow list.`,
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
return adapter
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isChannelKind(value: string): value is ChannelKind {
|
|
98
|
+
return (CHANNEL_KINDS as ReadonlyArray<string>).includes(value)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function pickChannel(configured: Set<ChannelKind>): Promise<ChannelKind> {
|
|
102
|
+
const available = CHANNEL_KINDS.filter((kind) => !configured.has(kind))
|
|
103
|
+
if (available.length === 0) {
|
|
104
|
+
console.error(errorLine('All supported channel adapters are already configured in typeclaw.json. Nothing to add.'))
|
|
105
|
+
process.exit(0)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const selected = await select<ChannelKind>({
|
|
109
|
+
message: 'Pick a channel to add',
|
|
110
|
+
options: available.map((kind) => ({ value: kind, label: CHANNEL_LABELS[kind] })),
|
|
111
|
+
initialValue: available[0],
|
|
112
|
+
})
|
|
113
|
+
if (isCancel(selected)) {
|
|
114
|
+
cancel('Aborted.')
|
|
115
|
+
process.exit(0)
|
|
116
|
+
}
|
|
117
|
+
return selected
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type CollectedCredentials =
|
|
121
|
+
| { channel: 'discord-bot'; discordBotToken: string }
|
|
122
|
+
| { channel: 'slack-bot'; slackBotToken: string; slackAppToken: string }
|
|
123
|
+
| { channel: 'telegram-bot'; telegramBotToken: string }
|
|
124
|
+
| { channel: 'kakaotalk'; runKakaotalkAuth: (options: { cwd: string }) => Promise<KakaotalkAuthResult> }
|
|
125
|
+
|
|
126
|
+
async function collectCredentials(channel: ChannelKind): Promise<CollectedCredentials> {
|
|
127
|
+
switch (channel) {
|
|
128
|
+
case 'discord-bot':
|
|
129
|
+
return { channel, discordBotToken: await promptDiscordToken() }
|
|
130
|
+
case 'slack-bot': {
|
|
131
|
+
const slack = await promptSlackTokens()
|
|
132
|
+
return { channel, slackBotToken: slack.bot, slackAppToken: slack.app }
|
|
133
|
+
}
|
|
134
|
+
case 'telegram-bot':
|
|
135
|
+
return { channel, telegramBotToken: await promptTelegramToken() }
|
|
136
|
+
case 'kakaotalk': {
|
|
137
|
+
const creds = await promptKakaotalkCredentials()
|
|
138
|
+
return {
|
|
139
|
+
channel,
|
|
140
|
+
runKakaotalkAuth: ({ cwd: agentDir }) =>
|
|
141
|
+
runKakaotalkBootstrap({
|
|
142
|
+
email: creds.email,
|
|
143
|
+
password: creds.password,
|
|
144
|
+
agentDir,
|
|
145
|
+
callbacks: { onPasscode: (code) => log.info(`Confirm this passcode on your phone: ${code}`) },
|
|
146
|
+
}),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function promptDiscordToken(): Promise<string> {
|
|
153
|
+
note(
|
|
154
|
+
[
|
|
155
|
+
'https://discord.com/developers/applications',
|
|
156
|
+
'New Application → Bot tab → Reset Token.',
|
|
157
|
+
'Enable the MESSAGE CONTENT intent.',
|
|
158
|
+
].join('\n'),
|
|
159
|
+
'Get a Discord bot token',
|
|
160
|
+
)
|
|
161
|
+
const token = await password({
|
|
162
|
+
message: 'Discord bot token',
|
|
163
|
+
validate: (value) => (value && value.length > 0 ? undefined : 'Token is required'),
|
|
164
|
+
})
|
|
165
|
+
if (isCancel(token)) {
|
|
166
|
+
cancel('Aborted.')
|
|
167
|
+
process.exit(0)
|
|
168
|
+
}
|
|
169
|
+
return token
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function promptSlackTokens(): Promise<{ bot: string; app: string }> {
|
|
173
|
+
note(
|
|
174
|
+
[
|
|
175
|
+
'1. https://api.slack.com/apps → Create New App → From a manifest.',
|
|
176
|
+
' Pick your workspace, then paste this JSON manifest:',
|
|
177
|
+
'',
|
|
178
|
+
' {',
|
|
179
|
+
' "display_information": { "name": "TypeClaw" },',
|
|
180
|
+
' "features": {',
|
|
181
|
+
' "bot_user": { "display_name": "TypeClaw", "always_online": true }',
|
|
182
|
+
' },',
|
|
183
|
+
' "oauth_config": {',
|
|
184
|
+
' "scopes": {',
|
|
185
|
+
' "bot": [',
|
|
186
|
+
' "app_mentions:read", "chat:write", "users:read", "files:read",',
|
|
187
|
+
' "channels:history", "channels:read",',
|
|
188
|
+
' "groups:history", "groups:read",',
|
|
189
|
+
' "im:history", "im:read",',
|
|
190
|
+
' "mpim:history", "mpim:read"',
|
|
191
|
+
' ]',
|
|
192
|
+
' }',
|
|
193
|
+
' },',
|
|
194
|
+
' "settings": {',
|
|
195
|
+
' "event_subscriptions": {',
|
|
196
|
+
' "bot_events": [',
|
|
197
|
+
' "app_mention",',
|
|
198
|
+
' "message.channels", "message.groups",',
|
|
199
|
+
' "message.im", "message.mpim"',
|
|
200
|
+
' ]',
|
|
201
|
+
' },',
|
|
202
|
+
' "socket_mode_enabled": true',
|
|
203
|
+
' }',
|
|
204
|
+
' }',
|
|
205
|
+
'',
|
|
206
|
+
'2. Install to Workspace, then OAuth & Permissions →',
|
|
207
|
+
' copy the Bot User OAuth Token (xoxb-...).',
|
|
208
|
+
'3. Basic Information → App-Level Tokens → Generate Token and',
|
|
209
|
+
' Scopes, add the connections:write scope, and copy the',
|
|
210
|
+
' token (xapp-...). Socket Mode needs this; the manifest',
|
|
211
|
+
' cannot grant it.',
|
|
212
|
+
'4. Invite the bot to any private channel or DM you want it in:',
|
|
213
|
+
' /invite @TypeClaw',
|
|
214
|
+
].join('\n'),
|
|
215
|
+
'Get a Slack bot',
|
|
216
|
+
)
|
|
217
|
+
const botToken = await password({
|
|
218
|
+
message: 'Slack bot token (xoxb-...)',
|
|
219
|
+
validate: (value) =>
|
|
220
|
+
value && value.length > 0
|
|
221
|
+
? value.startsWith('xoxb-')
|
|
222
|
+
? undefined
|
|
223
|
+
: 'Bot token must start with "xoxb-"'
|
|
224
|
+
: 'Token is required',
|
|
225
|
+
})
|
|
226
|
+
if (isCancel(botToken)) {
|
|
227
|
+
cancel('Aborted.')
|
|
228
|
+
process.exit(0)
|
|
229
|
+
}
|
|
230
|
+
note(
|
|
231
|
+
[
|
|
232
|
+
'Slack does not accept connections:write inside the manifest, so',
|
|
233
|
+
'this token has to be generated by hand:',
|
|
234
|
+
'',
|
|
235
|
+
'1. Basic Information → App-Level Tokens → Generate Token and Scopes.',
|
|
236
|
+
'2. Token Name: anything (e.g. "socket-mode").',
|
|
237
|
+
'3. Add Scope → connections:write → Generate.',
|
|
238
|
+
'4. Copy the xapp-... token shown once on screen.',
|
|
239
|
+
' (You cannot retrieve it later — only revoke and regenerate.)',
|
|
240
|
+
].join('\n'),
|
|
241
|
+
'Generate the Slack app-level token',
|
|
242
|
+
)
|
|
243
|
+
const appToken = await password({
|
|
244
|
+
message: 'Slack app-level token (xapp-...) — Socket Mode requires this',
|
|
245
|
+
validate: (value) =>
|
|
246
|
+
value && value.length > 0
|
|
247
|
+
? value.startsWith('xapp-')
|
|
248
|
+
? undefined
|
|
249
|
+
: 'App-level token must start with "xapp-"'
|
|
250
|
+
: 'Token is required',
|
|
251
|
+
})
|
|
252
|
+
if (isCancel(appToken)) {
|
|
253
|
+
cancel('Aborted.')
|
|
254
|
+
process.exit(0)
|
|
255
|
+
}
|
|
256
|
+
return { bot: botToken, app: appToken }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function promptTelegramToken(): Promise<string> {
|
|
260
|
+
note(
|
|
261
|
+
[
|
|
262
|
+
'Open Telegram and message @BotFather.',
|
|
263
|
+
'/newbot → pick a name and username, copy the HTTP API token',
|
|
264
|
+
' (looks like 1234567890:ABCdef...).',
|
|
265
|
+
'In @BotFather: /setprivacy → Disable, so the bot can see group messages.',
|
|
266
|
+
].join('\n'),
|
|
267
|
+
'Get a Telegram bot token',
|
|
268
|
+
)
|
|
269
|
+
const token = await password({
|
|
270
|
+
message: 'Telegram bot token',
|
|
271
|
+
validate: (value) =>
|
|
272
|
+
value && value.length > 0
|
|
273
|
+
? /^\d+:/.test(value)
|
|
274
|
+
? undefined
|
|
275
|
+
: 'Bot token must look like "<digits>:<secret>" (from @BotFather)'
|
|
276
|
+
: 'Token is required',
|
|
277
|
+
})
|
|
278
|
+
if (isCancel(token)) {
|
|
279
|
+
cancel('Aborted.')
|
|
280
|
+
process.exit(0)
|
|
281
|
+
}
|
|
282
|
+
return token
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function promptKakaotalkCredentials(): Promise<{ email: string; password: string }> {
|
|
286
|
+
note(
|
|
287
|
+
[
|
|
288
|
+
'KakaoTalk authentication uses a personal account, registered as a',
|
|
289
|
+
'tablet sub-device. Messages will be sent and received under this',
|
|
290
|
+
'account. Use a non-primary account if possible.',
|
|
291
|
+
'',
|
|
292
|
+
'After you submit the password, KakaoTalk may ask you to confirm a',
|
|
293
|
+
'passcode on your phone. Watch the screen for the code.',
|
|
294
|
+
].join('\n'),
|
|
295
|
+
'About to log in to KakaoTalk',
|
|
296
|
+
)
|
|
297
|
+
const email = await text({
|
|
298
|
+
message: 'KakaoTalk email',
|
|
299
|
+
validate: (value) => (value && value.length > 0 ? undefined : 'Email is required'),
|
|
300
|
+
})
|
|
301
|
+
if (isCancel(email)) {
|
|
302
|
+
cancel('Aborted.')
|
|
303
|
+
process.exit(0)
|
|
304
|
+
}
|
|
305
|
+
const pwd = await password({
|
|
306
|
+
message: 'KakaoTalk password',
|
|
307
|
+
validate: (value) => (value && value.length > 0 ? undefined : 'Password is required'),
|
|
308
|
+
})
|
|
309
|
+
if (isCancel(pwd)) {
|
|
310
|
+
cancel('Aborted.')
|
|
311
|
+
process.exit(0)
|
|
312
|
+
}
|
|
313
|
+
return { email, password: pwd }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function reportProgress(events: AddChannelStepEvent[]): (event: AddChannelStepEvent) => void {
|
|
317
|
+
const spinners: Partial<Record<AddChannelStepEvent['step'], ReturnType<typeof spinner>>> = {}
|
|
318
|
+
|
|
319
|
+
return (event) => {
|
|
320
|
+
events.push(event)
|
|
321
|
+
if (event.phase === 'start') {
|
|
322
|
+
const s = spinner()
|
|
323
|
+
s.start(START_MESSAGES[event.step])
|
|
324
|
+
spinners[event.step] = s
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const s = spinners[event.step]
|
|
329
|
+
if (!s) return
|
|
330
|
+
|
|
331
|
+
switch (event.step) {
|
|
332
|
+
case 'kakaotalk-auth':
|
|
333
|
+
s.stop(reportKakaotalkAuth(event.result))
|
|
334
|
+
break
|
|
335
|
+
case 'config':
|
|
336
|
+
s.stop('Updated typeclaw.json.')
|
|
337
|
+
break
|
|
338
|
+
case 'secrets':
|
|
339
|
+
s.stop('Appended credentials to .env.')
|
|
340
|
+
break
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const START_MESSAGES: Record<AddChannelStepEvent['step'], string> = {
|
|
346
|
+
'kakaotalk-auth': 'Logging in to KakaoTalk...',
|
|
347
|
+
config: 'Updating typeclaw.json...',
|
|
348
|
+
secrets: 'Appending credentials to .env...',
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function reportKakaotalkAuth(result: KakaotalkAuthResult): string {
|
|
352
|
+
if (result.ok) return 'KakaoTalk credentials saved to workspace/.agent-messenger/.'
|
|
353
|
+
return `KakaoTalk login failed: ${result.reason}`
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function maybePromptRestart(cwd: string, channel: ChannelKind): Promise<void> {
|
|
357
|
+
const label = CHANNEL_LABELS[channel]
|
|
358
|
+
const current = await status({ cwd }).catch(() => null)
|
|
359
|
+
if (current === null || current.kind !== 'running') {
|
|
360
|
+
done({
|
|
361
|
+
title: c.green(`${label} channel added.`),
|
|
362
|
+
hints: [
|
|
363
|
+
{ label: 'Start the agent:', command: 'typeclaw start' },
|
|
364
|
+
{ label: 'Then check status:', command: 'typeclaw status' },
|
|
365
|
+
],
|
|
366
|
+
})
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const restartNow = await confirm({
|
|
371
|
+
message:
|
|
372
|
+
'Channel config is restart-required and the agent container is running. Restart it now to apply the new channel?',
|
|
373
|
+
initialValue: true,
|
|
374
|
+
})
|
|
375
|
+
if (isCancel(restartNow) || !restartNow) {
|
|
376
|
+
done({
|
|
377
|
+
title: c.green(`${label} channel added.`),
|
|
378
|
+
hints: [
|
|
379
|
+
{ label: 'Apply later:', command: 'typeclaw restart' },
|
|
380
|
+
{ label: 'Check status:', command: 'typeclaw status' },
|
|
381
|
+
],
|
|
382
|
+
})
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const stopped = await stop({ cwd })
|
|
387
|
+
if (!stopped.ok) {
|
|
388
|
+
console.error(errorLine(`Restart failed during stop: ${stopped.reason}`))
|
|
389
|
+
process.exit(1)
|
|
390
|
+
}
|
|
391
|
+
const started = await start({ cwd, preferredHostPort: config.port, cliEntry: process.argv[1] })
|
|
392
|
+
if (!started.ok) {
|
|
393
|
+
console.error(errorLine(`Restart failed during start: ${started.reason}`))
|
|
394
|
+
process.exit(1)
|
|
395
|
+
}
|
|
396
|
+
done({
|
|
397
|
+
title: c.green(`${label} channel added. Restarted ${started.plan.containerName} on host port ${started.hostPort}.`),
|
|
398
|
+
hints: [
|
|
399
|
+
{ label: 'Attach TUI:', command: 'typeclaw tui' },
|
|
400
|
+
{ label: 'Follow logs:', command: 'typeclaw logs -f' },
|
|
401
|
+
],
|
|
402
|
+
})
|
|
403
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { styleText } from 'node:util'
|
|
2
|
+
|
|
3
|
+
import type { AgentRuntimeState, ComposeStatusResult } from '@/compose'
|
|
4
|
+
|
|
5
|
+
export type FormatComposeStatusOptions = { useColor?: boolean }
|
|
6
|
+
|
|
7
|
+
type ColorFn = (s: string) => string
|
|
8
|
+
type Palette = {
|
|
9
|
+
bold: ColorFn
|
|
10
|
+
dim: ColorFn
|
|
11
|
+
green: ColorFn
|
|
12
|
+
yellow: ColorFn
|
|
13
|
+
cyan: ColorFn
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const identity: ColorFn = (s) => s
|
|
17
|
+
const NO_PALETTE: Palette = {
|
|
18
|
+
bold: identity,
|
|
19
|
+
dim: identity,
|
|
20
|
+
green: identity,
|
|
21
|
+
yellow: identity,
|
|
22
|
+
cyan: identity,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const COLOR_PALETTE: Palette = {
|
|
26
|
+
bold: (s) => styleText('bold', s),
|
|
27
|
+
dim: (s) => styleText('dim', s),
|
|
28
|
+
green: (s) => styleText('green', s),
|
|
29
|
+
yellow: (s) => styleText('yellow', s),
|
|
30
|
+
cyan: (s) => styleText('cyan', s),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const STATE_LABELS: Record<AgentRuntimeState, string> = {
|
|
34
|
+
running: 'running',
|
|
35
|
+
stopped: 'stopped',
|
|
36
|
+
absent: 'not started',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const STATE_LABEL_WIDTH = Math.max(...Object.values(STATE_LABELS).map((l) => l.length))
|
|
40
|
+
|
|
41
|
+
export function formatComposeStatus(result: ComposeStatusResult, opts: FormatComposeStatusOptions = {}): string {
|
|
42
|
+
const useColor = opts.useColor ?? false
|
|
43
|
+
const p: Palette = useColor ? COLOR_PALETTE : NO_PALETTE
|
|
44
|
+
|
|
45
|
+
if (result.entries.length === 0) {
|
|
46
|
+
return p.dim(`No typeclaw agents in ${result.rootCwd}.`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const nameWidth = result.entries.reduce((w, e) => Math.max(w, e.name.length), 0)
|
|
50
|
+
const header = p.dim(
|
|
51
|
+
`${result.entries.length} ${result.entries.length === 1 ? 'agent' : 'agents'} in ${result.rootCwd}`,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const lines = [header, '']
|
|
55
|
+
for (const entry of result.entries) {
|
|
56
|
+
lines.push(renderRow(entry, nameWidth, p))
|
|
57
|
+
}
|
|
58
|
+
return lines.join('\n')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderRow(entry: ComposeStatusResult['entries'][number], nameWidth: number, p: Palette): string {
|
|
62
|
+
const glyph = renderGlyph(entry.state, p)
|
|
63
|
+
const name = p.bold(entry.name.padEnd(nameWidth))
|
|
64
|
+
const state = renderState(entry.state, p)
|
|
65
|
+
const detail = renderDetail(entry, p)
|
|
66
|
+
return detail ? ` ${glyph} ${name} ${state} ${detail}` : ` ${glyph} ${name} ${state}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderGlyph(state: AgentRuntimeState, p: Palette): string {
|
|
70
|
+
switch (state) {
|
|
71
|
+
case 'running':
|
|
72
|
+
return p.green('●')
|
|
73
|
+
case 'stopped':
|
|
74
|
+
return p.yellow('○')
|
|
75
|
+
case 'absent':
|
|
76
|
+
return p.dim('·')
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderState(state: AgentRuntimeState, p: Palette): string {
|
|
81
|
+
const label = STATE_LABELS[state].padEnd(STATE_LABEL_WIDTH)
|
|
82
|
+
switch (state) {
|
|
83
|
+
case 'running':
|
|
84
|
+
return p.green(label)
|
|
85
|
+
case 'stopped':
|
|
86
|
+
return p.yellow(label)
|
|
87
|
+
case 'absent':
|
|
88
|
+
return p.dim(label)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderDetail(entry: ComposeStatusResult['entries'][number], p: Palette): string {
|
|
93
|
+
if (entry.state !== 'running' || entry.hostPort === null) return ''
|
|
94
|
+
return p.dim('port ') + p.cyan(String(entry.hostPort))
|
|
95
|
+
}
|