@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,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three gates: scope (chat known?), user (in allowedUsers?), mention (group + requireMention?).
|
|
3
|
+
*
|
|
4
|
+
* Failed gates return reasons that the caller may log but should NOT reflect
|
|
5
|
+
* back to the user (silent drop) — surfacing the bot's existence is its own
|
|
6
|
+
* security exposure.
|
|
7
|
+
*/
|
|
8
|
+
export function checkWhitelist(input) {
|
|
9
|
+
if (!input.scope)
|
|
10
|
+
return { ok: false, reason: "scope_not_found" };
|
|
11
|
+
if (input.chatType === "topic")
|
|
12
|
+
return { ok: false, reason: "topic_chat_unsupported" };
|
|
13
|
+
if (!input.scope.allowedUsers.includes(input.userId)) {
|
|
14
|
+
return { ok: false, reason: "user_not_allowed" };
|
|
15
|
+
}
|
|
16
|
+
if (input.chatType === "group" && input.requireMentionInGroup && !input.mentionedBot) {
|
|
17
|
+
return { ok: false, reason: "no_mention_in_group" };
|
|
18
|
+
}
|
|
19
|
+
return { ok: true };
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-scope active-run tracker with preemption.
|
|
3
|
+
*
|
|
4
|
+
* Same scopeKey can have at most one run. Starting a new one aborts the
|
|
5
|
+
* previous one's AbortController and waits (briefly) for it to settle
|
|
6
|
+
* before resolving startOrReplace().
|
|
7
|
+
*/
|
|
8
|
+
import type { ScopeKey } from "../types.js";
|
|
9
|
+
export declare class ActiveRuns {
|
|
10
|
+
private readonly runs;
|
|
11
|
+
isActive(scopeKey: ScopeKey): boolean;
|
|
12
|
+
getSignal(scopeKey: ScopeKey): AbortSignal | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Register a new run for `scopeKey`. If one was already active, aborts it
|
|
15
|
+
* and awaits its settlement (capped at `waitMs`) before returning the new
|
|
16
|
+
* AbortSignal.
|
|
17
|
+
*
|
|
18
|
+
* Caller is responsible for calling `complete(scopeKey)` (or letting the
|
|
19
|
+
* donePromise resolve) when its work is done.
|
|
20
|
+
*/
|
|
21
|
+
startOrReplace(scopeKey: ScopeKey, waitMs?: number): Promise<{
|
|
22
|
+
signal: AbortSignal;
|
|
23
|
+
complete: () => void;
|
|
24
|
+
}>;
|
|
25
|
+
/** Abort a single scope's run. Used by /stop and shutdown. */
|
|
26
|
+
abort(scopeKey: ScopeKey): boolean;
|
|
27
|
+
/** Abort all active runs. Returns the number aborted. */
|
|
28
|
+
abortAll(): number;
|
|
29
|
+
/** Wait for all current runs to settle, capped at `maxWaitMs`. */
|
|
30
|
+
waitAll(maxWaitMs?: number): Promise<void>;
|
|
31
|
+
size(): number;
|
|
32
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-scope active-run tracker with preemption.
|
|
3
|
+
*
|
|
4
|
+
* Same scopeKey can have at most one run. Starting a new one aborts the
|
|
5
|
+
* previous one's AbortController and waits (briefly) for it to settle
|
|
6
|
+
* before resolving startOrReplace().
|
|
7
|
+
*/
|
|
8
|
+
export class ActiveRuns {
|
|
9
|
+
runs = new Map();
|
|
10
|
+
isActive(scopeKey) {
|
|
11
|
+
return this.runs.has(scopeKey);
|
|
12
|
+
}
|
|
13
|
+
getSignal(scopeKey) {
|
|
14
|
+
return this.runs.get(scopeKey)?.abortController.signal;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Register a new run for `scopeKey`. If one was already active, aborts it
|
|
18
|
+
* and awaits its settlement (capped at `waitMs`) before returning the new
|
|
19
|
+
* AbortSignal.
|
|
20
|
+
*
|
|
21
|
+
* Caller is responsible for calling `complete(scopeKey)` (or letting the
|
|
22
|
+
* donePromise resolve) when its work is done.
|
|
23
|
+
*/
|
|
24
|
+
async startOrReplace(scopeKey, waitMs = 10_000) {
|
|
25
|
+
const existing = this.runs.get(scopeKey);
|
|
26
|
+
if (existing) {
|
|
27
|
+
existing.abortController.abort();
|
|
28
|
+
// Wait for prior run to finish so we don't double-spend the pool slot.
|
|
29
|
+
await Promise.race([
|
|
30
|
+
existing.donePromise,
|
|
31
|
+
new Promise((resolve) => setTimeout(resolve, waitMs)),
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
const abortController = new AbortController();
|
|
35
|
+
let resolveDone;
|
|
36
|
+
const donePromise = new Promise((res) => {
|
|
37
|
+
resolveDone = res;
|
|
38
|
+
});
|
|
39
|
+
const handle = {
|
|
40
|
+
abortController,
|
|
41
|
+
donePromise,
|
|
42
|
+
startedAt: Date.now(),
|
|
43
|
+
};
|
|
44
|
+
this.runs.set(scopeKey, handle);
|
|
45
|
+
const complete = () => {
|
|
46
|
+
const current = this.runs.get(scopeKey);
|
|
47
|
+
if (current === handle) {
|
|
48
|
+
this.runs.delete(scopeKey);
|
|
49
|
+
}
|
|
50
|
+
resolveDone();
|
|
51
|
+
};
|
|
52
|
+
return { signal: abortController.signal, complete };
|
|
53
|
+
}
|
|
54
|
+
/** Abort a single scope's run. Used by /stop and shutdown. */
|
|
55
|
+
abort(scopeKey) {
|
|
56
|
+
const handle = this.runs.get(scopeKey);
|
|
57
|
+
if (!handle)
|
|
58
|
+
return false;
|
|
59
|
+
handle.abortController.abort();
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/** Abort all active runs. Returns the number aborted. */
|
|
63
|
+
abortAll() {
|
|
64
|
+
let count = 0;
|
|
65
|
+
for (const handle of this.runs.values()) {
|
|
66
|
+
handle.abortController.abort();
|
|
67
|
+
count++;
|
|
68
|
+
}
|
|
69
|
+
return count;
|
|
70
|
+
}
|
|
71
|
+
/** Wait for all current runs to settle, capped at `maxWaitMs`. */
|
|
72
|
+
async waitAll(maxWaitMs = 10_000) {
|
|
73
|
+
const promises = Array.from(this.runs.values()).map((h) => h.donePromise);
|
|
74
|
+
if (promises.length === 0)
|
|
75
|
+
return;
|
|
76
|
+
await Promise.race([
|
|
77
|
+
Promise.all(promises),
|
|
78
|
+
new Promise((resolve) => setTimeout(resolve, maxWaitMs)),
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
size() {
|
|
82
|
+
return this.runs.size;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-scope inbound message debounce. Coalesces messages arriving within
|
|
3
|
+
* `debounceMs` of each other into a single prompt, so a user typing
|
|
4
|
+
* fragments doesn't kick off N parallel runs.
|
|
5
|
+
*
|
|
6
|
+
* Lifecycle per scopeKey:
|
|
7
|
+
* push(scopeKey, msg) → start/reset timer
|
|
8
|
+
* timer fires → flush queued messages as combined prompt, call onFlush()
|
|
9
|
+
* block(scopeKey) → suppress flushes until unblock() (used while a run is in flight)
|
|
10
|
+
* unblock(scopeKey) → resume; if queue non-empty, flush immediately
|
|
11
|
+
*/
|
|
12
|
+
import type { ScopeKey } from "../types.js";
|
|
13
|
+
export interface QueuedMessage {
|
|
14
|
+
text: string;
|
|
15
|
+
messageId: string;
|
|
16
|
+
receivedAt: number;
|
|
17
|
+
}
|
|
18
|
+
export interface PendingQueueOptions {
|
|
19
|
+
debounceMs: number;
|
|
20
|
+
onFlush: (scopeKey: ScopeKey, batch: QueuedMessage[]) => void | Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export declare class PendingQueue {
|
|
23
|
+
private readonly opts;
|
|
24
|
+
private readonly states;
|
|
25
|
+
constructor(opts: PendingQueueOptions);
|
|
26
|
+
push(scopeKey: ScopeKey, msg: QueuedMessage): void;
|
|
27
|
+
/** Suspend flushing for `scopeKey` while a run is in flight. */
|
|
28
|
+
block(scopeKey: ScopeKey): void;
|
|
29
|
+
unblock(scopeKey: ScopeKey): void;
|
|
30
|
+
/** Stop all timers (used at shutdown). Does not flush. */
|
|
31
|
+
shutdown(): void;
|
|
32
|
+
private resetTimer;
|
|
33
|
+
private flush;
|
|
34
|
+
}
|
|
35
|
+
/** Concatenate queued messages into a single user prompt. */
|
|
36
|
+
export declare function combineQueuedMessages(batch: QueuedMessage[]): string;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-scope inbound message debounce. Coalesces messages arriving within
|
|
3
|
+
* `debounceMs` of each other into a single prompt, so a user typing
|
|
4
|
+
* fragments doesn't kick off N parallel runs.
|
|
5
|
+
*
|
|
6
|
+
* Lifecycle per scopeKey:
|
|
7
|
+
* push(scopeKey, msg) → start/reset timer
|
|
8
|
+
* timer fires → flush queued messages as combined prompt, call onFlush()
|
|
9
|
+
* block(scopeKey) → suppress flushes until unblock() (used while a run is in flight)
|
|
10
|
+
* unblock(scopeKey) → resume; if queue non-empty, flush immediately
|
|
11
|
+
*/
|
|
12
|
+
export class PendingQueue {
|
|
13
|
+
opts;
|
|
14
|
+
states = new Map();
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
this.opts = opts;
|
|
17
|
+
}
|
|
18
|
+
push(scopeKey, msg) {
|
|
19
|
+
let state = this.states.get(scopeKey);
|
|
20
|
+
if (!state) {
|
|
21
|
+
state = { messages: [], blocked: false };
|
|
22
|
+
this.states.set(scopeKey, state);
|
|
23
|
+
}
|
|
24
|
+
state.messages.push(msg);
|
|
25
|
+
if (state.blocked)
|
|
26
|
+
return;
|
|
27
|
+
this.resetTimer(scopeKey);
|
|
28
|
+
}
|
|
29
|
+
/** Suspend flushing for `scopeKey` while a run is in flight. */
|
|
30
|
+
block(scopeKey) {
|
|
31
|
+
let state = this.states.get(scopeKey);
|
|
32
|
+
if (!state) {
|
|
33
|
+
state = { messages: [], blocked: true };
|
|
34
|
+
this.states.set(scopeKey, state);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
state.blocked = true;
|
|
38
|
+
if (state.timer) {
|
|
39
|
+
clearTimeout(state.timer);
|
|
40
|
+
state.timer = undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
unblock(scopeKey) {
|
|
44
|
+
const state = this.states.get(scopeKey);
|
|
45
|
+
if (!state)
|
|
46
|
+
return;
|
|
47
|
+
state.blocked = false;
|
|
48
|
+
if (state.messages.length > 0) {
|
|
49
|
+
// Flush immediately — debounce already elapsed conceptually while we
|
|
50
|
+
// were blocked.
|
|
51
|
+
void this.flush(scopeKey);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Stop all timers (used at shutdown). Does not flush. */
|
|
55
|
+
shutdown() {
|
|
56
|
+
for (const state of this.states.values()) {
|
|
57
|
+
if (state.timer) {
|
|
58
|
+
clearTimeout(state.timer);
|
|
59
|
+
state.timer = undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
this.states.clear();
|
|
63
|
+
}
|
|
64
|
+
resetTimer(scopeKey) {
|
|
65
|
+
const state = this.states.get(scopeKey);
|
|
66
|
+
if (!state)
|
|
67
|
+
return;
|
|
68
|
+
if (state.timer)
|
|
69
|
+
clearTimeout(state.timer);
|
|
70
|
+
state.timer = setTimeout(() => {
|
|
71
|
+
void this.flush(scopeKey);
|
|
72
|
+
}, this.opts.debounceMs);
|
|
73
|
+
}
|
|
74
|
+
async flush(scopeKey) {
|
|
75
|
+
const state = this.states.get(scopeKey);
|
|
76
|
+
if (!state)
|
|
77
|
+
return;
|
|
78
|
+
if (state.blocked)
|
|
79
|
+
return;
|
|
80
|
+
if (state.messages.length === 0)
|
|
81
|
+
return;
|
|
82
|
+
const batch = state.messages;
|
|
83
|
+
state.messages = [];
|
|
84
|
+
state.timer = undefined;
|
|
85
|
+
try {
|
|
86
|
+
await this.opts.onFlush(scopeKey, batch);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Errors are caller's responsibility to surface; never crash the queue.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Concatenate queued messages into a single user prompt. */
|
|
94
|
+
export function combineQueuedMessages(batch) {
|
|
95
|
+
if (batch.length === 1)
|
|
96
|
+
return batch[0].text;
|
|
97
|
+
return batch.map((m) => m.text).join("\n\n");
|
|
98
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global concurrency cap. FIFO queue of tasks; at most `concurrency` in
|
|
3
|
+
* flight at any time.
|
|
4
|
+
*
|
|
5
|
+
* Bubble agents are CPU-light but LLM-network-heavy. The cap exists to
|
|
6
|
+
* (a) cap egress bandwidth and (b) keep API rate-limit failures localized.
|
|
7
|
+
*/
|
|
8
|
+
export interface ProcessPoolOptions {
|
|
9
|
+
concurrency: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class ProcessPool {
|
|
12
|
+
private readonly opts;
|
|
13
|
+
private active;
|
|
14
|
+
private readonly waiters;
|
|
15
|
+
constructor(opts: ProcessPoolOptions);
|
|
16
|
+
/**
|
|
17
|
+
* Acquire a slot. Caller must call `release()` exactly once when done
|
|
18
|
+
* (or use `run()` for the safer wrapped variant).
|
|
19
|
+
*/
|
|
20
|
+
acquire(): Promise<void>;
|
|
21
|
+
release(): void;
|
|
22
|
+
/** Run `fn` with a pool slot held; releases on settle. */
|
|
23
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
24
|
+
status(): {
|
|
25
|
+
active: number;
|
|
26
|
+
waiting: number;
|
|
27
|
+
cap: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global concurrency cap. FIFO queue of tasks; at most `concurrency` in
|
|
3
|
+
* flight at any time.
|
|
4
|
+
*
|
|
5
|
+
* Bubble agents are CPU-light but LLM-network-heavy. The cap exists to
|
|
6
|
+
* (a) cap egress bandwidth and (b) keep API rate-limit failures localized.
|
|
7
|
+
*/
|
|
8
|
+
export class ProcessPool {
|
|
9
|
+
opts;
|
|
10
|
+
active = 0;
|
|
11
|
+
waiters = [];
|
|
12
|
+
constructor(opts) {
|
|
13
|
+
this.opts = opts;
|
|
14
|
+
if (opts.concurrency < 1) {
|
|
15
|
+
throw new Error(`ProcessPool concurrency must be >= 1, got ${opts.concurrency}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Acquire a slot. Caller must call `release()` exactly once when done
|
|
20
|
+
* (or use `run()` for the safer wrapped variant).
|
|
21
|
+
*/
|
|
22
|
+
async acquire() {
|
|
23
|
+
if (this.active < this.opts.concurrency) {
|
|
24
|
+
this.active++;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
28
|
+
this.active++;
|
|
29
|
+
}
|
|
30
|
+
release() {
|
|
31
|
+
this.active = Math.max(0, this.active - 1);
|
|
32
|
+
const next = this.waiters.shift();
|
|
33
|
+
if (next)
|
|
34
|
+
next();
|
|
35
|
+
}
|
|
36
|
+
/** Run `fn` with a pool slot held; releases on settle. */
|
|
37
|
+
async run(fn) {
|
|
38
|
+
await this.acquire();
|
|
39
|
+
try {
|
|
40
|
+
return await fn();
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
this.release();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
status() {
|
|
47
|
+
return { active: this.active, waiting: this.waiters.length, cap: this.opts.concurrency };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-rolled schema validation for Feishu config / scopes / sessions JSON.
|
|
3
|
+
*
|
|
4
|
+
* We avoid zod to keep dependency footprint flat. Each validator returns
|
|
5
|
+
* an array of human-readable errors; empty array means valid.
|
|
6
|
+
*/
|
|
7
|
+
import type { FeishuConfig, ScopesFile, ScopeConfig, SessionsFile, SessionEntry } from "./types.js";
|
|
8
|
+
export interface ValidationResult<T> {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
value?: T;
|
|
11
|
+
errors: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function validateFeishuConfig(raw: unknown): ValidationResult<FeishuConfig>;
|
|
14
|
+
export declare function validateScopesFile(raw: unknown): ValidationResult<ScopesFile>;
|
|
15
|
+
export declare function validateScopeConfig(raw: unknown): ValidationResult<ScopeConfig>;
|
|
16
|
+
export declare function validateSessionsFile(raw: unknown): ValidationResult<SessionsFile>;
|
|
17
|
+
export declare function validateSessionEntry(raw: unknown): ValidationResult<SessionEntry>;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-rolled schema validation for Feishu config / scopes / sessions JSON.
|
|
3
|
+
*
|
|
4
|
+
* We avoid zod to keep dependency footprint flat. Each validator returns
|
|
5
|
+
* an array of human-readable errors; empty array means valid.
|
|
6
|
+
*/
|
|
7
|
+
import { DEFAULT_PREFERENCES, DEFAULT_GLOBAL_LIMITS } from "./types.js";
|
|
8
|
+
const PERMISSION_MODES = ["default", "plan", "bypassPermissions"];
|
|
9
|
+
const RENDER_MODES = ["card", "markdown", "text"];
|
|
10
|
+
function isString(v) {
|
|
11
|
+
return typeof v === "string";
|
|
12
|
+
}
|
|
13
|
+
function isNumber(v) {
|
|
14
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
15
|
+
}
|
|
16
|
+
function isBoolean(v) {
|
|
17
|
+
return typeof v === "boolean";
|
|
18
|
+
}
|
|
19
|
+
function isObject(v) {
|
|
20
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
21
|
+
}
|
|
22
|
+
function isStringArray(v) {
|
|
23
|
+
return Array.isArray(v) && v.every(isString);
|
|
24
|
+
}
|
|
25
|
+
export function validateFeishuConfig(raw) {
|
|
26
|
+
const errors = [];
|
|
27
|
+
if (!isObject(raw)) {
|
|
28
|
+
return { ok: false, errors: ["config must be an object"] };
|
|
29
|
+
}
|
|
30
|
+
if (raw.version !== 1)
|
|
31
|
+
errors.push(`config.version must be 1 (got ${String(raw.version)})`);
|
|
32
|
+
const app = raw.app;
|
|
33
|
+
if (!isObject(app)) {
|
|
34
|
+
errors.push("config.app must be an object");
|
|
35
|
+
return { ok: false, errors };
|
|
36
|
+
}
|
|
37
|
+
if (!isString(app.appId) || !app.appId.trim())
|
|
38
|
+
errors.push("config.app.appId must be a non-empty string");
|
|
39
|
+
if (!isString(app.ownerOpenId) || !app.ownerOpenId.trim())
|
|
40
|
+
errors.push("config.app.ownerOpenId must be a non-empty string");
|
|
41
|
+
if (!isString(app.encryptCheck))
|
|
42
|
+
errors.push("config.app.encryptCheck must be a string");
|
|
43
|
+
const secretRef = app.secretRef;
|
|
44
|
+
if (!isObject(secretRef) || !isString(secretRef.source)) {
|
|
45
|
+
errors.push("config.app.secretRef must be { source: 'keystore'|'env', ... }");
|
|
46
|
+
}
|
|
47
|
+
else if (secretRef.source === "keystore") {
|
|
48
|
+
if (!isString(secretRef.name) || !secretRef.name) {
|
|
49
|
+
errors.push("config.app.secretRef.name required when source=keystore");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (secretRef.source === "env") {
|
|
53
|
+
if (!isString(secretRef.varName) || !secretRef.varName) {
|
|
54
|
+
errors.push("config.app.secretRef.varName required when source=env");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
errors.push(`config.app.secretRef.source must be 'keystore' or 'env'`);
|
|
59
|
+
}
|
|
60
|
+
// Preferences: fill defaults for missing fields, validate provided ones.
|
|
61
|
+
const prefsRaw = isObject(raw.preferences) ? raw.preferences : {};
|
|
62
|
+
const preferences = { ...DEFAULT_PREFERENCES };
|
|
63
|
+
if (prefsRaw.outputThrottleMs !== undefined) {
|
|
64
|
+
if (!isNumber(prefsRaw.outputThrottleMs) || prefsRaw.outputThrottleMs < 50) {
|
|
65
|
+
errors.push("preferences.outputThrottleMs must be a number >= 50");
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
preferences.outputThrottleMs = prefsRaw.outputThrottleMs;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (prefsRaw.idleTimeoutMinutes !== undefined) {
|
|
72
|
+
if (!isNumber(prefsRaw.idleTimeoutMinutes) || prefsRaw.idleTimeoutMinutes < 1) {
|
|
73
|
+
errors.push("preferences.idleTimeoutMinutes must be a number >= 1");
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
preferences.idleTimeoutMinutes = prefsRaw.idleTimeoutMinutes;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (prefsRaw.renderMode !== undefined) {
|
|
80
|
+
if (!isString(prefsRaw.renderMode) || !RENDER_MODES.includes(prefsRaw.renderMode)) {
|
|
81
|
+
errors.push(`preferences.renderMode must be one of ${RENDER_MODES.join("|")}`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
preferences.renderMode = prefsRaw.renderMode;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (prefsRaw.requireMentionInGroup !== undefined) {
|
|
88
|
+
if (!isBoolean(prefsRaw.requireMentionInGroup)) {
|
|
89
|
+
errors.push("preferences.requireMentionInGroup must be boolean");
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
preferences.requireMentionInGroup = prefsRaw.requireMentionInGroup;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (prefsRaw.maxBytesPerElement !== undefined) {
|
|
96
|
+
if (!isNumber(prefsRaw.maxBytesPerElement) || prefsRaw.maxBytesPerElement < 1000) {
|
|
97
|
+
errors.push("preferences.maxBytesPerElement must be a number >= 1000");
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
preferences.maxBytesPerElement = prefsRaw.maxBytesPerElement;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (prefsRaw.maxBytesPerCard !== undefined) {
|
|
104
|
+
if (!isNumber(prefsRaw.maxBytesPerCard) || prefsRaw.maxBytesPerCard < preferences.maxBytesPerElement) {
|
|
105
|
+
errors.push("preferences.maxBytesPerCard must be a number >= maxBytesPerElement");
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
preferences.maxBytesPerCard = prefsRaw.maxBytesPerCard;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const limitsRaw = isObject(raw.globalLimits) ? raw.globalLimits : {};
|
|
112
|
+
const globalLimits = { ...DEFAULT_GLOBAL_LIMITS };
|
|
113
|
+
if (limitsRaw.maxConcurrentRuns !== undefined) {
|
|
114
|
+
if (!isNumber(limitsRaw.maxConcurrentRuns) || limitsRaw.maxConcurrentRuns < 1) {
|
|
115
|
+
errors.push("globalLimits.maxConcurrentRuns must be a number >= 1");
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
globalLimits.maxConcurrentRuns = limitsRaw.maxConcurrentRuns;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (errors.length > 0)
|
|
122
|
+
return { ok: false, errors };
|
|
123
|
+
const config = {
|
|
124
|
+
version: 1,
|
|
125
|
+
app: {
|
|
126
|
+
appId: app.appId.trim(),
|
|
127
|
+
ownerOpenId: app.ownerOpenId.trim(),
|
|
128
|
+
encryptCheck: app.encryptCheck,
|
|
129
|
+
secretRef: secretRef,
|
|
130
|
+
},
|
|
131
|
+
preferences,
|
|
132
|
+
globalLimits,
|
|
133
|
+
};
|
|
134
|
+
return { ok: true, value: config, errors: [] };
|
|
135
|
+
}
|
|
136
|
+
export function validateScopesFile(raw) {
|
|
137
|
+
const errors = [];
|
|
138
|
+
if (!isObject(raw)) {
|
|
139
|
+
return { ok: false, errors: ["scopes file must be an object"] };
|
|
140
|
+
}
|
|
141
|
+
if (raw.version !== 1)
|
|
142
|
+
errors.push(`scopes.version must be 1 (got ${String(raw.version)})`);
|
|
143
|
+
const scopes = raw.scopes;
|
|
144
|
+
if (!isObject(scopes)) {
|
|
145
|
+
errors.push("scopes.scopes must be an object");
|
|
146
|
+
return { ok: false, errors };
|
|
147
|
+
}
|
|
148
|
+
const out = {};
|
|
149
|
+
for (const [chatId, value] of Object.entries(scopes)) {
|
|
150
|
+
const { ok, value: scope, errors: scopeErrs } = validateScopeConfig(value);
|
|
151
|
+
if (!ok || !scope) {
|
|
152
|
+
for (const e of scopeErrs)
|
|
153
|
+
errors.push(`scopes[${chatId}]: ${e}`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
out[chatId] = scope;
|
|
157
|
+
}
|
|
158
|
+
if (errors.length > 0)
|
|
159
|
+
return { ok: false, errors };
|
|
160
|
+
return { ok: true, value: { version: 1, scopes: out }, errors: [] };
|
|
161
|
+
}
|
|
162
|
+
export function validateScopeConfig(raw) {
|
|
163
|
+
const errors = [];
|
|
164
|
+
if (!isObject(raw))
|
|
165
|
+
return { ok: false, errors: ["scope must be an object"] };
|
|
166
|
+
if (!isString(raw.cwd) || !raw.cwd.trim())
|
|
167
|
+
errors.push("cwd required");
|
|
168
|
+
if (!isString(raw.displayName) || !raw.displayName.trim())
|
|
169
|
+
errors.push("displayName required");
|
|
170
|
+
if (!isStringArray(raw.allowedUsers))
|
|
171
|
+
errors.push("allowedUsers must be string[]");
|
|
172
|
+
else if (raw.allowedUsers.length === 0)
|
|
173
|
+
errors.push("allowedUsers must be non-empty");
|
|
174
|
+
if (!isStringArray(raw.admins))
|
|
175
|
+
errors.push("admins must be string[]");
|
|
176
|
+
if (!isString(raw.defaultPermissionMode) || !PERMISSION_MODES.includes(raw.defaultPermissionMode)) {
|
|
177
|
+
errors.push(`defaultPermissionMode must be one of ${PERMISSION_MODES.join("|")}`);
|
|
178
|
+
}
|
|
179
|
+
if (raw.model !== null && !isString(raw.model))
|
|
180
|
+
errors.push("model must be string|null");
|
|
181
|
+
if (!isNumber(raw.createdAt))
|
|
182
|
+
errors.push("createdAt must be number");
|
|
183
|
+
if (!isNumber(raw.lastActiveAt))
|
|
184
|
+
errors.push("lastActiveAt must be number");
|
|
185
|
+
if (errors.length > 0)
|
|
186
|
+
return { ok: false, errors };
|
|
187
|
+
return {
|
|
188
|
+
ok: true,
|
|
189
|
+
value: {
|
|
190
|
+
cwd: raw.cwd.trim(),
|
|
191
|
+
displayName: raw.displayName.trim(),
|
|
192
|
+
allowedUsers: raw.allowedUsers,
|
|
193
|
+
admins: raw.admins,
|
|
194
|
+
defaultPermissionMode: raw.defaultPermissionMode,
|
|
195
|
+
model: raw.model,
|
|
196
|
+
createdAt: raw.createdAt,
|
|
197
|
+
lastActiveAt: raw.lastActiveAt,
|
|
198
|
+
},
|
|
199
|
+
errors: [],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
export function validateSessionsFile(raw) {
|
|
203
|
+
const errors = [];
|
|
204
|
+
if (!isObject(raw))
|
|
205
|
+
return { ok: false, errors: ["sessions file must be an object"] };
|
|
206
|
+
if (raw.version !== 1)
|
|
207
|
+
errors.push(`sessions.version must be 1 (got ${String(raw.version)})`);
|
|
208
|
+
const sessions = raw.sessions;
|
|
209
|
+
if (!isObject(sessions)) {
|
|
210
|
+
errors.push("sessions.sessions must be an object");
|
|
211
|
+
return { ok: false, errors };
|
|
212
|
+
}
|
|
213
|
+
const out = {};
|
|
214
|
+
for (const [key, value] of Object.entries(sessions)) {
|
|
215
|
+
const { ok, value: entry, errors: entryErrs } = validateSessionEntry(value);
|
|
216
|
+
if (!ok || !entry) {
|
|
217
|
+
for (const e of entryErrs)
|
|
218
|
+
errors.push(`sessions[${key}]: ${e}`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
out[key] = entry;
|
|
222
|
+
}
|
|
223
|
+
if (errors.length > 0)
|
|
224
|
+
return { ok: false, errors };
|
|
225
|
+
return { ok: true, value: { version: 1, sessions: out }, errors: [] };
|
|
226
|
+
}
|
|
227
|
+
export function validateSessionEntry(raw) {
|
|
228
|
+
const errors = [];
|
|
229
|
+
if (!isObject(raw))
|
|
230
|
+
return { ok: false, errors: ["session entry must be an object"] };
|
|
231
|
+
if (!isString(raw.sessionFile) || !raw.sessionFile)
|
|
232
|
+
errors.push("sessionFile required");
|
|
233
|
+
if (!isString(raw.cwd) || !raw.cwd)
|
|
234
|
+
errors.push("cwd required");
|
|
235
|
+
if (!isString(raw.permissionMode) || !PERMISSION_MODES.includes(raw.permissionMode)) {
|
|
236
|
+
errors.push(`permissionMode must be one of ${PERMISSION_MODES.join("|")}`);
|
|
237
|
+
}
|
|
238
|
+
if (!isNumber(raw.lastActiveAt))
|
|
239
|
+
errors.push("lastActiveAt must be number");
|
|
240
|
+
if (errors.length > 0)
|
|
241
|
+
return { ok: false, errors };
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
value: {
|
|
245
|
+
sessionFile: raw.sessionFile,
|
|
246
|
+
cwd: raw.cwd,
|
|
247
|
+
permissionMode: raw.permissionMode,
|
|
248
|
+
lastActiveAt: raw.lastActiveAt,
|
|
249
|
+
},
|
|
250
|
+
errors: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scopes.json reader/writer.
|
|
3
|
+
*
|
|
4
|
+
* `cwd` here is the *initial* cwd written by the wizard. The truth source
|
|
5
|
+
* for "current cwd" after first use is sessions.json. See SessionStore.
|
|
6
|
+
*
|
|
7
|
+
* `/feishu bind` is invoked from the TUI process, but the running serve
|
|
8
|
+
* is a separate spawned subprocess — so the serve's in-memory cache would
|
|
9
|
+
* go stale the moment the TUI updates scopes.json. To bridge that, every
|
|
10
|
+
* read path stats the file and reloads if its mtime is newer than what
|
|
11
|
+
* we have cached. The cost is one stat() call per inbound message, which
|
|
12
|
+
* is negligible compared to LLM latency.
|
|
13
|
+
*/
|
|
14
|
+
import type { ScopeConfig, ScopesFile } from "../types.js";
|
|
15
|
+
export declare class ScopeRegistry {
|
|
16
|
+
private file;
|
|
17
|
+
private lastMtimeMs;
|
|
18
|
+
constructor(file: ScopesFile, mtimeMs: number);
|
|
19
|
+
static load(): ScopeRegistry;
|
|
20
|
+
/**
|
|
21
|
+
* Reload from disk if scopes.json has been modified since our cached copy.
|
|
22
|
+
* Called from every read path so cross-process writes (e.g. `/feishu bind`
|
|
23
|
+
* from the TUI updating the file while the serve subprocess is running)
|
|
24
|
+
* become visible without restart.
|
|
25
|
+
*/
|
|
26
|
+
private syncFromDiskIfChanged;
|
|
27
|
+
save(): void;
|
|
28
|
+
get(chatId: string): ScopeConfig | undefined;
|
|
29
|
+
has(chatId: string): boolean;
|
|
30
|
+
list(): Array<{
|
|
31
|
+
chatId: string;
|
|
32
|
+
scope: ScopeConfig;
|
|
33
|
+
}>;
|
|
34
|
+
upsert(chatId: string, scope: ScopeConfig): void;
|
|
35
|
+
remove(chatId: string): boolean;
|
|
36
|
+
touch(chatId: string, when?: number): void;
|
|
37
|
+
isUserAllowed(chatId: string, userId: string): boolean;
|
|
38
|
+
isUserAdmin(chatId: string, userId: string): boolean;
|
|
39
|
+
}
|