@bubblebrain-ai/bubble 0.0.10 → 0.0.12
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/dist/agent.d.ts +1 -0
- package/dist/agent.js +6 -2
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +31 -3
- package/dist/feedback/collect.d.ts +7 -0
- package/dist/feedback/collect.js +119 -0
- package/dist/feedback/config.d.ts +14 -0
- package/dist/feedback/config.js +16 -0
- package/dist/feedback/redact.d.ts +1 -0
- package/dist/feedback/redact.js +25 -0
- package/dist/feedback/submit.d.ts +6 -0
- package/dist/feedback/submit.js +43 -0
- package/dist/feedback/types.d.ts +22 -0
- package/dist/feishu/agent-host/approval-card.d.ts +11 -0
- package/dist/feishu/agent-host/approval-card.js +46 -0
- package/dist/feishu/agent-host/approval-ui.d.ts +59 -0
- package/dist/feishu/agent-host/approval-ui.js +214 -0
- package/dist/feishu/agent-host/run-driver.d.ts +51 -0
- package/dist/feishu/agent-host/run-driver.js +302 -0
- package/dist/feishu/agent-host/runtime-deps.d.ts +33 -0
- package/dist/feishu/agent-host/runtime-deps.js +8 -0
- package/dist/feishu/card/budget.d.ts +40 -0
- package/dist/feishu/card/budget.js +134 -0
- package/dist/feishu/card/renderer.d.ts +29 -0
- package/dist/feishu/card/renderer.js +245 -0
- package/dist/feishu/card/run-state-types.d.ts +49 -0
- package/dist/feishu/card/run-state-types.js +15 -0
- package/dist/feishu/card/run-state.d.ts +21 -0
- package/dist/feishu/card/run-state.js +217 -0
- package/dist/feishu/channel/channel.d.ts +52 -0
- package/dist/feishu/channel/channel.js +74 -0
- package/dist/feishu/config.d.ts +24 -0
- package/dist/feishu/config.js +97 -0
- package/dist/feishu/format.d.ts +6 -0
- package/dist/feishu/format.js +14 -0
- package/dist/feishu/index.d.ts +4 -0
- package/dist/feishu/index.js +4 -0
- package/dist/feishu/logger.d.ts +31 -0
- package/dist/feishu/logger.js +62 -0
- package/dist/feishu/paths.d.ts +12 -0
- package/dist/feishu/paths.js +38 -0
- package/dist/feishu/process-registry.d.ts +29 -0
- package/dist/feishu/process-registry.js +90 -0
- package/dist/feishu/router/commands.d.ts +38 -0
- package/dist/feishu/router/commands.js +286 -0
- package/dist/feishu/router/event-router.d.ts +40 -0
- package/dist/feishu/router/event-router.js +208 -0
- package/dist/feishu/router/whitelist.d.ts +23 -0
- package/dist/feishu/router/whitelist.js +20 -0
- package/dist/feishu/runtime/active-runs.d.ts +32 -0
- package/dist/feishu/runtime/active-runs.js +84 -0
- package/dist/feishu/runtime/pending-queue.d.ts +36 -0
- package/dist/feishu/runtime/pending-queue.js +98 -0
- package/dist/feishu/runtime/process-pool.d.ts +29 -0
- package/dist/feishu/runtime/process-pool.js +49 -0
- package/dist/feishu/schema.d.ts +17 -0
- package/dist/feishu/schema.js +252 -0
- package/dist/feishu/scope/scope-registry.d.ts +39 -0
- package/dist/feishu/scope/scope-registry.js +148 -0
- package/dist/feishu/scope/session-binder.d.ts +44 -0
- package/dist/feishu/scope/session-binder.js +100 -0
- package/dist/feishu/scope/session-store.d.ts +24 -0
- package/dist/feishu/scope/session-store.js +73 -0
- package/dist/feishu/secrets.d.ts +37 -0
- package/dist/feishu/secrets.js +129 -0
- package/dist/feishu/serve.d.ts +12 -0
- package/dist/feishu/serve.js +288 -0
- package/dist/feishu/types.d.ts +75 -0
- package/dist/feishu/types.js +23 -0
- package/dist/feishu/wizard.d.ts +24 -0
- package/dist/feishu/wizard.js +121 -0
- package/dist/main.js +98 -32
- package/dist/model-catalog.js +3 -0
- package/dist/prompt/compose.js +3 -3
- package/dist/prompt/environment.js +2 -0
- package/dist/prompt/reminders.js +1 -1
- package/dist/provider-openai-codex.d.ts +8 -1
- package/dist/provider-openai-codex.js +33 -9
- package/dist/provider.d.ts +2 -0
- package/dist/session-title.d.ts +16 -0
- package/dist/session-title.js +134 -0
- package/dist/session-types.d.ts +5 -0
- package/dist/session.d.ts +16 -0
- package/dist/session.js +154 -2
- package/dist/skills/invocation.js +0 -18
- package/dist/skills/registry.d.ts +1 -0
- package/dist/skills/registry.js +2 -0
- package/dist/slash-commands/commands.js +15 -22
- package/dist/slash-commands/feishu.d.ts +17 -0
- package/dist/slash-commands/feishu.js +400 -0
- package/dist/slash-commands/registry.js +1 -1
- package/dist/slash-commands/types.d.ts +3 -1
- package/dist/text-display.d.ts +3 -0
- package/dist/text-display.js +25 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +3 -1
- package/dist/tools/skill-search.d.ts +10 -0
- package/dist/tools/skill-search.js +134 -0
- package/dist/tools/skill.js +1 -4
- package/dist/tui-ink/app.js +265 -118
- package/dist/tui-ink/code-highlight.js +2 -3
- package/dist/tui-ink/detect-theme.d.ts +1 -18
- package/dist/tui-ink/detect-theme.js +1 -37
- package/dist/tui-ink/display-history.d.ts +20 -3
- package/dist/tui-ink/display-history.js +26 -27
- package/dist/tui-ink/feedback-dialog.d.ts +19 -0
- package/dist/tui-ink/feedback-dialog.js +123 -0
- package/dist/tui-ink/feishu-setup-picker.d.ts +5 -0
- package/dist/tui-ink/feishu-setup-picker.js +261 -0
- package/dist/tui-ink/input-box.d.ts +25 -1
- package/dist/tui-ink/input-box.js +132 -11
- package/dist/tui-ink/input-history.js +3 -5
- package/dist/tui-ink/markdown.d.ts +32 -0
- package/dist/tui-ink/markdown.js +111 -4
- package/dist/tui-ink/message-list.d.ts +1 -6
- package/dist/tui-ink/message-list.js +86 -34
- package/dist/tui-ink/model-picker.d.ts +18 -0
- package/dist/tui-ink/model-picker.js +81 -27
- package/dist/tui-ink/run-session-picker.d.ts +10 -0
- package/dist/tui-ink/run-session-picker.js +22 -0
- package/dist/tui-ink/run.js +7 -2
- package/dist/tui-ink/session-picker.d.ts +10 -0
- package/dist/tui-ink/session-picker.js +110 -0
- package/dist/tui-ink/terminal-mouse.d.ts +4 -0
- package/dist/tui-ink/terminal-mouse.js +23 -0
- package/dist/tui-ink/theme.js +2 -2
- package/dist/tui-ink/trace-groups.js +25 -2
- package/dist/tui-ink/welcome.js +2 -4
- package/package.json +4 -5
- package/dist/tui/clipboard.d.ts +0 -1
- package/dist/tui/clipboard.js +0 -53
- package/dist/tui/display-history.d.ts +0 -44
- package/dist/tui/display-history.js +0 -243
- package/dist/tui/escape-confirmation.d.ts +0 -15
- package/dist/tui/escape-confirmation.js +0 -30
- package/dist/tui/file-mentions.d.ts +0 -29
- package/dist/tui/file-mentions.js +0 -174
- package/dist/tui/global-key-router.d.ts +0 -3
- package/dist/tui/global-key-router.js +0 -87
- package/dist/tui/image-paste.d.ts +0 -95
- package/dist/tui/image-paste.js +0 -505
- package/dist/tui/markdown-inline.d.ts +0 -22
- package/dist/tui/markdown-inline.js +0 -68
- package/dist/tui/markdown-theme-rules.d.ts +0 -23
- package/dist/tui/markdown-theme-rules.js +0 -164
- package/dist/tui/markdown-theme.d.ts +0 -5
- package/dist/tui/markdown-theme.js +0 -27
- package/dist/tui/opencode-spinner.d.ts +0 -21
- package/dist/tui/opencode-spinner.js +0 -216
- package/dist/tui/prompt-keybindings.d.ts +0 -42
- package/dist/tui/prompt-keybindings.js +0 -35
- package/dist/tui/recent-activity.d.ts +0 -8
- package/dist/tui/recent-activity.js +0 -71
- package/dist/tui/render-signature.d.ts +0 -1
- package/dist/tui/render-signature.js +0 -7
- package/dist/tui/run.d.ts +0 -38
- package/dist/tui/run.js +0 -6996
- package/dist/tui/sidebar-mcp.d.ts +0 -31
- package/dist/tui/sidebar-mcp.js +0 -62
- package/dist/tui/sidebar-state.d.ts +0 -12
- package/dist/tui/sidebar-state.js +0 -69
- package/dist/tui/streaming-tool-args.d.ts +0 -15
- package/dist/tui/streaming-tool-args.js +0 -30
- package/dist/tui/tool-renderers/fallback.d.ts +0 -2
- package/dist/tui/tool-renderers/fallback.js +0 -75
- package/dist/tui/tool-renderers/registry.d.ts +0 -3
- package/dist/tui/tool-renderers/registry.js +0 -11
- package/dist/tui/tool-renderers/subagent.d.ts +0 -2
- package/dist/tui/tool-renderers/subagent.js +0 -114
- package/dist/tui/tool-renderers/types.d.ts +0 -36
- package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
- package/dist/tui/tool-renderers/write-preview.js +0 -30
- package/dist/tui/tool-renderers/write.d.ts +0 -6
- package/dist/tui/tool-renderers/write.js +0 -88
- /package/dist/{tui/tool-renderers → feedback}/types.js +0 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bubble serve --feishu` entry. Wires every layer of the host together.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { configExists, loadConfig, resolveAppSecret } from "./config.js";
|
|
7
|
+
import { runWizard } from "./wizard.js";
|
|
8
|
+
import { ScopeRegistry } from "./scope/scope-registry.js";
|
|
9
|
+
import { SessionStore } from "./scope/session-store.js";
|
|
10
|
+
import { SessionBinder } from "./scope/session-binder.js";
|
|
11
|
+
import { PendingQueue, combineQueuedMessages } from "./runtime/pending-queue.js";
|
|
12
|
+
import { ActiveRuns } from "./runtime/active-runs.js";
|
|
13
|
+
import { ProcessPool } from "./runtime/process-pool.js";
|
|
14
|
+
import { createBubbleChannel } from "./channel/channel.js";
|
|
15
|
+
import { FeishuApprovalUI } from "./agent-host/approval-ui.js";
|
|
16
|
+
import { RunDriver } from "./agent-host/run-driver.js";
|
|
17
|
+
import { EventRouter } from "./router/event-router.js";
|
|
18
|
+
import { FeishuLogger } from "./logger.js";
|
|
19
|
+
import { ProcessRegistry } from "./process-registry.js";
|
|
20
|
+
import { parseScopeKey } from "./types.js";
|
|
21
|
+
// Re-use existing process-level building blocks.
|
|
22
|
+
import { UserConfig } from "../config.js";
|
|
23
|
+
import { ProviderRegistry } from "../provider-registry.js";
|
|
24
|
+
import { createProviderInstance } from "../provider.js";
|
|
25
|
+
import { SettingsManager } from "../permissions/settings.js";
|
|
26
|
+
import { SkillRegistry } from "../skills/registry.js";
|
|
27
|
+
import { loadMcpConfig } from "../mcp/config.js";
|
|
28
|
+
import { McpManager } from "../mcp/manager.js";
|
|
29
|
+
import { BashAllowlist } from "../approval/session-cache.js";
|
|
30
|
+
export async function serveFeishu(opts = {}) {
|
|
31
|
+
// 1. Setup or load config
|
|
32
|
+
if (opts.setup || !configExists()) {
|
|
33
|
+
await runWizard();
|
|
34
|
+
if (opts.setup) {
|
|
35
|
+
console.log(chalk.dim("Re-run `bubble serve --feishu` (without --setup) to start serving."));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const config = loadConfig();
|
|
40
|
+
const appSecret = resolveAppSecret(config);
|
|
41
|
+
// 2. Process registry — detect duplicates
|
|
42
|
+
const procRegistry = new ProcessRegistry();
|
|
43
|
+
procRegistry.gc();
|
|
44
|
+
const conflicts = procRegistry.findConflicts(config.app.appId);
|
|
45
|
+
if (conflicts.length > 0) {
|
|
46
|
+
if (opts.killOld || process.env.BUBBLE_KILL_OLD === "1") {
|
|
47
|
+
const killed = procRegistry.killConflicts(config.app.appId);
|
|
48
|
+
console.log(chalk.dim(`Killed ${killed} stale instance(s) for appId ${config.app.appId}.`));
|
|
49
|
+
}
|
|
50
|
+
else if (process.stdin.isTTY) {
|
|
51
|
+
console.log(chalk.yellow(`\n⚠ Another bubble serve --feishu is running for app ${config.app.appId}:`));
|
|
52
|
+
for (const c of conflicts) {
|
|
53
|
+
console.log(` pid ${c.entry.pid} (started ${new Date(c.entry.startedAt).toLocaleString()})`);
|
|
54
|
+
}
|
|
55
|
+
console.log("\n c) Continue anyway (both will fight for events — not recommended)");
|
|
56
|
+
console.log(" k) Kill the old one and continue");
|
|
57
|
+
console.log(" a) Abort\n");
|
|
58
|
+
const choice = await prompt("Choice [c/k/a]: ");
|
|
59
|
+
if (/^k/i.test(choice)) {
|
|
60
|
+
procRegistry.killConflicts(config.app.appId);
|
|
61
|
+
}
|
|
62
|
+
else if (!/^c/i.test(choice)) {
|
|
63
|
+
console.log("Aborted.");
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.error(chalk.red(`Another instance is running for appId ${config.app.appId}. Set BUBBLE_KILL_OLD=1 to kill it, or run without -y to interactively resolve.`));
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
procRegistry.register({
|
|
73
|
+
pid: process.pid,
|
|
74
|
+
appId: config.app.appId,
|
|
75
|
+
startedAt: Date.now(),
|
|
76
|
+
cwd: process.cwd(),
|
|
77
|
+
});
|
|
78
|
+
// 3. Process-level dependencies (shared across scopes)
|
|
79
|
+
const userConfig = new UserConfig();
|
|
80
|
+
const providerRegistry = new ProviderRegistry(userConfig);
|
|
81
|
+
// Use the user's home as the "root" for settings/skills/MCP discovery.
|
|
82
|
+
// Per-scope cwd overrides happen at the run-driver level (future work).
|
|
83
|
+
const rootCwd = homedir();
|
|
84
|
+
const settingsManager = new SettingsManager(rootCwd);
|
|
85
|
+
const skillRegistry = new SkillRegistry({
|
|
86
|
+
cwd: rootCwd,
|
|
87
|
+
skillPaths: userConfig.getSkillPaths(),
|
|
88
|
+
});
|
|
89
|
+
const mcpLoaded = loadMcpConfig({ cwd: rootCwd });
|
|
90
|
+
const mcpManager = new McpManager({ servers: mcpLoaded.servers });
|
|
91
|
+
if (mcpLoaded.servers.length > 0) {
|
|
92
|
+
await mcpManager.start();
|
|
93
|
+
}
|
|
94
|
+
const createProvider = (providerId, apiKey, baseURL, promptCacheKey) => createProviderInstance({ providerId, apiKey, baseURL, promptCacheKey });
|
|
95
|
+
const createProviderForRoute = async (route, promptCacheKey) => {
|
|
96
|
+
const target = providerRegistry.getConfigured().find((p) => p.id === route.providerId);
|
|
97
|
+
if (!target?.apiKey) {
|
|
98
|
+
throw new Error(`Subagent route requires provider "${route.providerId}", not configured.`);
|
|
99
|
+
}
|
|
100
|
+
return createProvider(route.providerId, target.apiKey, target.baseURL, promptCacheKey);
|
|
101
|
+
};
|
|
102
|
+
const deps = {
|
|
103
|
+
settingsManager,
|
|
104
|
+
providerRegistry,
|
|
105
|
+
userConfig,
|
|
106
|
+
skillRegistry,
|
|
107
|
+
mcpManager,
|
|
108
|
+
createProvider,
|
|
109
|
+
createProviderForRoute,
|
|
110
|
+
ownerOpenId: config.app.ownerOpenId,
|
|
111
|
+
};
|
|
112
|
+
// 4. Persistence stores
|
|
113
|
+
const scopeRegistry = ScopeRegistry.load();
|
|
114
|
+
const sessionStore = SessionStore.load();
|
|
115
|
+
const sessionBinder = new SessionBinder(sessionStore);
|
|
116
|
+
// 5. Channel
|
|
117
|
+
const channel = createBubbleChannel({
|
|
118
|
+
appId: config.app.appId,
|
|
119
|
+
appSecret,
|
|
120
|
+
outputThrottleMs: config.preferences.outputThrottleMs,
|
|
121
|
+
requireMentionInGroup: config.preferences.requireMentionInGroup,
|
|
122
|
+
});
|
|
123
|
+
// 6. Logger
|
|
124
|
+
const logger = new FeishuLogger();
|
|
125
|
+
logger.pruneOldLogs(7);
|
|
126
|
+
// 7. Approval UI (shared across all scopes; clicker-restricted enforces per-scope safety)
|
|
127
|
+
const approvalUI = new FeishuApprovalUI({
|
|
128
|
+
sendCard: async (chatId, card) => {
|
|
129
|
+
const res = await channel.send(chatId, { card }, undefined);
|
|
130
|
+
return { messageId: res.messageId };
|
|
131
|
+
},
|
|
132
|
+
updateCard: (messageId, card) => channel.updateCard(messageId, card),
|
|
133
|
+
bashAllowlist: new BashAllowlist(),
|
|
134
|
+
timeoutMs: 60_000,
|
|
135
|
+
});
|
|
136
|
+
// 8. Runtime control
|
|
137
|
+
const activeRuns = new ActiveRuns();
|
|
138
|
+
const processPool = new ProcessPool({ concurrency: config.globalLimits.maxConcurrentRuns });
|
|
139
|
+
// 9. Run driver
|
|
140
|
+
const driver = new RunDriver({
|
|
141
|
+
channel,
|
|
142
|
+
deps,
|
|
143
|
+
binder: sessionBinder,
|
|
144
|
+
approvalUI,
|
|
145
|
+
outputThrottleMs: config.preferences.outputThrottleMs,
|
|
146
|
+
idleTimeoutMinutes: config.preferences.idleTimeoutMinutes,
|
|
147
|
+
maxBytesPerElement: config.preferences.maxBytesPerElement,
|
|
148
|
+
maxBytesPerCard: config.preferences.maxBytesPerCard,
|
|
149
|
+
});
|
|
150
|
+
// 10. PendingQueue with flush → run-driver
|
|
151
|
+
const pendingQueue = new PendingQueue({
|
|
152
|
+
debounceMs: 600,
|
|
153
|
+
onFlush: async (scopeKey, batch) => {
|
|
154
|
+
const parsed = parseScopeKey(scopeKey);
|
|
155
|
+
if (!parsed)
|
|
156
|
+
return;
|
|
157
|
+
const scope = scopeRegistry.get(parsed.chatId);
|
|
158
|
+
if (!scope)
|
|
159
|
+
return;
|
|
160
|
+
// Block further flushes for this scope while the run is in flight.
|
|
161
|
+
pendingQueue.block(scopeKey);
|
|
162
|
+
const { signal, complete } = await activeRuns.startOrReplace(scopeKey);
|
|
163
|
+
try {
|
|
164
|
+
await processPool.run(async () => driver.runOnce({
|
|
165
|
+
scopeKey,
|
|
166
|
+
scope,
|
|
167
|
+
chatId: parsed.chatId,
|
|
168
|
+
userId: parsed.userId,
|
|
169
|
+
prompt: combineQueuedMessages(batch),
|
|
170
|
+
replyToMessageId: batch[0]?.messageId,
|
|
171
|
+
abortSignal: signal,
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
logger.error("run_driver_error", {
|
|
176
|
+
phase: "runtime",
|
|
177
|
+
scope: scopeKey,
|
|
178
|
+
error: serializeError(err),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
complete();
|
|
183
|
+
pendingQueue.unblock(scopeKey);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
// 11. Event router
|
|
188
|
+
const router = new EventRouter({
|
|
189
|
+
channel,
|
|
190
|
+
scopeRegistry,
|
|
191
|
+
sessionStore,
|
|
192
|
+
activeRuns,
|
|
193
|
+
pendingQueue,
|
|
194
|
+
approvalUI,
|
|
195
|
+
logger,
|
|
196
|
+
requireMentionInGroup: config.preferences.requireMentionInGroup,
|
|
197
|
+
commandContext: {
|
|
198
|
+
channel,
|
|
199
|
+
scopeRegistry,
|
|
200
|
+
sessionStore,
|
|
201
|
+
sessionBinder,
|
|
202
|
+
activeRuns,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
// 12. Channel events for status/log
|
|
206
|
+
channel.onError((err) => {
|
|
207
|
+
logger.error("channel_error", { phase: "channel", error: serializeError(err) });
|
|
208
|
+
console.error(chalk.red(`[channel] ${err.message}`));
|
|
209
|
+
});
|
|
210
|
+
channel.onReconnecting(() => {
|
|
211
|
+
logger.warn("channel_reconnecting", { phase: "channel" });
|
|
212
|
+
console.log(chalk.yellow("[channel] reconnecting…"));
|
|
213
|
+
});
|
|
214
|
+
channel.onReconnected(() => {
|
|
215
|
+
logger.info("channel_reconnected", { phase: "channel" });
|
|
216
|
+
console.log(chalk.green("[channel] reconnected"));
|
|
217
|
+
});
|
|
218
|
+
router.start();
|
|
219
|
+
// 13. Shutdown
|
|
220
|
+
let shuttingDown = false;
|
|
221
|
+
let resolveServeDone;
|
|
222
|
+
const serveDone = new Promise((resolve) => {
|
|
223
|
+
resolveServeDone = resolve;
|
|
224
|
+
});
|
|
225
|
+
const shutdown = async (signal) => {
|
|
226
|
+
if (shuttingDown)
|
|
227
|
+
return;
|
|
228
|
+
shuttingDown = true;
|
|
229
|
+
console.log(chalk.dim(`\nGot ${signal}, shutting down…`));
|
|
230
|
+
router.stop();
|
|
231
|
+
pendingQueue.shutdown();
|
|
232
|
+
const aborted = activeRuns.abortAll();
|
|
233
|
+
if (aborted > 0)
|
|
234
|
+
console.log(chalk.dim(`Aborted ${aborted} active run(s).`));
|
|
235
|
+
approvalUI.cancelAll("Shutdown");
|
|
236
|
+
await activeRuns.waitAll(8_000);
|
|
237
|
+
try {
|
|
238
|
+
await channel.disconnect();
|
|
239
|
+
}
|
|
240
|
+
catch { /* */ }
|
|
241
|
+
try {
|
|
242
|
+
await mcpManager.shutdown();
|
|
243
|
+
}
|
|
244
|
+
catch { /* */ }
|
|
245
|
+
procRegistry.deregister(process.pid);
|
|
246
|
+
resolveServeDone?.();
|
|
247
|
+
};
|
|
248
|
+
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
249
|
+
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
250
|
+
// 14. Connect
|
|
251
|
+
console.log(chalk.dim(`\nConnecting to Feishu (app ${config.app.appId})…`));
|
|
252
|
+
try {
|
|
253
|
+
await channel.connect();
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
console.error(chalk.red(`Failed to connect: ${err.message}`));
|
|
257
|
+
procRegistry.deregister(process.pid);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
const botId = channel.botOpenId();
|
|
261
|
+
const scopesCount = scopeRegistry.list().length;
|
|
262
|
+
console.log(chalk.green(`✅ Listening on Feishu.`));
|
|
263
|
+
console.log(chalk.dim(` bot open_id: ${botId ?? "(unknown)"}`));
|
|
264
|
+
console.log(chalk.dim(` ${scopesCount} scope${scopesCount === 1 ? "" : "s"} configured`));
|
|
265
|
+
console.log(chalk.dim("\nSend a message to your bot to start. /help in chat for commands."));
|
|
266
|
+
if (opts.dryRun) {
|
|
267
|
+
console.log(chalk.dim("\n--dry-run set; exiting after successful connect."));
|
|
268
|
+
await shutdown("dry-run");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Block here until SIGINT/SIGTERM triggers shutdown. Without this await
|
|
272
|
+
// the function would return, main() would resolve, and exitAfterFlush()
|
|
273
|
+
// in main.ts would call process.exit(0) — killing the freshly-connected
|
|
274
|
+
// service. The LarkChannel WebSocket alone doesn't always keep Node's
|
|
275
|
+
// event loop alive (e.g., under detached spawn where stdin is /dev/null).
|
|
276
|
+
await serveDone;
|
|
277
|
+
}
|
|
278
|
+
function prompt(question) {
|
|
279
|
+
return new Promise((resolve) => {
|
|
280
|
+
process.stdout.write(question);
|
|
281
|
+
process.stdin.once("data", (data) => resolve(String(data).trim()));
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function serializeError(err) {
|
|
285
|
+
if (err instanceof Error)
|
|
286
|
+
return { message: err.message, name: err.name, stack: err.stack };
|
|
287
|
+
return { message: String(err) };
|
|
288
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the Feishu host.
|
|
3
|
+
*/
|
|
4
|
+
import type { PermissionMode } from "../types.js";
|
|
5
|
+
export interface FeishuConfig {
|
|
6
|
+
version: 1;
|
|
7
|
+
app: {
|
|
8
|
+
appId: string;
|
|
9
|
+
secretRef: SecretRef;
|
|
10
|
+
ownerOpenId: string;
|
|
11
|
+
encryptCheck: string;
|
|
12
|
+
};
|
|
13
|
+
preferences: {
|
|
14
|
+
outputThrottleMs: number;
|
|
15
|
+
idleTimeoutMinutes: number;
|
|
16
|
+
renderMode: "card" | "markdown" | "text";
|
|
17
|
+
requireMentionInGroup: boolean;
|
|
18
|
+
maxBytesPerElement: number;
|
|
19
|
+
maxBytesPerCard: number;
|
|
20
|
+
};
|
|
21
|
+
globalLimits: {
|
|
22
|
+
maxConcurrentRuns: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export type SecretRef = {
|
|
26
|
+
source: "keystore";
|
|
27
|
+
name: string;
|
|
28
|
+
} | {
|
|
29
|
+
source: "env";
|
|
30
|
+
varName: string;
|
|
31
|
+
};
|
|
32
|
+
export interface ScopesFile {
|
|
33
|
+
version: 1;
|
|
34
|
+
scopes: Record<string, ScopeConfig>;
|
|
35
|
+
}
|
|
36
|
+
export interface ScopeConfig {
|
|
37
|
+
/** Initial cwd used the first time this scope sees traffic. */
|
|
38
|
+
cwd: string;
|
|
39
|
+
displayName: string;
|
|
40
|
+
allowedUsers: string[];
|
|
41
|
+
admins: string[];
|
|
42
|
+
defaultPermissionMode: PermissionMode;
|
|
43
|
+
model: string | null;
|
|
44
|
+
createdAt: number;
|
|
45
|
+
lastActiveAt: number;
|
|
46
|
+
}
|
|
47
|
+
export interface SessionsFile {
|
|
48
|
+
version: 1;
|
|
49
|
+
sessions: Record<string, SessionEntry>;
|
|
50
|
+
}
|
|
51
|
+
/** Keyed by `<chatId>:<userId>` in sessions.json. */
|
|
52
|
+
export interface SessionEntry {
|
|
53
|
+
sessionFile: string;
|
|
54
|
+
cwd: string;
|
|
55
|
+
permissionMode: PermissionMode;
|
|
56
|
+
lastActiveAt: number;
|
|
57
|
+
}
|
|
58
|
+
export interface ProcessRegistryFile {
|
|
59
|
+
version: 1;
|
|
60
|
+
processes: ProcessRegistryEntry[];
|
|
61
|
+
}
|
|
62
|
+
export interface ProcessRegistryEntry {
|
|
63
|
+
pid: number;
|
|
64
|
+
appId: string;
|
|
65
|
+
startedAt: number;
|
|
66
|
+
cwd: string;
|
|
67
|
+
}
|
|
68
|
+
export declare const DEFAULT_PREFERENCES: FeishuConfig["preferences"];
|
|
69
|
+
export declare const DEFAULT_GLOBAL_LIMITS: FeishuConfig["globalLimits"];
|
|
70
|
+
export type ScopeKey = string;
|
|
71
|
+
export declare function makeScopeKey(chatId: string, userId: string): ScopeKey;
|
|
72
|
+
export declare function parseScopeKey(key: ScopeKey): {
|
|
73
|
+
chatId: string;
|
|
74
|
+
userId: string;
|
|
75
|
+
} | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the Feishu host.
|
|
3
|
+
*/
|
|
4
|
+
export const DEFAULT_PREFERENCES = {
|
|
5
|
+
outputThrottleMs: 400,
|
|
6
|
+
idleTimeoutMinutes: 15,
|
|
7
|
+
renderMode: "card",
|
|
8
|
+
requireMentionInGroup: true,
|
|
9
|
+
maxBytesPerElement: 28000,
|
|
10
|
+
maxBytesPerCard: 140000,
|
|
11
|
+
};
|
|
12
|
+
export const DEFAULT_GLOBAL_LIMITS = {
|
|
13
|
+
maxConcurrentRuns: 5,
|
|
14
|
+
};
|
|
15
|
+
export function makeScopeKey(chatId, userId) {
|
|
16
|
+
return `${chatId}:${userId}`;
|
|
17
|
+
}
|
|
18
|
+
export function parseScopeKey(key) {
|
|
19
|
+
const idx = key.indexOf(":");
|
|
20
|
+
if (idx <= 0)
|
|
21
|
+
return undefined;
|
|
22
|
+
return { chatId: key.slice(0, idx), userId: key.slice(idx + 1) };
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-time setup wizard.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Call `registerApp` from the SDK — it returns a QR code URL that
|
|
6
|
+
* points at Feishu's app-registration page; user scans on phone to
|
|
7
|
+
* create an app and authorize us.
|
|
8
|
+
* 2. We receive `{ client_id, client_secret, user_info.open_id }`.
|
|
9
|
+
* 3. Encrypt the secret to `secrets.enc`, write `config.json`, prompt
|
|
10
|
+
* the user (in terminal) for the first scope (chat_id + cwd).
|
|
11
|
+
* 4. Persist the scope to `scopes.json`.
|
|
12
|
+
*
|
|
13
|
+
* The terminal prompts use a tiny line-mode reader (readline). We don't
|
|
14
|
+
* pull in @clack to keep dependencies flat.
|
|
15
|
+
*/
|
|
16
|
+
import type { FeishuConfig, ScopeConfig } from "./types.js";
|
|
17
|
+
export interface WizardResult {
|
|
18
|
+
config: FeishuConfig;
|
|
19
|
+
firstScope?: {
|
|
20
|
+
chatId: string;
|
|
21
|
+
scope: ScopeConfig;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare function runWizard(): Promise<WizardResult>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-time setup wizard.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Call `registerApp` from the SDK — it returns a QR code URL that
|
|
6
|
+
* points at Feishu's app-registration page; user scans on phone to
|
|
7
|
+
* create an app and authorize us.
|
|
8
|
+
* 2. We receive `{ client_id, client_secret, user_info.open_id }`.
|
|
9
|
+
* 3. Encrypt the secret to `secrets.enc`, write `config.json`, prompt
|
|
10
|
+
* the user (in terminal) for the first scope (chat_id + cwd).
|
|
11
|
+
* 4. Persist the scope to `scopes.json`.
|
|
12
|
+
*
|
|
13
|
+
* The terminal prompts use a tiny line-mode reader (readline). We don't
|
|
14
|
+
* pull in @clack to keep dependencies flat.
|
|
15
|
+
*/
|
|
16
|
+
import { registerApp } from "@larksuiteoapi/node-sdk";
|
|
17
|
+
import qrTerminal from "qrcode-terminal";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
import { createInterface } from "node:readline";
|
|
20
|
+
import { existsSync, statSync } from "node:fs";
|
|
21
|
+
import { resolve as resolvePath, isAbsolute } from "node:path";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { bootstrapConfig } from "./config.js";
|
|
24
|
+
import { ScopeRegistry } from "./scope/scope-registry.js";
|
|
25
|
+
export async function runWizard() {
|
|
26
|
+
console.log(chalk.bold("\n🫧 Bubble Feishu setup\n"));
|
|
27
|
+
console.log("This will register a Feishu personal-agent app, encrypt the secret to your");
|
|
28
|
+
console.log("keystore, and let you bind your first chat to a local directory.\n");
|
|
29
|
+
const registered = await runQrFlow();
|
|
30
|
+
console.log(chalk.green(`✅ Registered. owner open_id: ${registered.userInfo?.open_id ?? "(unknown)"}\n`));
|
|
31
|
+
const ownerOpenId = registered.userInfo?.open_id ?? "";
|
|
32
|
+
if (!ownerOpenId) {
|
|
33
|
+
throw new Error("registerApp did not return user_info.open_id — cannot continue.");
|
|
34
|
+
}
|
|
35
|
+
const config = bootstrapConfig({
|
|
36
|
+
appId: registered.clientId,
|
|
37
|
+
appSecret: registered.clientSecret,
|
|
38
|
+
ownerOpenId,
|
|
39
|
+
});
|
|
40
|
+
console.log(chalk.dim(`Wrote config + encrypted secret to ~/.bubble/feishu/\n`));
|
|
41
|
+
// Optional first-scope binding.
|
|
42
|
+
console.log("Want to bind a chat to a local directory now? You can also do this later by");
|
|
43
|
+
console.log("editing ~/.bubble/feishu/scopes.json directly.\n");
|
|
44
|
+
const wantBind = await ask("Bind a chat now? [y/N]: ");
|
|
45
|
+
if (!/^y/i.test(wantBind.trim())) {
|
|
46
|
+
return { config };
|
|
47
|
+
}
|
|
48
|
+
const chatId = (await ask("Chat ID (oc_...): ")).trim();
|
|
49
|
+
if (!chatId.startsWith("oc_")) {
|
|
50
|
+
console.log(chalk.yellow("Chat IDs typically start with `oc_`. Continuing anyway."));
|
|
51
|
+
}
|
|
52
|
+
const cwdInput = (await ask(`Local cwd to bind (e.g. ${homedir()}/projects/my-app): `)).trim();
|
|
53
|
+
const expandedCwd = expandUser(cwdInput);
|
|
54
|
+
if (!isAbsolute(expandedCwd) || !existsSync(expandedCwd) || !statSync(expandedCwd).isDirectory()) {
|
|
55
|
+
throw new Error(`Invalid cwd: ${expandedCwd} (must be an existing absolute directory)`);
|
|
56
|
+
}
|
|
57
|
+
const displayName = (await ask("Display name (short label for the card header): ")).trim() || basenameSafe(expandedCwd);
|
|
58
|
+
const scope = {
|
|
59
|
+
cwd: expandedCwd,
|
|
60
|
+
displayName,
|
|
61
|
+
allowedUsers: [ownerOpenId],
|
|
62
|
+
admins: [ownerOpenId],
|
|
63
|
+
defaultPermissionMode: "default",
|
|
64
|
+
model: null,
|
|
65
|
+
createdAt: Date.now(),
|
|
66
|
+
lastActiveAt: Date.now(),
|
|
67
|
+
};
|
|
68
|
+
const registry = ScopeRegistry.load();
|
|
69
|
+
registry.upsert(chatId, scope);
|
|
70
|
+
console.log(chalk.green(`\n✅ Bound chat ${chatId} → ${expandedCwd}\n`));
|
|
71
|
+
return { config, firstScope: { chatId, scope } };
|
|
72
|
+
}
|
|
73
|
+
async function runQrFlow() {
|
|
74
|
+
console.log("Opening QR code below. Scan with your Feishu mobile app and authorize.\n");
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let printed = false;
|
|
77
|
+
void registerApp({
|
|
78
|
+
onQRCodeReady: (info) => {
|
|
79
|
+
if (!printed) {
|
|
80
|
+
qrTerminal.generate(info.url, { small: true }, (code) => {
|
|
81
|
+
process.stdout.write(code + "\n");
|
|
82
|
+
console.log(chalk.dim(`(QR expires in ${info.expireIn}s)`));
|
|
83
|
+
});
|
|
84
|
+
printed = true;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
onStatusChange: (info) => {
|
|
88
|
+
if (info.status === "slow_down")
|
|
89
|
+
console.log(chalk.dim("(polling slowed — still waiting…)"));
|
|
90
|
+
if (info.status === "domain_switched")
|
|
91
|
+
console.log(chalk.dim("(domain switched)"));
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
.then((res) => {
|
|
95
|
+
resolve({
|
|
96
|
+
clientId: res.client_id,
|
|
97
|
+
clientSecret: res.client_secret,
|
|
98
|
+
userInfo: res.user_info,
|
|
99
|
+
});
|
|
100
|
+
})
|
|
101
|
+
.catch(reject);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function ask(prompt) {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
107
|
+
rl.question(prompt, (answer) => {
|
|
108
|
+
rl.close();
|
|
109
|
+
resolve(answer);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function expandUser(p) {
|
|
114
|
+
if (p === "~" || p.startsWith("~/"))
|
|
115
|
+
return homedir() + p.slice(1);
|
|
116
|
+
return resolvePath(p);
|
|
117
|
+
}
|
|
118
|
+
function basenameSafe(p) {
|
|
119
|
+
const parts = p.split(/[\\/]/);
|
|
120
|
+
return parts[parts.length - 1] || p;
|
|
121
|
+
}
|