@fugood/buttress-server 2.25.5 → 2.25.7
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/README.md +164 -4
- package/config/function-samples/README.md +2 -0
- package/config/function-samples/bank-note.ts +47 -0
- package/config/function-samples/bank-watch-daemon.ts +63 -0
- package/config/function-samples/run-agent.ts +39 -0
- package/config/sample.toml +22 -0
- package/lib/agent/cli.d.ts +19 -0
- package/lib/agent/client.d.ts +66 -0
- package/lib/agent/config.d.ts +15 -0
- package/lib/agent/context.d.ts +11 -0
- package/lib/agent/loopback.d.ts +21 -0
- package/lib/agent/mcp.d.ts +23 -0
- package/lib/agent/models.d.ts +20 -0
- package/lib/agent/service.d.ts +16 -0
- package/lib/agent/session-fs.d.ts +3 -0
- package/lib/agent/sessions.d.ts +17 -0
- package/lib/agent/tools.d.ts +32 -0
- package/lib/agent/tui.d.ts +17 -0
- package/lib/agent/types.d.ts +123 -0
- package/lib/cli-DrbWX4ea.mjs +22 -0
- package/lib/client-BCBBen9i.mjs +8 -0
- package/lib/config-lP89VahD.mjs +2 -0
- package/lib/functions/bank-subscribe.d.ts +46 -0
- package/lib/functions/bank.d.ts +21 -0
- package/lib/functions/daemons.d.ts +45 -0
- package/lib/functions/executor.d.ts +31 -4
- package/lib/functions/index.d.ts +17 -7
- package/lib/functions/registry.d.ts +7 -1
- package/lib/functions/status.d.ts +49 -1
- package/lib/functions/templates.d.ts +3 -1
- package/lib/functions/types.d.ts +129 -0
- package/lib/index.d.ts +8 -2
- package/lib/index.mjs +270 -58
- package/lib/mlx-bridge.py +681 -0
- package/lib/routes/agents.d.ts +37 -0
- package/lib/routes/anthropic-messages.d.ts +2 -2
- package/lib/routes/index.d.ts +1 -0
- package/lib/routes/openai-compat.d.ts +2 -2
- package/lib/tui-7B7x6A08.mjs +2 -0
- package/lib/types.d.ts +9 -0
- package/lib/utils/cors.check.d.ts +1 -0
- package/lib/utils/cors.d.ts +72 -0
- package/lib/utils/workspaceState.d.ts +9 -0
- package/package.json +9 -6
- package/public/status.html +77 -1
- package/public/lib/index.d.ts +0 -27
- package/public/lib/index.mjs +0 -110
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent endpoints (EXPERIMENTAL).
|
|
3
|
+
*
|
|
4
|
+
* GET /agents configured agent names
|
|
5
|
+
* POST /agents/:name/run run a prompt; ?stream=1 for SSE events
|
|
6
|
+
* (first a `session` event with the id,
|
|
7
|
+
* then `agent` events, then result/error)
|
|
8
|
+
* GET /agents/:name/sessions newest-first session summaries
|
|
9
|
+
* GET /agents/:name/sessions/:id full transcript (pi messages)
|
|
10
|
+
* POST /agents/:name/sessions/:id/abort abort the active run on a session
|
|
11
|
+
*
|
|
12
|
+
* Configured via [[agents]] tables. These endpoints double as the surface the
|
|
13
|
+
* `bricks-buttress agent` CLI (and future clients) consume.
|
|
14
|
+
*
|
|
15
|
+
* Auth mirrors the functions surface, not the open inference endpoints:
|
|
16
|
+
* agents execute local functions, so an unbound server rejects every call
|
|
17
|
+
* unless `[agents_options] allow_unauthenticated = true`. A bound server
|
|
18
|
+
* accepts workspace JWTs. The server's own ephemeral internal token (also
|
|
19
|
+
* written to the 0600 runtime file for same-host CLIs) is always accepted.
|
|
20
|
+
*/
|
|
21
|
+
import { type AgentsService } from '../agent/types';
|
|
22
|
+
/**
|
|
23
|
+
* Slim pi agent events for the wire. `message_update` carries the FULL partial
|
|
24
|
+
* assistant message on every delta — per-token that would dwarf the payload —
|
|
25
|
+
* and tool results can be huge; clients that want the transcript fetch it
|
|
26
|
+
* from the sessions endpoint afterwards.
|
|
27
|
+
*/
|
|
28
|
+
export declare const slimAgentEvent: (event: any) => unknown;
|
|
29
|
+
export declare function streamAgentRun(agents: AgentsService, name: string, body: {
|
|
30
|
+
prompt: string;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
fork?: boolean;
|
|
33
|
+
}, signal: AbortSignal | undefined): AsyncGenerator<{
|
|
34
|
+
readonly event: string;
|
|
35
|
+
readonly data: string;
|
|
36
|
+
}, void, unknown>;
|
|
37
|
+
export default function factory(agents: AgentsService): import("../types").ButtressApp;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Note: This feature is experimental and may change in future versions.
|
|
13
13
|
*/
|
|
14
|
-
import type { EventStream
|
|
14
|
+
import type { EventStream } from '../types';
|
|
15
15
|
/**
|
|
16
16
|
* Stream an Anthropic Messages SSE response from the backend stream.
|
|
17
17
|
*
|
|
@@ -52,4 +52,4 @@ export declare function streamAnthropicMessage(completionStream: ReadableStream<
|
|
|
52
52
|
readonly event: "message_stop";
|
|
53
53
|
readonly data: string;
|
|
54
54
|
}, void, unknown>;
|
|
55
|
-
export default function factory(
|
|
55
|
+
export default function factory(): import("../types").ButtressApp;
|
package/lib/routes/index.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export { default as status } from './status';
|
|
|
4
4
|
export { default as openaiCompatFactory } from './openai-compat';
|
|
5
5
|
export { default as anthropicMessagesFactory } from './anthropic-messages';
|
|
6
6
|
export { default as functionsFactory } from './functions';
|
|
7
|
+
export { default as agentsFactory } from './agents';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Note: This feature is experimental and may change in future versions.
|
|
8
8
|
*/
|
|
9
|
-
import type { EventStream
|
|
9
|
+
import type { EventStream } from '../types';
|
|
10
10
|
/**
|
|
11
11
|
* Stream an OpenAI-compatible chat completion (SSE) from the backend stream.
|
|
12
12
|
* Mirrors collectChatCompletion (the non-streaming path).
|
|
@@ -14,4 +14,4 @@ import type { EventStream, Config } from '../types';
|
|
|
14
14
|
export declare function streamChatCompletion(completionStream: ReadableStream<EventStream>, completionId: string, created: number, modelId: string, includeUsage: boolean): AsyncGenerator<{
|
|
15
15
|
readonly data: string;
|
|
16
16
|
}, void, unknown>;
|
|
17
|
-
export default function factory(
|
|
17
|
+
export default function factory(): import("../types").ButtressApp;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{a as e,t}from"./client-BCBBen9i.mjs";import{Container as n,Editor as r,Loader as i,Markdown as a,ProcessTerminal as o,Text as s,TuiMainScreen as c}from"@earendil-works/pi-tui";const l=e=>t=>`\x1b[${e}m${t}\x1b[0m`,u=l(`2`),d=l(`1`),f=l(`36`),p=l(`33`),m=l(`31`),h=l(`32`),g=l(`35`),_={heading:e=>d(f(e)),link:f,linkUrl:e=>u(f(e)),code:p,codeBlock:p,codeBlockBorder:u,quote:u,quoteBorder:u,hr:u,listBullet:f,bold:d,italic:l(`3`),strikethrough:l(`9`),underline:l(`4`)},v={borderColor:u,selectList:{selectedPrefix:f,selectedText:d,description:u,scrollInfo:u,noMatch:u}},y=(e,t)=>e.length>t?`${e.slice(0,t)}…`:e;var b=class{tui;chat;thinking=null;thinkingBuffer=``;answer=null;answerBuffer=``;constructor(e,t){this.tui=e,this.chat=t}touch(){this.tui.requestRender()}appendThinking(e){this.thinking||(this.thinking=new s(``,0,0),this.chat.addChild(this.thinking)),this.thinkingBuffer+=e,this.thinking.setText(u(this.thinkingBuffer.trimEnd())),this.touch()}appendAnswer(e){this.answer||(this.answer=new a(``,0,0,_),this.chat.addChild(this.answer)),this.answerBuffer+=e,this.answer.setText(this.answerBuffer),this.touch()}addLine(e){this.answer=null,this.answerBuffer=``,this.chat.addChild(new s(e,0,0)),this.touch()}};const x=async({connection:a,agentName:l,sessionId:_,fork:x})=>{let S=new o,C=new c(S);S.setTitle(`buttress agent · ${l}`);let w=new n,T=new s(``,0,0),E=new r(C,v);C.addChild(w),C.addChild(T),C.addChild(E),C.setFocus(E);let D=_,O=x,k=null,A=null,j=e=>{T.setText(e),C.requestRender()},M=()=>{A||(A=new i(C,f,u,`thinking…`),w.addChild(A),C.requestRender())},N=()=>{A&&(w.removeChild(A),A=null,C.requestRender())},P=`${d(l)} ${u(`@ ${a.baseUrl}`)}`+(D?` ${u(`(${O?`forking`:`continuing`} ${D})`)}`:``);w.addChild(new s(P,0,0)),j(u(`Enter to send · /new fresh session · /exit or Ctrl+C to quit`));let F=async()=>{C.stop(),await S.drainInput().catch(()=>{}),process.exit(0)};C.addInputListener(e=>{if(e===``)return k?(k.abort(),k=null,{consume:!0}):(F(),{consume:!0})});let I=async n=>{w.addChild(new s(`${f(`>`)} ${n}`,0,0));let r=new b(C,w),i=D;k=new AbortController,M(),j(u(`running · Ctrl+C aborts this turn`));let o;try{o=await e(a,l,{prompt:n,sessionId:D??void 0,fork:O},{signal:k.signal,onFrame:({event:e,payload:t})=>{if(e===`session`&&t?.sessionId){t.sessionId!==D&&(D=t.sessionId,w.addChild(new s(u(`session ${D}`),0,0)));return}if(e!==`agent`)return;N();let n=t?.event;if(t?.type===`message_update`&&n)n.type===`thinking_delta`?r.appendThinking(n.delta??``):n.type===`text_delta`&&r.appendAnswer(n.delta??``);else if(t?.type===`tool_execution_start`){let e=JSON.stringify(t.args??{});r.addLine(`${p(`⚙ ${t.toolName}`)}${u(`(${y(e,120)})`)}`)}else t?.type===`tool_execution_end`?r.addLine(t.isError?m(`✗ ${t.toolName} failed`):u(`✓ ${t.toolName}`)):t?.type===`tool_emit`&&r.addLine(u(` ${t.toolName} → ${t.event}`))}})}catch(e){o={kind:`error`,message:e?.message||String(e),sessionId:D}}if(N(),k=null,o.kind===`result`){let{result:e}=o;D=e.sessionId??D;let t=e.usage||{};w.addChild(new s(u(`— ${e.stopReason} · ${t.totalTurns??`?`} turn(s) · ${t.input??0} in / ${t.output??0} out · session ${D}`),0,0))}else o.kind===`aborted`?(D=o.sessionId??D,w.addChild(new s(p(`(aborted — session is continuable)`),0,0))):o.kind===`error`?(D=o.sessionId??D,w.addChild(new s(m(`Run failed: ${o.message}`),0,0))):w.addChild(new s(m(`Connection lost mid-run.`),0,0));t(i,D)&&(O=!1),j(u(`Enter to send · /new fresh session · /exit or Ctrl+C to quit`)),C.requestRender()};E.onSubmit=e=>{let t=e.trim();if(E.setText(``),t){if(k){j(p(`A turn is still running — Ctrl+C aborts it.`));return}if(t===`/exit`||t===`/quit`){F();return}if(t===`/new`){D=null,O=!1,w.addChild(new s(`${h(`•`)} ${u(`started a fresh session`)}`,0,0)),C.requestRender();return}if(t===`/session`){w.addChild(new s(u(D?`session ${D}`:`no session yet`),0,0)),C.requestRender();return}I(t)}},w.addChild(new s(u(g(`ready`)),0,0)),C.start(),await new Promise(()=>{})};export{x as runAgentTui};
|
package/lib/types.d.ts
CHANGED
|
@@ -53,6 +53,10 @@ export type GlobalConfig = {
|
|
|
53
53
|
config?: Record<string, any>;
|
|
54
54
|
cors_allowed_origins?: string | string[];
|
|
55
55
|
};
|
|
56
|
+
/** `[[agents]]` tables; validated by `agent/config.ts` at startup. */
|
|
57
|
+
agents?: Record<string, any>[];
|
|
58
|
+
/** `[agents_options]`: sessions_dir, retention, max_depth, allow_unauthenticated. */
|
|
59
|
+
agents_options?: Record<string, any>;
|
|
56
60
|
} & Record<string, any>;
|
|
57
61
|
export type AutodiscoverConfig = {
|
|
58
62
|
udp: {
|
|
@@ -135,6 +139,11 @@ export type ServerInfo = {
|
|
|
135
139
|
enabled: boolean;
|
|
136
140
|
count: number;
|
|
137
141
|
};
|
|
142
|
+
/** Agent surface: a flag and a count, for the same ANNOUNCE-size reason. */
|
|
143
|
+
agents?: {
|
|
144
|
+
enabled: boolean;
|
|
145
|
+
count: number;
|
|
146
|
+
};
|
|
138
147
|
};
|
|
139
148
|
export type EventStream = {
|
|
140
149
|
event: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Config } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* The server's whole CORS policy, decided in one place, by path.
|
|
4
|
+
*
|
|
5
|
+
* Every surface used to install its own `@elysiajs/cors` plugin on its own
|
|
6
|
+
* router. That does not do what it looks like it does: the plugin works through
|
|
7
|
+
* an `onRequest` hook, which Elysia runs *before* routing and therefore applies
|
|
8
|
+
* app-wide regardless of the router it was mounted on, and the plugins' default
|
|
9
|
+
* response headers all merge into one map on the root app. The four policies
|
|
10
|
+
* ended up fighting over a single set of `Access-Control-*` headers, last
|
|
11
|
+
* writer winning:
|
|
12
|
+
*
|
|
13
|
+
* - `/buttress/info` ran without an explicit `allowedHeaders`, which makes
|
|
14
|
+
* @elysiajs/cors echo the request's own header names back. That answer
|
|
15
|
+
* overwrote every other surface's allow-list, so `Content-Type` was missing
|
|
16
|
+
* from it and any browser `POST /functions/<name>` with a JSON body failed
|
|
17
|
+
* preflight — the symptom that surfaced this.
|
|
18
|
+
* - With that route disabled the *last registered* plugin's header list won
|
|
19
|
+
* instead, so `/anthropic-messages` was served `/functions`'s list and lost
|
|
20
|
+
* `x-api-key` / `anthropic-version`.
|
|
21
|
+
* - `handleOrigin` only ever sets `Access-Control-Allow-Origin` on a match and
|
|
22
|
+
* never clears it on a miss, so an origin allow-listed for one surface stayed
|
|
23
|
+
* reflected on the others — per-surface `cors_allowed_origins` meant nothing.
|
|
24
|
+
*
|
|
25
|
+
* Hence: one hook, one policy per request, chosen by path. A request that no
|
|
26
|
+
* policy owns gets no `Access-Control-*` headers at all.
|
|
27
|
+
*
|
|
28
|
+
* This is CORS only — it decides what a *browser* is willing to hand to script.
|
|
29
|
+
* It is not the access gate. `functionsAuthGuard` (cross-site `Origin` /
|
|
30
|
+
* `Sec-Fetch-Site` check) and `buttressAuthGuard` (workspace JWT) still run on
|
|
31
|
+
* every call and are what actually keeps callers out.
|
|
32
|
+
*/
|
|
33
|
+
export type CorsPolicy = {
|
|
34
|
+
/** Path the policy owns: this exact path, or anything below it. */
|
|
35
|
+
path: string;
|
|
36
|
+
/** `true` reflects whatever `Origin` asked. Only for surfaces with no secrets. */
|
|
37
|
+
origin: true | string | string[];
|
|
38
|
+
methods: string[];
|
|
39
|
+
allowedHeaders: string[];
|
|
40
|
+
/** Preflight cache lifetime, in seconds. */
|
|
41
|
+
maxAge: number;
|
|
42
|
+
/**
|
|
43
|
+
* Emit `Access-Control-Allow-Credentials`. Never combined with `origin: true`:
|
|
44
|
+
* reflecting an arbitrary origin *and* allowing credentials is the pairing
|
|
45
|
+
* that turns a public endpoint into a cross-site read primitive.
|
|
46
|
+
*/
|
|
47
|
+
credentials: boolean;
|
|
48
|
+
/** Answer Chrome's Private Network Access opt-in when the client asks for it. */
|
|
49
|
+
privateNetwork?: boolean;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Build the policy table from config. The `enable*` flags mirror the conditions
|
|
53
|
+
* `startServer` mounts each router under, so a surface that isn't served never
|
|
54
|
+
* gets a CORS answer either.
|
|
55
|
+
*/
|
|
56
|
+
export declare const buildCorsPolicies: (config: Config, enabled?: {
|
|
57
|
+
openaiCompat?: boolean;
|
|
58
|
+
anthropicMessages?: boolean;
|
|
59
|
+
functions?: boolean;
|
|
60
|
+
}) => CorsPolicy[];
|
|
61
|
+
/**
|
|
62
|
+
* Install the policy table as the single writer of `Access-Control-*` headers.
|
|
63
|
+
*
|
|
64
|
+
* Preflights are answered from the hook rather than from `OPTIONS` routes: the
|
|
65
|
+
* hook runs before routing, so there is nothing to collide with a real route,
|
|
66
|
+
* and a preflight for an unmounted path simply falls through to a 404. A bare
|
|
67
|
+
* `OPTIONS` with no `Access-Control-Request-Method` is not a preflight and is
|
|
68
|
+
* left to routing.
|
|
69
|
+
*/
|
|
70
|
+
export declare const installCors: <T extends {
|
|
71
|
+
onRequest: (handler: any) => unknown;
|
|
72
|
+
}>(app: T, policies: CorsPolicy[]) => T;
|
|
@@ -11,9 +11,18 @@ export interface ServerKeyPair {
|
|
|
11
11
|
privateKeyPkcs8: string;
|
|
12
12
|
kid: string;
|
|
13
13
|
}
|
|
14
|
+
export interface BankBinding {
|
|
15
|
+
/** Public Data Bank API base URL, e.g. https://bank.bricks.tools */
|
|
16
|
+
endpoint: string;
|
|
17
|
+
spacename: string;
|
|
18
|
+
spacekey: string;
|
|
19
|
+
keyName?: string;
|
|
20
|
+
issuedAt?: string;
|
|
21
|
+
}
|
|
14
22
|
export interface WorkspaceState {
|
|
15
23
|
workspace: WorkspaceBinding | null;
|
|
16
24
|
serverKeyPair: ServerKeyPair | null;
|
|
25
|
+
bank: BankBinding | null;
|
|
17
26
|
}
|
|
18
27
|
export declare const resolveStateDir: () => string;
|
|
19
28
|
export declare const resolveStatePath: () => string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugood/buttress-server",
|
|
3
|
-
"version": "2.25.
|
|
3
|
+
"version": "2.25.7",
|
|
4
4
|
"main": "lib/index.mjs",
|
|
5
5
|
"types": "lib/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"typecheck": "tsc --noEmit",
|
|
22
|
-
"build": "tsdown -c rolldown.config.js --config-loader native && tsc --noCheck --emitDeclarationOnly",
|
|
22
|
+
"build": "tsdown -c rolldown.config.js --config-loader native && tsc --noCheck --emitDeclarationOnly && cp ../buttress-backend-core/src/backends/mlx-bridge.py lib/mlx-bridge.py",
|
|
23
23
|
"build:dist": "$npm_execpath scripts/build-distribution.js",
|
|
24
24
|
"release": "$npm_execpath scripts/build-distribution.js",
|
|
25
25
|
"release-beta": "$npm_execpath scripts/build-distribution.js --beta",
|
|
@@ -37,13 +37,16 @@
|
|
|
37
37
|
],
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"@earendil-works/pi-agent-core": "^0.85.0",
|
|
41
|
+
"@earendil-works/pi-ai": "0.85.0",
|
|
42
|
+
"@earendil-works/pi-tui": "^0.85.0",
|
|
40
43
|
"@elysiajs/cors": "^1.1.1",
|
|
41
44
|
"@elysiajs/node": "^1.4.2",
|
|
42
|
-
"@fugood/llama.node": "^1.8.0-rc.
|
|
43
|
-
"@fugood/whisper.node": "^1.1.
|
|
45
|
+
"@fugood/llama.node": "^1.8.0-rc.5",
|
|
46
|
+
"@fugood/whisper.node": "^1.1.3",
|
|
44
47
|
"@huggingface/gguf": "^0.3.2",
|
|
45
48
|
"@iarna/toml": "^3.0.0",
|
|
46
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
47
50
|
"bytes": "^3.1.0",
|
|
48
51
|
"chroma-js": "^2.1.2",
|
|
49
52
|
"elysia": "^1.4.19",
|
|
@@ -69,5 +72,5 @@
|
|
|
69
72
|
"tsdown": "^0.22.4",
|
|
70
73
|
"typescript": "^7.0.2"
|
|
71
74
|
},
|
|
72
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "984a440ed04862f12c65f3cf62bdc70a938fcdd6"
|
|
73
76
|
}
|
package/public/status.html
CHANGED
|
@@ -610,6 +610,16 @@
|
|
|
610
610
|
<span class="badge badge-info" id="functionsCount">0 functions</span>
|
|
611
611
|
</div>
|
|
612
612
|
<div id="functionsSummary"></div>
|
|
613
|
+
<div class="section" id="functionsDaemonsSection" style="display:none">
|
|
614
|
+
<div class="section-title collapsible" onclick="toggleSection(this)">Daemons</div>
|
|
615
|
+
<div class="collapsible-content" id="functionsDaemons"></div>
|
|
616
|
+
</div>
|
|
617
|
+
<div class="section" id="functionsDaemonHistorySection" style="display:none">
|
|
618
|
+
<div class="section-title collapsible" onclick="toggleSection(this)">Daemon Activity</div>
|
|
619
|
+
<div class="collapsible-content" id="functionsDaemonHistory">
|
|
620
|
+
<div class="empty-state">No daemon activity</div>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
613
623
|
<div class="section">
|
|
614
624
|
<div class="section-title collapsible" onclick="toggleSection(this)">Call History</div>
|
|
615
625
|
<div class="collapsible-content" id="functionsCallHistory">
|
|
@@ -1287,8 +1297,10 @@
|
|
|
1287
1297
|
if (!fns.enabled) return
|
|
1288
1298
|
|
|
1289
1299
|
const count = fns.count ?? 0
|
|
1300
|
+
const daemonCount = fns.daemonCount ?? 0
|
|
1290
1301
|
document.getElementById('functionsCount').textContent =
|
|
1291
|
-
`${count} function${count !== 1 ? 's' : ''}`
|
|
1302
|
+
`${count} function${count !== 1 ? 's' : ''}` +
|
|
1303
|
+
(daemonCount > 0 ? ` · ${daemonCount} daemon${daemonCount !== 1 ? 's' : ''}` : '')
|
|
1292
1304
|
|
|
1293
1305
|
const c = fns.counters || {}
|
|
1294
1306
|
const summary = document.getElementById('functionsSummary')
|
|
@@ -1309,6 +1321,7 @@
|
|
|
1309
1321
|
</thead>
|
|
1310
1322
|
<tbody>
|
|
1311
1323
|
${stat('Calls', c.calls?.total ?? 0, c.calls?.failed ? `${c.calls.failed} failed` : '')}
|
|
1324
|
+
${stat('Daemon runs', c.daemons?.invocations ?? 0, c.daemons?.failed ? `${c.daemons.failed} failed` : '')}
|
|
1312
1325
|
${stat('Uploads', `${c.uploads?.total ?? 0} (${formatBytes(c.uploads?.bytes ?? 0)})`, c.uploads?.failed ? `${c.uploads.failed} failed` : '')}
|
|
1313
1326
|
${stat('Downloads', `${c.downloads?.total ?? 0} (${formatBytes(c.downloads?.bytes ?? 0)})`, c.downloads?.missed ? `${c.downloads.missed} missed` : '')}
|
|
1314
1327
|
${stat('Auth checks', c.auth?.total ?? 0, c.auth?.denied ? `${c.auth.denied} denied` : '')}
|
|
@@ -1319,6 +1332,8 @@
|
|
|
1319
1332
|
`
|
|
1320
1333
|
})
|
|
1321
1334
|
|
|
1335
|
+
renderFunctionsDaemons(fns.daemons || [], fns.history?.daemons || [])
|
|
1336
|
+
|
|
1322
1337
|
const history = fns.history || {}
|
|
1323
1338
|
const statusBadge = i => i.success ?
|
|
1324
1339
|
'<span class="badge badge-success">Success</span>' :
|
|
@@ -1362,6 +1377,67 @@
|
|
|
1362
1377
|
])
|
|
1363
1378
|
}
|
|
1364
1379
|
|
|
1380
|
+
// Live daemon state + recent daemon event invocations
|
|
1381
|
+
function renderFunctionsDaemons(daemons, activity) {
|
|
1382
|
+
const hasDaemons = daemons.length > 0
|
|
1383
|
+
document.getElementById('functionsDaemonsSection').style.display = hasDaemons ? '' : 'none'
|
|
1384
|
+
document.getElementById('functionsDaemonHistorySection').style.display =
|
|
1385
|
+
hasDaemons || activity.length > 0 ? '' : 'none'
|
|
1386
|
+
|
|
1387
|
+
if (hasDaemons) {
|
|
1388
|
+
const container = document.getElementById('functionsDaemons')
|
|
1389
|
+
const stateBadge = d => d.state === 'running' ?
|
|
1390
|
+
'<span class="badge badge-success">Running</span>' :
|
|
1391
|
+
`<span class="badge badge-error">Error${d.error ? `: ${escapeHtml(d.error)}` : ''}</span>`
|
|
1392
|
+
const bankBadge = d => {
|
|
1393
|
+
if (!d.bankSubscriptions) return '-'
|
|
1394
|
+
const cls = d.bank === 'connected' ? 'badge-success' :
|
|
1395
|
+
d.bank === 'connecting' ? 'badge-info' : 'badge-warning'
|
|
1396
|
+
return `<span class="badge ${cls}">${escapeHtml(d.bank || 'connecting')} (${d.bankSubscriptions})</span>`
|
|
1397
|
+
}
|
|
1398
|
+
withScrollPreserve(container, () => {
|
|
1399
|
+
container.innerHTML = `
|
|
1400
|
+
<div class="table-wrapper">
|
|
1401
|
+
<div class="table-inner">
|
|
1402
|
+
<table>
|
|
1403
|
+
<thead>
|
|
1404
|
+
<tr>
|
|
1405
|
+
<th>Daemon</th><th>State</th><th>Started</th><th>Timers</th>
|
|
1406
|
+
<th>Bank</th><th>Events</th><th>Runs</th>
|
|
1407
|
+
</tr>
|
|
1408
|
+
</thead>
|
|
1409
|
+
<tbody>
|
|
1410
|
+
${daemons.map(d => `
|
|
1411
|
+
<tr>
|
|
1412
|
+
<td title="${escapeHtml(d.description || '')}">${escapeHtml(d.name)}</td>
|
|
1413
|
+
<td>${stateBadge(d)}</td>
|
|
1414
|
+
<td><span class="timestamp">${d.startedAt ? formatRelativeTime(d.startedAt) : '-'}</span></td>
|
|
1415
|
+
<td>${d.timers ?? 0}</td>
|
|
1416
|
+
<td>${bankBadge(d)}</td>
|
|
1417
|
+
<td>${d.listening ? '<span class="badge badge-info">listening</span>' : '-'}</td>
|
|
1418
|
+
<td>${d.counts?.runs ?? 0}${d.counts?.failed ? ` <span class="badge badge-error">${d.counts.failed} failed</span>` : ''}</td>
|
|
1419
|
+
</tr>
|
|
1420
|
+
`).join('')}
|
|
1421
|
+
</tbody>
|
|
1422
|
+
</table>
|
|
1423
|
+
</div>
|
|
1424
|
+
</div>
|
|
1425
|
+
`
|
|
1426
|
+
})
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
renderHistory('functionsDaemonHistory', activity, [
|
|
1430
|
+
{ label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
|
|
1431
|
+
{ label: 'Daemon', render: i => escapeHtml(i.name) },
|
|
1432
|
+
{ label: 'Event', render: i => `<span class="badge badge-info">${escapeHtml(i.event || '-')}</span>` },
|
|
1433
|
+
{ label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(2)}s` },
|
|
1434
|
+
{ label: 'Status', render: i => i.success ?
|
|
1435
|
+
'<span class="badge badge-success">Success</span>' :
|
|
1436
|
+
`<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>`
|
|
1437
|
+
},
|
|
1438
|
+
])
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1365
1441
|
// Fallback: Fetch status via HTTP polling
|
|
1366
1442
|
let pollingInterval = null
|
|
1367
1443
|
|
package/public/lib/index.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { AnyElysia } from "elysia";
|
|
2
|
-
import * as backendCore from "@fugood/buttress-backend-core";
|
|
3
|
-
import { AutodiscoverService } from "./autodiscover";
|
|
4
|
-
import type { Config } from "./types";
|
|
5
|
-
export { startModelDownload } from "@fugood/buttress-backend-core";
|
|
6
|
-
export { processConfig } from "./utils/config";
|
|
7
|
-
export declare const checkForUpdates: () => Promise<string | null>;
|
|
8
|
-
export declare const compareVersions: (current: string, latest: string) => boolean;
|
|
9
|
-
export declare const logUpdateMessage: (latestVersion: string) => void;
|
|
10
|
-
export declare const checkAndNotifyUpdates: () => Promise<void>;
|
|
11
|
-
export type Backend = typeof backendCore;
|
|
12
|
-
export interface StartServerOptions {
|
|
13
|
-
backend?: Backend;
|
|
14
|
-
router?: AnyElysia;
|
|
15
|
-
config: Config;
|
|
16
|
-
enableOpenAICompat?: boolean;
|
|
17
|
-
}
|
|
18
|
-
export declare const createServer: ({ backend, router, config, enableOpenAICompat }: StartServerOptions) => Promise<{
|
|
19
|
-
app: AnyElysia;
|
|
20
|
-
config: Config;
|
|
21
|
-
}>;
|
|
22
|
-
export declare const startServer: ({ backend, router, config, enableOpenAICompat }: StartServerOptions) => Promise<{
|
|
23
|
-
app: AnyElysia;
|
|
24
|
-
port: number;
|
|
25
|
-
openaiEnabled: boolean;
|
|
26
|
-
autoDiscover: AutodiscoverService | null;
|
|
27
|
-
}>;
|