@bubblebrain-ai/bubble 0.0.9 → 0.0.11
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 +5 -0
- 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 +295 -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 +285 -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 +78 -29
- package/dist/model-catalog.js +3 -0
- package/dist/session.d.ts +11 -0
- package/dist/session.js +88 -2
- package/dist/slash-commands/commands.js +13 -0
- package/dist/slash-commands/feishu.d.ts +17 -0
- package/dist/slash-commands/feishu.js +400 -0
- package/dist/slash-commands/types.d.ts +3 -1
- package/dist/tui-ink/app.js +218 -60
- 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 +3 -0
- package/dist/tui-ink/input-box.js +27 -0
- 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 +85 -34
- package/dist/tui-ink/model-picker.js +1 -4
- 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 +112 -0
- package/dist/tui-ink/terminal-mouse.d.ts +4 -0
- package/dist/tui-ink/terminal-mouse.js +23 -0
- 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) => createProviderInstance({ providerId, apiKey, baseURL });
|
|
95
|
+
const createProviderForRoute = async (route) => {
|
|
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);
|
|
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
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -23,12 +23,26 @@ import { loadMcpConfig } from "./mcp/config.js";
|
|
|
23
23
|
import { McpManager } from "./mcp/manager.js";
|
|
24
24
|
import { QuestionController } from "./question/index.js";
|
|
25
25
|
import { buildMemoryPrompt, formatMemoryStartupResult, recordMemoryCitations, runMemoryPhase2, runMemoryStartupPipeline, startMemoryStartupTask, } from "./memory/index.js";
|
|
26
|
+
import { basename } from "node:path";
|
|
26
27
|
async function main() {
|
|
27
28
|
const args = parseArgs(process.argv.slice(2));
|
|
28
29
|
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
29
30
|
printHelp();
|
|
30
31
|
process.exit(0);
|
|
31
32
|
}
|
|
33
|
+
if (args.command === "serve") {
|
|
34
|
+
if (args.serveHost !== "feishu") {
|
|
35
|
+
console.error(chalk.red("Usage: bubble serve --feishu [--setup | --kill-old | --dry-run]"));
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
const { serveFeishu } = await import("./feishu/index.js");
|
|
39
|
+
await serveFeishu({
|
|
40
|
+
setup: args.setup,
|
|
41
|
+
killOld: args.killOld,
|
|
42
|
+
dryRun: args.dryRun,
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
32
46
|
const userConfig = new UserConfig();
|
|
33
47
|
const registry = new ProviderRegistry(userConfig);
|
|
34
48
|
const skillRegistry = new SkillRegistry({
|
|
@@ -156,11 +170,46 @@ async function main() {
|
|
|
156
170
|
process.once("SIGTERM", () => { void shutdownMcp().then(() => process.exit(143)); });
|
|
157
171
|
// Session management:
|
|
158
172
|
// - default: always start a fresh session
|
|
159
|
-
// - --resume
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
// - --resume --session <name>: restore the named session
|
|
174
|
+
// - --resume (no name): show interactive picker
|
|
175
|
+
let sessionManager;
|
|
176
|
+
let resumedExistingSession = false;
|
|
177
|
+
// Resolved before any Ink render so picker and main TUI share the same value
|
|
178
|
+
// and we only run OSC 11 once.
|
|
179
|
+
let preResolvedTheme;
|
|
180
|
+
if (args.resume && !args.sessionName) {
|
|
181
|
+
const currentSessions = SessionManager.summarizeSessionsForCwd(args.cwd);
|
|
182
|
+
const allSessions = SessionManager.listAllSessions();
|
|
183
|
+
if (currentSessions.length === 0 && allSessions.length === 0) {
|
|
184
|
+
console.log(chalk.dim("No previous sessions found — starting a fresh one."));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const themeConfig = userConfig.getTheme();
|
|
188
|
+
if (themeConfig.mode === "auto") {
|
|
189
|
+
const { detectTerminalTheme } = await import("./tui-ink/detect-theme.js");
|
|
190
|
+
preResolvedTheme = await detectTerminalTheme();
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
preResolvedTheme = themeConfig.mode;
|
|
194
|
+
}
|
|
195
|
+
const { runSessionPicker } = await import("./tui-ink/run-session-picker.js");
|
|
196
|
+
const picked = await runSessionPicker({
|
|
197
|
+
currentCwd: args.cwd,
|
|
198
|
+
currentSessions,
|
|
199
|
+
allSessions,
|
|
200
|
+
resolvedTheme: preResolvedTheme,
|
|
201
|
+
themeOverrides: themeConfig.overrides,
|
|
202
|
+
});
|
|
203
|
+
if (picked) {
|
|
204
|
+
sessionManager = new SessionManager(picked);
|
|
205
|
+
resumedExistingSession = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else if (args.resume) {
|
|
210
|
+
sessionManager = SessionManager.resume(args.cwd, args.sessionName);
|
|
211
|
+
resumedExistingSession = !!sessionManager;
|
|
212
|
+
}
|
|
164
213
|
if (!sessionManager) {
|
|
165
214
|
sessionManager = args.sessionName && !args.resume
|
|
166
215
|
? SessionManager.create(args.cwd, args.sessionName)
|
|
@@ -350,8 +399,20 @@ async function main() {
|
|
|
350
399
|
console.log();
|
|
351
400
|
return;
|
|
352
401
|
}
|
|
353
|
-
const tuiRuntime = process.env.BUBBLE_TUI === "opentui" ? "opentui" : "ink";
|
|
354
402
|
const themeConfig = userConfig.getTheme();
|
|
403
|
+
let detectedTheme = "dark";
|
|
404
|
+
if (preResolvedTheme) {
|
|
405
|
+
detectedTheme = preResolvedTheme;
|
|
406
|
+
}
|
|
407
|
+
else if (themeConfig.mode === "auto") {
|
|
408
|
+
// Probe before either TUI runtime owns stdin. OSC 11 needs raw mode, and
|
|
409
|
+
// runtime renderers can consume the reply before startup code sees it.
|
|
410
|
+
const { detectTerminalTheme } = await import("./tui-ink/detect-theme.js");
|
|
411
|
+
detectedTheme = await detectTerminalTheme();
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
detectedTheme = themeConfig.mode;
|
|
415
|
+
}
|
|
355
416
|
const commonOptions = {
|
|
356
417
|
sessionManager,
|
|
357
418
|
createProvider,
|
|
@@ -369,29 +430,17 @@ async function main() {
|
|
|
369
430
|
runMemorySummary,
|
|
370
431
|
runMemoryRefresh,
|
|
371
432
|
};
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (themeConfig.mode === "auto") {
|
|
384
|
-
const { detectTerminalTheme } = await import("./tui-ink/detect-theme.js");
|
|
385
|
-
detectedTheme = await detectTerminalTheme();
|
|
386
|
-
}
|
|
387
|
-
const { runTui } = await import("./tui-ink/run.js");
|
|
388
|
-
await runTui(agent, args, {
|
|
389
|
-
...commonOptions,
|
|
390
|
-
themeMode: themeConfig.mode,
|
|
391
|
-
themeOverrides: themeConfig.overrides,
|
|
392
|
-
detectedTheme,
|
|
393
|
-
onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
|
|
394
|
-
});
|
|
433
|
+
const { runTui } = await import("./tui-ink/run.js");
|
|
434
|
+
await runTui(agent, args, {
|
|
435
|
+
...commonOptions,
|
|
436
|
+
themeMode: themeConfig.mode,
|
|
437
|
+
themeOverrides: themeConfig.overrides,
|
|
438
|
+
detectedTheme,
|
|
439
|
+
onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
|
|
440
|
+
});
|
|
441
|
+
if (sessionManager) {
|
|
442
|
+
const sessionName = basename(sessionManager.getSessionFile());
|
|
443
|
+
console.log(chalk.dim(`To resume: bubble --resume (or --resume --session ${sessionName})`));
|
|
395
444
|
}
|
|
396
445
|
}
|
|
397
446
|
finally {
|