@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
package/dist/model-catalog.js
CHANGED
|
@@ -8,6 +8,7 @@ export const BUILTIN_PROVIDERS = [
|
|
|
8
8
|
{ id: "zhipuai-coding-plan", name: "Zhipu AI Coding Plan", baseURL: "https://open.bigmodel.cn/api/coding/paas/v4" },
|
|
9
9
|
{ id: "zai", name: "Z.AI", baseURL: "https://api.z.ai/api/paas/v4" },
|
|
10
10
|
{ id: "zai-coding-plan", name: "Z.AI Coding Plan", baseURL: "https://api.z.ai/api/coding/paas/v4" },
|
|
11
|
+
{ id: "alibaba", name: "Alibaba DashScope", baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1" },
|
|
11
12
|
{ id: "moonshot-cn", name: "Moonshot (国内 platform.moonshot.cn)", baseURL: "https://api.moonshot.cn/v1" },
|
|
12
13
|
{ id: "moonshot-intl", name: "Moonshot (海外 platform.moonshot.ai)", baseURL: "https://api.moonshot.ai/v1" },
|
|
13
14
|
{ id: "kimi-for-coding", name: "Kimi for Coding", baseURL: "https://api.kimi.com/coding/v1" },
|
|
@@ -55,6 +56,8 @@ export const BUILTIN_MODELS = [
|
|
|
55
56
|
{ id: "glm-5-turbo", name: "GLM-5-Turbo", providerId: "zai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
|
|
56
57
|
{ id: "glm-4.7", name: "GLM-4.7", providerId: "zai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
|
|
57
58
|
{ id: "glm-4.6", name: "GLM-4.6", providerId: "zai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
|
|
59
|
+
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", providerId: "alibaba", reasoningLevels: ["off"], contextWindow: 1048576 },
|
|
60
|
+
{ id: "qwen3.7-max", name: "Qwen3.7 Max", providerId: "alibaba", reasoningLevels: ["off"], contextWindow: 1048576 },
|
|
58
61
|
{ id: "kimi-k2.6", name: "Kimi K2.6", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
|
59
62
|
{ id: "k2.6-code-preview", name: "Kimi K2.6 Code Preview", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
|
60
63
|
{ id: "kimi-k2.5", name: "Kimi K2.5", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
|
package/dist/session.d.ts
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
import { type CompactOptions, type CompactResult } from "./context/compact.js";
|
|
5
5
|
import type { Message, Todo } from "./types.js";
|
|
6
6
|
import type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
|
|
7
|
+
export interface SessionSummary {
|
|
8
|
+
file: string;
|
|
9
|
+
name: string;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
cwdLabel: string;
|
|
12
|
+
firstUserMessage: string;
|
|
13
|
+
messageCount: number;
|
|
14
|
+
mtime: number;
|
|
15
|
+
}
|
|
7
16
|
export type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
|
|
8
17
|
export declare class SessionManager {
|
|
9
18
|
private sessionFile;
|
|
@@ -13,6 +22,8 @@ export declare class SessionManager {
|
|
|
13
22
|
static resume(cwd: string, sessionName?: string): SessionManager | undefined;
|
|
14
23
|
static createFresh(cwd: string): SessionManager;
|
|
15
24
|
static listSessions(cwd: string): string[];
|
|
25
|
+
static summarizeSessionsForCwd(cwd: string): SessionSummary[];
|
|
26
|
+
static listAllSessions(): SessionSummary[];
|
|
16
27
|
private load;
|
|
17
28
|
private persist;
|
|
18
29
|
private rewrite;
|
package/dist/session.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session Manager - Append-only JSONL persistence over a structured session log.
|
|
3
3
|
*/
|
|
4
|
-
import { mkdirSync, appendFileSync, existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { mkdirSync, appendFileSync, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
6
|
import { getBubbleHome } from "./bubble-home.js";
|
|
7
7
|
import { compactSessionEntries } from "./context/compact.js";
|
|
8
8
|
import { SessionLog } from "./session-log.js";
|
|
@@ -42,6 +42,44 @@ export class SessionManager {
|
|
|
42
42
|
return [];
|
|
43
43
|
return readdirSync(sessionsDir).filter((file) => file.endsWith(".jsonl"));
|
|
44
44
|
}
|
|
45
|
+
static summarizeSessionsForCwd(cwd) {
|
|
46
|
+
const dir = getSessionsDir(cwd);
|
|
47
|
+
if (!existsSync(dir))
|
|
48
|
+
return [];
|
|
49
|
+
const summaries = [];
|
|
50
|
+
for (const file of readdirSync(dir)) {
|
|
51
|
+
if (!file.endsWith(".jsonl"))
|
|
52
|
+
continue;
|
|
53
|
+
const summary = summarizeSessionFile(join(dir, file), basename(dir));
|
|
54
|
+
if (summary)
|
|
55
|
+
summaries.push(summary);
|
|
56
|
+
}
|
|
57
|
+
return summaries.sort((a, b) => b.mtime - a.mtime);
|
|
58
|
+
}
|
|
59
|
+
static listAllSessions() {
|
|
60
|
+
const root = join(getBubbleHome(), "sessions");
|
|
61
|
+
if (!existsSync(root))
|
|
62
|
+
return [];
|
|
63
|
+
const summaries = [];
|
|
64
|
+
for (const cwdDir of readdirSync(root)) {
|
|
65
|
+
const dir = join(root, cwdDir);
|
|
66
|
+
try {
|
|
67
|
+
if (!statSync(dir).isDirectory())
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
for (const file of readdirSync(dir)) {
|
|
74
|
+
if (!file.endsWith(".jsonl"))
|
|
75
|
+
continue;
|
|
76
|
+
const summary = summarizeSessionFile(join(dir, file), cwdDir);
|
|
77
|
+
if (summary)
|
|
78
|
+
summaries.push(summary);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return summaries.sort((a, b) => b.mtime - a.mtime);
|
|
82
|
+
}
|
|
45
83
|
load() {
|
|
46
84
|
const content = readFileSync(this.sessionFile, "utf-8");
|
|
47
85
|
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
@@ -133,3 +171,51 @@ export function getSessionsDir(cwd) {
|
|
|
133
171
|
function resolveSessionFile(cwd, sessionName) {
|
|
134
172
|
return join(getSessionsDir(cwd), sessionName);
|
|
135
173
|
}
|
|
174
|
+
function summarizeSessionFile(file, cwdDir) {
|
|
175
|
+
let stat;
|
|
176
|
+
try {
|
|
177
|
+
stat = statSync(file);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
let content;
|
|
183
|
+
try {
|
|
184
|
+
content = readFileSync(file, "utf-8");
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const lines = content.split("\n").filter((line) => line.trim() !== "");
|
|
190
|
+
if (lines.length === 0)
|
|
191
|
+
return undefined;
|
|
192
|
+
const log = new SessionLog();
|
|
193
|
+
log.load(lines);
|
|
194
|
+
const metadata = log.getMetadata();
|
|
195
|
+
const messages = log.toMessages();
|
|
196
|
+
const firstUser = messages.find((m) => m.role === "user");
|
|
197
|
+
let firstUserText = "";
|
|
198
|
+
if (firstUser) {
|
|
199
|
+
firstUserText = typeof firstUser.content === "string"
|
|
200
|
+
? firstUser.content
|
|
201
|
+
: firstUser.content.map((part) => part.type === "text" ? part.text : "").join("");
|
|
202
|
+
}
|
|
203
|
+
const snippet = firstUserText.trim().replace(/\s+/g, " ").slice(0, 80);
|
|
204
|
+
return {
|
|
205
|
+
file,
|
|
206
|
+
name: basename(file).replace(/\.jsonl$/, ""),
|
|
207
|
+
cwd: metadata.cwd,
|
|
208
|
+
cwdLabel: metadata.cwd ?? decodeCwdDir(cwdDir),
|
|
209
|
+
firstUserMessage: snippet || "(no user message)",
|
|
210
|
+
messageCount: messages.length,
|
|
211
|
+
mtime: stat.mtimeMs,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function decodeCwdDir(safe) {
|
|
215
|
+
// safeCwd is cwd.replace(/[/\\:]/g, "_") — not perfectly reversible because we
|
|
216
|
+
// can't tell underscores apart from path separators. For typical absolute
|
|
217
|
+
// Unix paths this still produces a readable approximation.
|
|
218
|
+
if (safe.startsWith("_"))
|
|
219
|
+
return "/" + safe.slice(1).replace(/_/g, "/");
|
|
220
|
+
return safe.replace(/_/g, "/");
|
|
221
|
+
}
|
|
@@ -9,6 +9,7 @@ import { buildSystemPrompt } from "../system-prompt.js";
|
|
|
9
9
|
import { formatLoadedSkill } from "../tools/skill.js";
|
|
10
10
|
import { isThinkingLevel } from "../variant/thinking-level.js";
|
|
11
11
|
import { buildMemoryPrompt, getMemoryStatus, isMemoryDisabled, resetMemory, searchMemory, } from "../memory/index.js";
|
|
12
|
+
import { feishuCommand } from "./feishu.js";
|
|
12
13
|
const VALID_SCOPES = ["user", "project", "local"];
|
|
13
14
|
const VALID_LISTS = ["allow", "deny"];
|
|
14
15
|
function isScope(value) {
|
|
@@ -782,10 +783,22 @@ const builtinSlashCommandEntries = [
|
|
|
782
783
|
...(systemMessage ? [systemMessage] : []),
|
|
783
784
|
...ctx.sessionManager.getMessages(),
|
|
784
785
|
];
|
|
786
|
+
ctx.agent.resetContextUsageAnchor();
|
|
785
787
|
const dropped = result.droppedEntries ?? 0;
|
|
786
788
|
return `✓ Compaction complete · ${dropped} log entr${dropped === 1 ? "y" : "ies"} summarized`;
|
|
787
789
|
},
|
|
788
790
|
},
|
|
791
|
+
{
|
|
792
|
+
name: "feedback",
|
|
793
|
+
description: "Send feedback or report a bug to Bubble developers",
|
|
794
|
+
async handler(args, ctx) {
|
|
795
|
+
if (!ctx.openFeedback) {
|
|
796
|
+
return "Feedback is only available in interactive TUI mode.";
|
|
797
|
+
}
|
|
798
|
+
ctx.openFeedback(args ?? "");
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
feishuCommand,
|
|
789
802
|
];
|
|
790
803
|
/**
|
|
791
804
|
* Public export — built-in commands tagged with `source: "builtin"` so the
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/feishu` slash command — control the Feishu remote-access serve process
|
|
3
|
+
* from inside the Bubble TUI without leaving it.
|
|
4
|
+
*
|
|
5
|
+
* Subcommands:
|
|
6
|
+
* /feishu equivalent to `/feishu status`
|
|
7
|
+
* /feishu status show running state and configured scopes
|
|
8
|
+
* /feishu start spawn `bubble serve --feishu` detached
|
|
9
|
+
* /feishu stop SIGTERM the running serve instance
|
|
10
|
+
* /feishu logs [N] tail last N lines of today's log (default 30)
|
|
11
|
+
*
|
|
12
|
+
* The serve subprocess runs independently of the TUI — closing the TUI
|
|
13
|
+
* does not stop it. Use `/feishu stop` (or kill the PID directly) to
|
|
14
|
+
* terminate.
|
|
15
|
+
*/
|
|
16
|
+
import type { SlashCommand } from "./types.js";
|
|
17
|
+
export declare const feishuCommand: SlashCommand;
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/feishu` slash command — control the Feishu remote-access serve process
|
|
3
|
+
* from inside the Bubble TUI without leaving it.
|
|
4
|
+
*
|
|
5
|
+
* Subcommands:
|
|
6
|
+
* /feishu equivalent to `/feishu status`
|
|
7
|
+
* /feishu status show running state and configured scopes
|
|
8
|
+
* /feishu start spawn `bubble serve --feishu` detached
|
|
9
|
+
* /feishu stop SIGTERM the running serve instance
|
|
10
|
+
* /feishu logs [N] tail last N lines of today's log (default 30)
|
|
11
|
+
*
|
|
12
|
+
* The serve subprocess runs independently of the TUI — closing the TUI
|
|
13
|
+
* does not stop it. Use `/feishu stop` (or kill the PID directly) to
|
|
14
|
+
* terminate.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { existsSync, openSync, readFileSync, statSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { configExists, loadConfig } from "../feishu/config.js";
|
|
20
|
+
import { getConfigPath, getLogsDir } from "../feishu/paths.js";
|
|
21
|
+
import { ProcessRegistry } from "../feishu/process-registry.js";
|
|
22
|
+
import { ScopeRegistry } from "../feishu/scope/scope-registry.js";
|
|
23
|
+
const SUBCOMMANDS = ["status", "setup", "start", "stop", "logs", "discover", "bind"];
|
|
24
|
+
export const feishuCommand = {
|
|
25
|
+
name: "feishu",
|
|
26
|
+
description: "Control the Feishu remote-access service (setup/status/start/stop/logs/discover/bind)",
|
|
27
|
+
async handler(args, ctx) {
|
|
28
|
+
const [sub, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
29
|
+
const cmd = (sub ?? "status").toLowerCase();
|
|
30
|
+
if (!SUBCOMMANDS.includes(cmd)) {
|
|
31
|
+
return `Unknown subcommand \`${cmd}\`. Usage: /feishu [setup|status|start|stop|logs|discover|bind]`;
|
|
32
|
+
}
|
|
33
|
+
switch (cmd) {
|
|
34
|
+
case "setup":
|
|
35
|
+
return runSetup(ctx);
|
|
36
|
+
case "status":
|
|
37
|
+
return runStatus();
|
|
38
|
+
case "start":
|
|
39
|
+
return runStart();
|
|
40
|
+
case "stop":
|
|
41
|
+
return runStop();
|
|
42
|
+
case "logs":
|
|
43
|
+
return runLogs(parseInt(rest[0] ?? "30", 10));
|
|
44
|
+
case "discover":
|
|
45
|
+
return runDiscover();
|
|
46
|
+
case "bind":
|
|
47
|
+
return runBind(rest);
|
|
48
|
+
default:
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
function runSetup(ctx) {
|
|
54
|
+
if (!ctx.openPicker) {
|
|
55
|
+
return "Setup wizard is only available in interactive TUI mode. Run `bubble serve --feishu --setup` from a shell instead.";
|
|
56
|
+
}
|
|
57
|
+
if (configExists()) {
|
|
58
|
+
return [
|
|
59
|
+
"已检测到现有 config (`~/.bubble/feishu/config.json`)。重新 setup 会覆盖现有的应用注册。",
|
|
60
|
+
"",
|
|
61
|
+
"如果只是想加新的 chat scope,编辑 `~/.bubble/feishu/scopes.json` 即可;要重置则先 `rm ~/.bubble/feishu/config.json ~/.bubble/feishu/secrets.enc` 再运行 `/feishu setup`。",
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
ctx.openPicker("feishu-setup");
|
|
65
|
+
}
|
|
66
|
+
function runStatus() {
|
|
67
|
+
if (!configExists()) {
|
|
68
|
+
return [
|
|
69
|
+
"Feishu serve is **not configured**.",
|
|
70
|
+
"",
|
|
71
|
+
"Run from a shell: `bubble serve --feishu --setup` to scan the QR code and create config.",
|
|
72
|
+
`Config path: \`${getConfigPath()}\``,
|
|
73
|
+
].join("\n");
|
|
74
|
+
}
|
|
75
|
+
let config;
|
|
76
|
+
try {
|
|
77
|
+
config = loadConfig();
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return `Failed to read config: ${err.message}`;
|
|
81
|
+
}
|
|
82
|
+
const procRegistry = new ProcessRegistry();
|
|
83
|
+
const conflicts = procRegistry.findConflicts(config.app.appId);
|
|
84
|
+
const running = conflicts.length > 0;
|
|
85
|
+
let scopesCount = 0;
|
|
86
|
+
try {
|
|
87
|
+
scopesCount = ScopeRegistry.load().list().length;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// ignore
|
|
91
|
+
}
|
|
92
|
+
const lines = [];
|
|
93
|
+
lines.push(`**Feishu serve status**`);
|
|
94
|
+
lines.push(`- app: \`${config.app.appId}\``);
|
|
95
|
+
lines.push(`- owner: \`${config.app.ownerOpenId}\``);
|
|
96
|
+
lines.push(`- scopes configured: ${scopesCount}`);
|
|
97
|
+
if (running) {
|
|
98
|
+
const pids = conflicts.map((c) => c.entry.pid).join(", ");
|
|
99
|
+
lines.push(`- 🟢 running (pid ${pids})`);
|
|
100
|
+
lines.push(` use \`/feishu stop\` to terminate, \`/feishu logs\` to tail logs`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
lines.push(`- ⚪ not running`);
|
|
104
|
+
lines.push(` use \`/feishu start\` to launch`);
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
function runStart() {
|
|
109
|
+
if (!configExists()) {
|
|
110
|
+
return [
|
|
111
|
+
"Cannot start: no Feishu config found.",
|
|
112
|
+
"",
|
|
113
|
+
"Run from a shell first: `bubble serve --feishu --setup`",
|
|
114
|
+
"(the wizard needs an interactive terminal to scan the QR code).",
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
117
|
+
let config;
|
|
118
|
+
try {
|
|
119
|
+
config = loadConfig();
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return `Failed to read config: ${err.message}`;
|
|
123
|
+
}
|
|
124
|
+
// Bail if already running for this appId.
|
|
125
|
+
const procRegistry = new ProcessRegistry();
|
|
126
|
+
procRegistry.gc();
|
|
127
|
+
if (procRegistry.findConflicts(config.app.appId).length > 0) {
|
|
128
|
+
return "Feishu serve is already running. Use `/feishu status` for details.";
|
|
129
|
+
}
|
|
130
|
+
// Spawn detached subprocess. Redirect stdout/stderr to a log file so we
|
|
131
|
+
// don't fight the TUI for the terminal. Use process.execPath + argv[1] so
|
|
132
|
+
// the subprocess inherits whatever way the user launched the TUI (npm bin,
|
|
133
|
+
// node dist/main.js, bun dist/main.js, etc.).
|
|
134
|
+
const scriptPath = process.argv[1];
|
|
135
|
+
if (!scriptPath) {
|
|
136
|
+
return "Cannot determine bubble script path; please launch from shell instead.";
|
|
137
|
+
}
|
|
138
|
+
const stdoutLog = join(getLogsDir(), "serve-stdout.log");
|
|
139
|
+
const stderrLog = join(getLogsDir(), "serve-stderr.log");
|
|
140
|
+
let outFd;
|
|
141
|
+
let errFd;
|
|
142
|
+
try {
|
|
143
|
+
outFd = openSync(stdoutLog, "a");
|
|
144
|
+
errFd = openSync(stderrLog, "a");
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
return `Failed to open log files: ${err.message}`;
|
|
148
|
+
}
|
|
149
|
+
let child;
|
|
150
|
+
try {
|
|
151
|
+
child = spawn(process.execPath, [scriptPath, "serve", "--feishu"], {
|
|
152
|
+
detached: true,
|
|
153
|
+
stdio: ["ignore", outFd, errFd],
|
|
154
|
+
env: { ...process.env },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
return `Failed to spawn: ${err.message}`;
|
|
159
|
+
}
|
|
160
|
+
// unref so the parent (TUI) can exit without waiting for the child.
|
|
161
|
+
child.unref();
|
|
162
|
+
// If spawn failed asynchronously, we won't catch it here — the user will
|
|
163
|
+
// see emptiness in /feishu status, which then prompts them to check logs.
|
|
164
|
+
return [
|
|
165
|
+
`🚀 Started Feishu serve (pid ${child.pid ?? "?"}).`,
|
|
166
|
+
` stdout: \`${stdoutLog}\``,
|
|
167
|
+
` stderr: \`${stderrLog}\``,
|
|
168
|
+
"",
|
|
169
|
+
"Use `/feishu status` to verify, `/feishu logs` to tail.",
|
|
170
|
+
].join("\n");
|
|
171
|
+
}
|
|
172
|
+
function runStop() {
|
|
173
|
+
if (!configExists())
|
|
174
|
+
return "No config — nothing to stop.";
|
|
175
|
+
let config;
|
|
176
|
+
try {
|
|
177
|
+
config = loadConfig();
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
return `Failed to read config: ${err.message}`;
|
|
181
|
+
}
|
|
182
|
+
const procRegistry = new ProcessRegistry();
|
|
183
|
+
const conflicts = procRegistry.findConflicts(config.app.appId);
|
|
184
|
+
if (conflicts.length === 0) {
|
|
185
|
+
return "Feishu serve is not running.";
|
|
186
|
+
}
|
|
187
|
+
const killed = procRegistry.killConflicts(config.app.appId);
|
|
188
|
+
if (killed === 0) {
|
|
189
|
+
return "Found stale registry entries but no live processes — cleaned up.";
|
|
190
|
+
}
|
|
191
|
+
return `⏹ Sent SIGTERM to ${killed} process(es). Use \`/feishu status\` to confirm shutdown.`;
|
|
192
|
+
}
|
|
193
|
+
function runDiscover() {
|
|
194
|
+
if (!configExists()) {
|
|
195
|
+
return "未配置。先跑 `/feishu setup`。";
|
|
196
|
+
}
|
|
197
|
+
let config;
|
|
198
|
+
try {
|
|
199
|
+
config = loadConfig();
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
return `Failed to read config: ${err.message}`;
|
|
203
|
+
}
|
|
204
|
+
// Scan today's + yesterday's log for `scope_not_found` (or user_not_allowed)
|
|
205
|
+
// events. Most recent first.
|
|
206
|
+
const events = collectGateEvents();
|
|
207
|
+
if (events.length === 0) {
|
|
208
|
+
return [
|
|
209
|
+
"没找到未授权的 chat 记录。",
|
|
210
|
+
"",
|
|
211
|
+
"确保:",
|
|
212
|
+
"1. `/feishu start` 已经把服务跑起来了(`/feishu status` 看一下)",
|
|
213
|
+
"2. 你已经在手机飞书里给 bot 发过至少一条消息",
|
|
214
|
+
"3. 如果之前测试过,可能已经被 `requireMentionInGroup` 过滤;用私聊试试",
|
|
215
|
+
].join("\n");
|
|
216
|
+
}
|
|
217
|
+
const known = new Set(loadKnownChats());
|
|
218
|
+
const unknown = events.filter((e) => !known.has(e.chatId));
|
|
219
|
+
if (unknown.length === 0) {
|
|
220
|
+
return [
|
|
221
|
+
"找到的 chat 都已经配置过了:",
|
|
222
|
+
...events.slice(0, 5).map((e) => `- ${e.chatId} (user ${e.userId}, ${e.reason})`),
|
|
223
|
+
].join("\n");
|
|
224
|
+
}
|
|
225
|
+
const lines = [
|
|
226
|
+
`发现 ${unknown.length} 个未授权的 chat:`,
|
|
227
|
+
"",
|
|
228
|
+
];
|
|
229
|
+
// Group by chatId, keep latest senderId per chat.
|
|
230
|
+
const byChat = new Map();
|
|
231
|
+
for (const e of unknown) {
|
|
232
|
+
if (!byChat.has(e.chatId))
|
|
233
|
+
byChat.set(e.chatId, e);
|
|
234
|
+
}
|
|
235
|
+
let idx = 1;
|
|
236
|
+
for (const [chatId, info] of byChat.entries()) {
|
|
237
|
+
lines.push(`${idx}. \`${chatId}\``);
|
|
238
|
+
lines.push(` sender: \`${info.userId}\` · ${info.reason} · ${info.ts}`);
|
|
239
|
+
lines.push(` → \`/feishu bind ${chatId} <你的项目路径>\``);
|
|
240
|
+
lines.push("");
|
|
241
|
+
idx++;
|
|
242
|
+
}
|
|
243
|
+
lines.push(`(owner open_id = \`${config.app.ownerOpenId}\`,会自动加入新 scope 的 allowedUsers)`);
|
|
244
|
+
return lines.join("\n");
|
|
245
|
+
}
|
|
246
|
+
function collectGateEvents() {
|
|
247
|
+
const today = new Date();
|
|
248
|
+
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
|
249
|
+
const dates = [iso(today), iso(yesterday)];
|
|
250
|
+
const all = [];
|
|
251
|
+
for (const d of dates) {
|
|
252
|
+
const path = join(getLogsDir(), `${d}.log`);
|
|
253
|
+
if (!existsSync(path))
|
|
254
|
+
continue;
|
|
255
|
+
let raw;
|
|
256
|
+
try {
|
|
257
|
+
raw = readFileSync(path, "utf8");
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
for (const line of raw.split("\n")) {
|
|
263
|
+
if (!line.trim())
|
|
264
|
+
continue;
|
|
265
|
+
try {
|
|
266
|
+
const obj = JSON.parse(line);
|
|
267
|
+
if (obj.msg !== "gate_rejected")
|
|
268
|
+
continue;
|
|
269
|
+
all.push({
|
|
270
|
+
chatId: String(obj.chatId ?? ""),
|
|
271
|
+
userId: String(obj.userId ?? ""),
|
|
272
|
+
reason: String(obj.reason ?? ""),
|
|
273
|
+
ts: String(obj.ts ?? "").slice(11, 19),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
catch { /* skip */ }
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return all.reverse(); // newest first
|
|
280
|
+
}
|
|
281
|
+
function iso(d) {
|
|
282
|
+
return d.toISOString().slice(0, 10);
|
|
283
|
+
}
|
|
284
|
+
function loadKnownChats() {
|
|
285
|
+
try {
|
|
286
|
+
return ScopeRegistry.load().list().map((s) => s.chatId);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function runBind(args) {
|
|
293
|
+
if (!configExists()) {
|
|
294
|
+
return "未配置。先跑 `/feishu setup`。";
|
|
295
|
+
}
|
|
296
|
+
let config;
|
|
297
|
+
try {
|
|
298
|
+
config = loadConfig();
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
return `Failed to read config: ${err.message}`;
|
|
302
|
+
}
|
|
303
|
+
if (args.length < 2) {
|
|
304
|
+
return [
|
|
305
|
+
"用法:`/feishu bind <chat_id> <cwd> [display_name]`",
|
|
306
|
+
"",
|
|
307
|
+
"示例:`/feishu bind oc_abc123 ~/projects/my-app my-app`",
|
|
308
|
+
"",
|
|
309
|
+
"用 `/feishu discover` 找到未授权的 chat_id。",
|
|
310
|
+
].join("\n");
|
|
311
|
+
}
|
|
312
|
+
const chatId = args[0].trim();
|
|
313
|
+
const cwdRaw = args[1].trim();
|
|
314
|
+
const displayName = args.slice(2).join(" ").trim();
|
|
315
|
+
// Expand ~
|
|
316
|
+
const cwd = (cwdRaw === "~" || cwdRaw.startsWith("~/"))
|
|
317
|
+
? (process.env.HOME ?? "") + cwdRaw.slice(1)
|
|
318
|
+
: cwdRaw;
|
|
319
|
+
// Validate
|
|
320
|
+
let stat;
|
|
321
|
+
try {
|
|
322
|
+
stat = existsSync(cwd) ? statSync(cwd) : undefined;
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
stat = undefined;
|
|
326
|
+
}
|
|
327
|
+
if (!stat || !stat.isDirectory()) {
|
|
328
|
+
return `❌ cwd 无效(不存在或不是目录):\`${cwd}\``;
|
|
329
|
+
}
|
|
330
|
+
if (!chatId.startsWith("oc_")) {
|
|
331
|
+
// Not strictly required, but worth flagging.
|
|
332
|
+
return `⚠️ chat_id 看起来不像飞书的(一般以 \`oc_\` 开头)。确认无误后强制添加请直接编辑 \`~/.bubble/feishu/scopes.json\`。收到:\`${chatId}\``;
|
|
333
|
+
}
|
|
334
|
+
const registry = ScopeRegistry.load();
|
|
335
|
+
if (registry.has(chatId)) {
|
|
336
|
+
return `⚠️ scope \`${chatId}\` 已存在。如果想改 cwd,先 \`rm ~/.bubble/feishu/scopes.json\` 里手动调,或者用 \`/cd\` 在飞书会话里切换。`;
|
|
337
|
+
}
|
|
338
|
+
const finalName = displayName || basenameOf(cwd);
|
|
339
|
+
registry.upsert(chatId, {
|
|
340
|
+
cwd,
|
|
341
|
+
displayName: finalName,
|
|
342
|
+
allowedUsers: [config.app.ownerOpenId],
|
|
343
|
+
admins: [config.app.ownerOpenId],
|
|
344
|
+
defaultPermissionMode: "default",
|
|
345
|
+
model: null,
|
|
346
|
+
createdAt: Date.now(),
|
|
347
|
+
lastActiveAt: Date.now(),
|
|
348
|
+
});
|
|
349
|
+
return [
|
|
350
|
+
`✅ 已绑定 scope:`,
|
|
351
|
+
` chat: \`${chatId}\``,
|
|
352
|
+
` cwd: \`${cwd}\``,
|
|
353
|
+
` name: \`${finalName}\``,
|
|
354
|
+
` allowedUsers: [\`${config.app.ownerOpenId}\`]`,
|
|
355
|
+
"",
|
|
356
|
+
"现在去飞书重新发条消息,应该能看到卡片回复了。",
|
|
357
|
+
].join("\n");
|
|
358
|
+
}
|
|
359
|
+
function basenameOf(p) {
|
|
360
|
+
const parts = p.split(/[\\/]/);
|
|
361
|
+
return parts[parts.length - 1] || p;
|
|
362
|
+
}
|
|
363
|
+
function runLogs(n) {
|
|
364
|
+
const tailN = Number.isFinite(n) && n > 0 ? Math.min(n, 500) : 30;
|
|
365
|
+
const dateKey = new Date().toISOString().slice(0, 10);
|
|
366
|
+
const path = join(getLogsDir(), `${dateKey}.log`);
|
|
367
|
+
if (!existsSync(path)) {
|
|
368
|
+
// Fall back to the most recent log file in the dir.
|
|
369
|
+
return `No log file for today yet (\`${path}\`). Start the service first or check \`${getLogsDir()}\`.`;
|
|
370
|
+
}
|
|
371
|
+
let raw;
|
|
372
|
+
try {
|
|
373
|
+
raw = readFileSync(path, "utf8");
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
return `Failed to read log: ${err.message}`;
|
|
377
|
+
}
|
|
378
|
+
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
379
|
+
const slice = lines.slice(-tailN);
|
|
380
|
+
if (slice.length === 0)
|
|
381
|
+
return `(log is empty: \`${path}\`)`;
|
|
382
|
+
// Format JSON lines compactly for readability in chat.
|
|
383
|
+
const formatted = slice.map((line) => {
|
|
384
|
+
try {
|
|
385
|
+
const obj = JSON.parse(line);
|
|
386
|
+
const ts = String(obj.ts ?? "").slice(11, 19);
|
|
387
|
+
const lvl = String(obj.level ?? "?").padEnd(5);
|
|
388
|
+
const msg = String(obj.msg ?? "");
|
|
389
|
+
const extra = Object.entries(obj)
|
|
390
|
+
.filter(([k]) => !["ts", "level", "msg"].includes(k))
|
|
391
|
+
.map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`)
|
|
392
|
+
.join(" ");
|
|
393
|
+
return `${ts} ${lvl} ${msg}${extra ? " " + extra : ""}`;
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return line;
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
return ["```", ...formatted, "```", `(${slice.length} lines from \`${path}\`)`].join("\n");
|
|
400
|
+
}
|
|
@@ -17,7 +17,7 @@ export interface SlashCommandContext {
|
|
|
17
17
|
exit: () => void;
|
|
18
18
|
sessionManager?: SessionManager;
|
|
19
19
|
createProvider: (providerId: string, apiKey: string, baseURL: string) => Provider;
|
|
20
|
-
openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout" | "skill", providerId?: string) => void;
|
|
20
|
+
openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout" | "skill" | "feishu-setup", providerId?: string) => void;
|
|
21
21
|
registry: ProviderRegistry;
|
|
22
22
|
skillRegistry: SkillRegistry;
|
|
23
23
|
bashAllowlist?: BashAllowlist;
|
|
@@ -34,6 +34,8 @@ export interface SlashCommandContext {
|
|
|
34
34
|
getResolvedTheme?: () => "light" | "dark";
|
|
35
35
|
/** Persist a new theme mode AND apply it to the running TUI. */
|
|
36
36
|
setThemeMode?: (mode: ThemeMode) => void;
|
|
37
|
+
/** Open the feedback dialog. `initialDescription` prefills the description field. */
|
|
38
|
+
openFeedback?: (initialDescription: string) => void;
|
|
37
39
|
}
|
|
38
40
|
/**
|
|
39
41
|
* Return types for a slash command handler:
|