@agentprojectcontext/apx 1.15.6 → 1.16.0
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/package.json +40 -5
- package/src/cli/commands/log.js +113 -0
- package/src/cli/commands/overlay.js +253 -0
- package/src/cli/commands/sys.js +88 -16
- package/src/cli/index.js +23 -1
- package/src/cli/terminal-chat/renderer.js +71 -56
- package/src/cli-ts/commands/agent.ts +173 -0
- package/src/cli-ts/commands/chat.ts +119 -0
- package/src/cli-ts/commands/daemon.ts +112 -0
- package/src/cli-ts/commands/exec.ts +109 -0
- package/src/cli-ts/commands/mcp.ts +235 -0
- package/src/cli-ts/commands/session.ts +224 -0
- package/src/cli-ts/commands/status.ts +61 -0
- package/src/cli-ts/http.ts +36 -0
- package/src/cli-ts/index.ts +73 -0
- package/src/cli-ts/ui.ts +107 -0
- package/src/core/logging.js +81 -0
- package/src/daemon/api.js +58 -0
- package/src/daemon/engines/anthropic.js +60 -1
- package/src/daemon/engines/index.js +2 -1
- package/src/daemon/engines/ollama.js +70 -3
- package/src/daemon/index.js +58 -0
- package/src/daemon/overlay-ws.js +40 -0
- package/src/daemon/plugins/index.js +2 -1
- package/src/daemon/plugins/overlay.js +177 -0
- package/src/daemon/plugins/telegram.js +15 -3
- package/src/daemon/super-agent.js +102 -19
- package/src/daemon/transcription.js +262 -59
- package/src/daemon/whisper-server.py +57 -6
- package/src/overlay/index.html +44 -0
- package/src/overlay/main.js +480 -0
- package/src/overlay/package.json +3 -0
- package/src/overlay/preload.js +34 -0
- package/src/overlay/renderer.js +371 -0
- package/src/overlay/style.css +250 -0
- package/src/tui/_shims/cli-error.ts +6 -0
- package/src/tui/_shims/cli-logo.ts +18 -0
- package/src/tui/_shims/cli-ui.ts +1 -0
- package/src/tui/_shims/config-console-state.ts +7 -0
- package/src/tui/_shims/core-any.ts +30 -0
- package/src/tui/_shims/core-binary.ts +13 -0
- package/src/tui/_shims/core-flag.ts +3 -0
- package/src/tui/_shims/core-log.ts +14 -0
- package/src/tui/_shims/lsp-language.ts +1 -0
- package/src/tui/_shims/opencode-any.ts +135 -0
- package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
- package/src/tui/_shims/plugin-tui.ts +13 -0
- package/src/tui/_shims/provider-provider.ts +10 -0
- package/src/tui/_shims/session-retry.ts +1 -0
- package/src/tui/_shims/session-schema.ts +15 -0
- package/src/tui/_shims/session-session.ts +3 -0
- package/src/tui/_shims/snapshot.ts +4 -0
- package/src/tui/_shims/tool-any.ts +18 -0
- package/src/tui/_shims/util-error.ts +7 -0
- package/src/tui/_shims/util-filesystem.ts +79 -0
- package/src/tui/_shims/util-format.ts +7 -0
- package/src/tui/_shims/util-iife.ts +3 -0
- package/src/tui/_shims/util-locale.ts +10 -0
- package/src/tui/_shims/util-process.ts +38 -0
- package/src/tui/app.tsx +783 -0
- package/src/tui/asset/charge.wav +0 -0
- package/src/tui/asset/pulse-a.wav +0 -0
- package/src/tui/asset/pulse-b.wav +0 -0
- package/src/tui/asset/pulse-c.wav +0 -0
- package/src/tui/attach.ts +100 -0
- package/src/tui/component/bg-pulse-render.ts +436 -0
- package/src/tui/component/bg-pulse.tsx +99 -0
- package/src/tui/component/border.tsx +21 -0
- package/src/tui/component/dialog-agent.tsx +31 -0
- package/src/tui/component/dialog-console-org.tsx +103 -0
- package/src/tui/component/dialog-mcp.tsx +85 -0
- package/src/tui/component/dialog-model.tsx +175 -0
- package/src/tui/component/dialog-provider.tsx +456 -0
- package/src/tui/component/dialog-retry-action.tsx +160 -0
- package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
- package/src/tui/component/dialog-session-list.tsx +323 -0
- package/src/tui/component/dialog-session-rename.tsx +31 -0
- package/src/tui/component/dialog-skill.tsx +36 -0
- package/src/tui/component/dialog-stash.tsx +87 -0
- package/src/tui/component/dialog-status.tsx +168 -0
- package/src/tui/component/dialog-tag.tsx +44 -0
- package/src/tui/component/dialog-theme-list.tsx +50 -0
- package/src/tui/component/dialog-variant.tsx +39 -0
- package/src/tui/component/dialog-workspace-create.tsx +302 -0
- package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
- package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/tui/component/error-component.tsx +92 -0
- package/src/tui/component/logo.tsx +896 -0
- package/src/tui/component/plugin-route-missing.tsx +14 -0
- package/src/tui/component/prompt/autocomplete.tsx +869 -0
- package/src/tui/component/prompt/cwd.ts +0 -0
- package/src/tui/component/prompt/frecency.tsx +90 -0
- package/src/tui/component/prompt/history.tsx +108 -0
- package/src/tui/component/prompt/index.tsx +1809 -0
- package/src/tui/component/prompt/part.ts +16 -0
- package/src/tui/component/prompt/stash.tsx +101 -0
- package/src/tui/component/prompt/traits.ts +35 -0
- package/src/tui/component/spinner.tsx +24 -0
- package/src/tui/component/startup-loading.tsx +63 -0
- package/src/tui/component/todo-item.tsx +32 -0
- package/src/tui/component/use-connected.tsx +9 -0
- package/src/tui/component/workspace-label.tsx +19 -0
- package/src/tui/config/cwd.ts +5 -0
- package/src/tui/config/keybind.ts +432 -0
- package/src/tui/config/tui-migrate.ts +154 -0
- package/src/tui/config/tui-schema.ts +34 -0
- package/src/tui/config/tui.ts +46 -0
- package/src/tui/context/aggregate-failures.ts +34 -0
- package/src/tui/context/args.tsx +15 -0
- package/src/tui/context/command-palette.tsx +163 -0
- package/src/tui/context/directory.ts +15 -0
- package/src/tui/context/editor-zed.ts +283 -0
- package/src/tui/context/editor.ts +468 -0
- package/src/tui/context/event-apx.ts +22 -0
- package/src/tui/context/event.ts +6 -0
- package/src/tui/context/exit.tsx +60 -0
- package/src/tui/context/helper.tsx +25 -0
- package/src/tui/context/kv.tsx +81 -0
- package/src/tui/context/local.tsx +608 -0
- package/src/tui/context/path-format.tsx +39 -0
- package/src/tui/context/project-apx.tsx +48 -0
- package/src/tui/context/project.tsx +7 -0
- package/src/tui/context/prompt.tsx +18 -0
- package/src/tui/context/route.tsx +52 -0
- package/src/tui/context/sdk-apx.tsx +185 -0
- package/src/tui/context/sdk.tsx +6 -0
- package/src/tui/context/sync-apx.tsx +178 -0
- package/src/tui/context/sync-v2.tsx +16 -0
- package/src/tui/context/sync.tsx +118 -0
- package/src/tui/context/theme/aura.json +69 -0
- package/src/tui/context/theme/ayu.json +80 -0
- package/src/tui/context/theme/carbonfox.json +248 -0
- package/src/tui/context/theme/catppuccin-frappe.json +230 -0
- package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
- package/src/tui/context/theme/catppuccin.json +112 -0
- package/src/tui/context/theme/cobalt2.json +225 -0
- package/src/tui/context/theme/cursor.json +249 -0
- package/src/tui/context/theme/dracula.json +219 -0
- package/src/tui/context/theme/everforest.json +241 -0
- package/src/tui/context/theme/flexoki.json +237 -0
- package/src/tui/context/theme/github.json +233 -0
- package/src/tui/context/theme/gruvbox.json +242 -0
- package/src/tui/context/theme/kanagawa.json +77 -0
- package/src/tui/context/theme/lucent-orng.json +234 -0
- package/src/tui/context/theme/material.json +235 -0
- package/src/tui/context/theme/matrix.json +77 -0
- package/src/tui/context/theme/mercury.json +252 -0
- package/src/tui/context/theme/monokai.json +221 -0
- package/src/tui/context/theme/nightowl.json +221 -0
- package/src/tui/context/theme/nord.json +223 -0
- package/src/tui/context/theme/one-dark.json +84 -0
- package/src/tui/context/theme/opencode.json +245 -0
- package/src/tui/context/theme/orng.json +249 -0
- package/src/tui/context/theme/osaka-jade.json +93 -0
- package/src/tui/context/theme/palenight.json +222 -0
- package/src/tui/context/theme/rosepine.json +234 -0
- package/src/tui/context/theme/solarized.json +223 -0
- package/src/tui/context/theme/synthwave84.json +226 -0
- package/src/tui/context/theme/tokyonight.json +243 -0
- package/src/tui/context/theme/vercel.json +245 -0
- package/src/tui/context/theme/vesper.json +218 -0
- package/src/tui/context/theme/zenburn.json +223 -0
- package/src/tui/context/theme.tsx +1247 -0
- package/src/tui/context/tui-config.tsx +9 -0
- package/src/tui/event.ts +16 -0
- package/src/tui/feature-plugins/home/footer.tsx +94 -0
- package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
- package/src/tui/feature-plugins/home/tips.tsx +59 -0
- package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
- package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
- package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/tui/feature-plugins/system/plugins.tsx +269 -0
- package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
- package/src/tui/feature-plugins/system/which-key.tsx +608 -0
- package/src/tui/keymap.tsx +166 -0
- package/src/tui/layer.ts +6 -0
- package/src/tui/plugin/api.tsx +381 -0
- package/src/tui/plugin/command-shim.ts +109 -0
- package/src/tui/plugin/internal.ts +33 -0
- package/src/tui/plugin/runtime.ts +1069 -0
- package/src/tui/plugin/slots.tsx +60 -0
- package/src/tui/routes/home.tsx +96 -0
- package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/tui/routes/session/dialog-message.tsx +108 -0
- package/src/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/tui/routes/session/footer.tsx +91 -0
- package/src/tui/routes/session/index.tsx +188 -0
- package/src/tui/routes/session/permission.tsx +722 -0
- package/src/tui/routes/session/question.tsx +490 -0
- package/src/tui/routes/session/sidebar.tsx +102 -0
- package/src/tui/routes/session/subagent-footer.tsx +133 -0
- package/src/tui/run.ts +84 -0
- package/src/tui/thread.ts +261 -0
- package/src/tui/tsconfig.json +40 -0
- package/src/tui/ui/dialog-alert.tsx +66 -0
- package/src/tui/ui/dialog-confirm.tsx +108 -0
- package/src/tui/ui/dialog-export-options.tsx +217 -0
- package/src/tui/ui/dialog-help.tsx +40 -0
- package/src/tui/ui/dialog-prompt.tsx +101 -0
- package/src/tui/ui/dialog-select.tsx +553 -0
- package/src/tui/ui/dialog.tsx +211 -0
- package/src/tui/ui/link.tsx +34 -0
- package/src/tui/ui/spinner.ts +368 -0
- package/src/tui/ui/toast.tsx +111 -0
- package/src/tui/util/clipboard.ts +217 -0
- package/src/tui/util/editor.ts +37 -0
- package/src/tui/util/model.ts +23 -0
- package/src/tui/util/provider-origin.ts +7 -0
- package/src/tui/util/revert-diff.ts +18 -0
- package/src/tui/util/scroll.ts +25 -0
- package/src/tui/util/selection.ts +65 -0
- package/src/tui/util/signal.ts +41 -0
- package/src/tui/util/sound.ts +156 -0
- package/src/tui/util/transcript.ts +112 -0
- package/src/tui/validate-session.ts +29 -0
- package/src/tui/win32.ts +130 -0
- package/src/tui/worker.ts +104 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// APX CLI v2 — TypeScript entry point, yargs-based.
|
|
2
|
+
// This replaces the manual arg-parsing in src/cli/index.js for commands
|
|
3
|
+
// that have been migrated here. Unmigrated commands delegate to the legacy handler.
|
|
4
|
+
|
|
5
|
+
import yargs from "yargs";
|
|
6
|
+
import { hideBin } from "yargs/helpers";
|
|
7
|
+
import { sessionCmd } from "./commands/session.js";
|
|
8
|
+
import { agentCmd } from "./commands/agent.js";
|
|
9
|
+
import { mcpCmd } from "./commands/mcp.js";
|
|
10
|
+
import { daemonCmd } from "./commands/daemon.js";
|
|
11
|
+
import { statusCmd } from "./commands/status.js";
|
|
12
|
+
import { execCmd } from "./commands/exec.js";
|
|
13
|
+
import { chatCmd } from "./commands/chat.js";
|
|
14
|
+
|
|
15
|
+
process.on("unhandledRejection", (err) => {
|
|
16
|
+
process.stderr.write(
|
|
17
|
+
"\x1b[91m✖ Unhandled error:\x1b[0m " + String(err) + "\n",
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
yargs(hideBin(process.argv))
|
|
23
|
+
.scriptName("apx")
|
|
24
|
+
.usage("$0 <command> [options]")
|
|
25
|
+
.version(false) // we handle --version ourselves below
|
|
26
|
+
.option("project", {
|
|
27
|
+
alias: "p",
|
|
28
|
+
type: "string",
|
|
29
|
+
describe: "Project name, ID, or path",
|
|
30
|
+
global: true,
|
|
31
|
+
})
|
|
32
|
+
.option("json", {
|
|
33
|
+
type: "boolean",
|
|
34
|
+
describe: "Output as JSON",
|
|
35
|
+
global: false,
|
|
36
|
+
})
|
|
37
|
+
.command(sessionCmd)
|
|
38
|
+
.command(agentCmd)
|
|
39
|
+
.command(mcpCmd)
|
|
40
|
+
.command(daemonCmd)
|
|
41
|
+
.command(statusCmd)
|
|
42
|
+
.command(execCmd)
|
|
43
|
+
.command(chatCmd)
|
|
44
|
+
.command({
|
|
45
|
+
command: "version",
|
|
46
|
+
aliases: ["--version", "-v"],
|
|
47
|
+
describe: "Print APX version",
|
|
48
|
+
handler: async () => {
|
|
49
|
+
const { createRequire } = await import("node:module");
|
|
50
|
+
const req = createRequire(import.meta.url);
|
|
51
|
+
try {
|
|
52
|
+
const pkg = req("../../package.json") as { version: string };
|
|
53
|
+
process.stdout.write(pkg.version + "\n");
|
|
54
|
+
} catch {
|
|
55
|
+
process.stdout.write("unknown\n");
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
.demandCommand(1, "Specify a command. Use --help for available commands.")
|
|
60
|
+
.strict()
|
|
61
|
+
.wrap(Math.min(100, yargs().terminalWidth()))
|
|
62
|
+
.help()
|
|
63
|
+
.alias("help", "h")
|
|
64
|
+
.fail((msg, err) => {
|
|
65
|
+
if (err) throw err;
|
|
66
|
+
process.stderr.write("\x1b[91m✖ " + msg + "\x1b[0m\n\n");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
})
|
|
69
|
+
.parseAsync()
|
|
70
|
+
.catch((err: Error) => {
|
|
71
|
+
process.stderr.write("\x1b[91m✖ " + err.message + "\x1b[0m\n");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|
package/src/cli-ts/ui.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Terminal UI utilities — ANSI colors, output helpers.
|
|
2
|
+
// Adapted from opencode_ref/packages/opencode/src/cli/ui.ts
|
|
3
|
+
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
|
|
6
|
+
export const Style = {
|
|
7
|
+
TEXT_HIGHLIGHT: "\x1b[96m",
|
|
8
|
+
TEXT_DIM: "\x1b[90m",
|
|
9
|
+
TEXT_NORMAL: "\x1b[0m",
|
|
10
|
+
TEXT_WARNING: "\x1b[93m",
|
|
11
|
+
TEXT_DANGER: "\x1b[91m",
|
|
12
|
+
TEXT_SUCCESS: "\x1b[92m",
|
|
13
|
+
TEXT_INFO: "\x1b[94m",
|
|
14
|
+
TEXT_BOLD: "\x1b[1m",
|
|
15
|
+
TEXT_BOLD_END: "\x1b[22m",
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
let _lastEmpty = false;
|
|
19
|
+
|
|
20
|
+
export function println(...parts: string[]): void {
|
|
21
|
+
_lastEmpty = false;
|
|
22
|
+
process.stderr.write(parts.join(" ") + "\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function print(...parts: string[]): void {
|
|
26
|
+
_lastEmpty = false;
|
|
27
|
+
process.stderr.write(parts.join(" "));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function empty(): void {
|
|
31
|
+
if (_lastEmpty) return;
|
|
32
|
+
_lastEmpty = true;
|
|
33
|
+
process.stderr.write("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function error(message: string): void {
|
|
37
|
+
println(Style.TEXT_DANGER + "✖ " + Style.TEXT_NORMAL + message);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function success(message: string): void {
|
|
41
|
+
println(Style.TEXT_SUCCESS + "✔ " + Style.TEXT_NORMAL + message);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function info(message: string): void {
|
|
45
|
+
println(Style.TEXT_INFO + "ℹ " + Style.TEXT_NORMAL + message);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function warn(message: string): void {
|
|
49
|
+
println(Style.TEXT_WARNING + "⚠ " + Style.TEXT_NORMAL + message);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function dim(message: string): string {
|
|
53
|
+
return Style.TEXT_DIM + message + Style.TEXT_NORMAL;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function highlight(message: string): string {
|
|
57
|
+
return Style.TEXT_HIGHLIGHT + message + Style.TEXT_NORMAL;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function bold(message: string): string {
|
|
61
|
+
return Style.TEXT_BOLD + message + Style.TEXT_BOLD_END;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function input(prompt: string): Promise<string> {
|
|
65
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
66
|
+
return new Promise<string>((resolve) => {
|
|
67
|
+
rl.question(Style.TEXT_HIGHLIGHT + prompt + Style.TEXT_NORMAL + " ", (answer) => {
|
|
68
|
+
rl.close();
|
|
69
|
+
resolve(answer.trim());
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function logo(): void {
|
|
75
|
+
if (!process.stdout.isTTY) {
|
|
76
|
+
println(bold("APX"));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
println(
|
|
80
|
+
Style.TEXT_DIM +
|
|
81
|
+
" ██████╗ ██████╗ ██╗ ██╗\n" +
|
|
82
|
+
Style.TEXT_INFO +
|
|
83
|
+
" ██╔══██╗██╔══██╗╚██╗██╔╝\n" +
|
|
84
|
+
Style.TEXT_HIGHLIGHT +
|
|
85
|
+
" ███████║██████╔╝ ╚███╔╝ \n" +
|
|
86
|
+
" ██╔══██║██╔═══╝ ██╔██╗ \n" +
|
|
87
|
+
Style.TEXT_NORMAL +
|
|
88
|
+
" ██║ ██║██║ ██╔╝ ██╗\n" +
|
|
89
|
+
" ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝" +
|
|
90
|
+
Style.TEXT_NORMAL,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function table(
|
|
95
|
+
rows: Array<Record<string, string>>,
|
|
96
|
+
cols: string[],
|
|
97
|
+
): void {
|
|
98
|
+
const widths = cols.map((col) =>
|
|
99
|
+
Math.max(col.length, ...rows.map((r) => (r[col] ?? "").length)),
|
|
100
|
+
);
|
|
101
|
+
const header = cols.map((col, i) => bold(col.padEnd(widths[i]!))).join(" ");
|
|
102
|
+
println(header);
|
|
103
|
+
println(dim("─".repeat(widths.reduce((a, b) => a + b + 2, -2))));
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
println(cols.map((col, i) => (row[col] ?? "").padEnd(widths[i]!)).join(" "));
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/core/logging.js
CHANGED
|
@@ -4,6 +4,10 @@ import { APX_HOME } from "./config.js";
|
|
|
4
4
|
|
|
5
5
|
export const LOG_DIR = path.join(APX_HOME, "logs");
|
|
6
6
|
export const ERROR_TRACE_PATH = path.join(LOG_DIR, "errors.jsonl");
|
|
7
|
+
// Unified daemon log. Every module (daemon, telegram, whisper, super-agent,
|
|
8
|
+
// tools, overlay) writes here with one consistent format so the user can
|
|
9
|
+
// follow the whole system from a single tail.
|
|
10
|
+
export const APX_LOG_PATH = path.join(LOG_DIR, "apx.log");
|
|
7
11
|
|
|
8
12
|
const SECRET_KEY_RE = /(token|secret|password|api[_-]?key|authorization|bot[_-]?token)/i;
|
|
9
13
|
|
|
@@ -35,3 +39,80 @@ export function previewText(text, max = 500) {
|
|
|
35
39
|
const clean = String(text || "").replace(/\s+/g, " ").trim();
|
|
36
40
|
return clean.length > max ? clean.slice(0, max - 1) + "…" : clean;
|
|
37
41
|
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Unified logger — writes to ~/.apx/logs/apx.log in the format:
|
|
45
|
+
// [2026-05-13 22:35:01.234] [LEVEL ] [module] message {meta}
|
|
46
|
+
//
|
|
47
|
+
// `level` is INFO | WARN | ERROR (case-insensitive). Unknown levels fall back
|
|
48
|
+
// to INFO. `module` is a short tag (telegram, whisper, super-agent, daemon…).
|
|
49
|
+
// `meta` is optional; if present and non-empty, it's stringified at the end.
|
|
50
|
+
// Secrets in meta are redacted via the same SECRET_KEY_RE used by error traces.
|
|
51
|
+
//
|
|
52
|
+
// Returns the line that was written, so callers can also surface it elsewhere
|
|
53
|
+
// (e.g. process.stdout for the daemon's existing stdout log).
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const LEVELS = new Set(["INFO", "WARN", "ERROR"]);
|
|
57
|
+
|
|
58
|
+
function fmtTs(d = new Date()) {
|
|
59
|
+
const pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
60
|
+
return (
|
|
61
|
+
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
62
|
+
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatLogLine(level, module, message, meta) {
|
|
67
|
+
const lvl = LEVELS.has(String(level || "").toUpperCase())
|
|
68
|
+
? String(level).toUpperCase()
|
|
69
|
+
: "INFO";
|
|
70
|
+
const mod = String(module || "apx").slice(0, 24);
|
|
71
|
+
const msg = String(message ?? "").replace(/\n/g, " ");
|
|
72
|
+
let suffix = "";
|
|
73
|
+
if (meta && typeof meta === "object" && Object.keys(meta).length > 0) {
|
|
74
|
+
try { suffix = " " + JSON.stringify(redact(meta)); }
|
|
75
|
+
catch { suffix = " {meta:unserializable}"; }
|
|
76
|
+
}
|
|
77
|
+
return `[${fmtTs()}] [${lvl.padEnd(5)}] [${mod}] ${msg}${suffix}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function log(level, module, message, meta) {
|
|
81
|
+
const line = formatLogLine(level, module, message, meta);
|
|
82
|
+
try {
|
|
83
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
84
|
+
fs.appendFileSync(APX_LOG_PATH, line + "\n", "utf8");
|
|
85
|
+
} catch {
|
|
86
|
+
// never throw from the logger — losing a log line must not crash the daemon
|
|
87
|
+
}
|
|
88
|
+
return line;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Convenience helpers so callers don't repeat the level string everywhere.
|
|
92
|
+
export const logInfo = (module, message, meta) => log("INFO", module, message, meta);
|
|
93
|
+
export const logWarn = (module, message, meta) => log("WARN", module, message, meta);
|
|
94
|
+
export const logError = (module, message, meta) => log("ERROR", module, message, meta);
|
|
95
|
+
|
|
96
|
+
// Build a module-bound logger so plugins can do `const log = loggerFor("telegram")`
|
|
97
|
+
// and then `log.info("...")` without repeating the module tag.
|
|
98
|
+
export function loggerFor(module) {
|
|
99
|
+
return {
|
|
100
|
+
info: (message, meta) => logInfo(module, message, meta),
|
|
101
|
+
warn: (message, meta) => logWarn(module, message, meta),
|
|
102
|
+
error: (message, meta) => logError(module, message, meta),
|
|
103
|
+
// Shorthand call form preserved for the daemon's old `log(msg)` callers:
|
|
104
|
+
// const log = loggerFor("daemon"); log("hello") // INFO
|
|
105
|
+
// We wrap it so `log` is callable directly *and* exposes .info/.warn/.error.
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Make a callable+method logger: `log("msg")` works AND `log.warn("msg")` works.
|
|
110
|
+
// This lets us replace the daemon's existing `log = (msg) => stdout.write(...)`
|
|
111
|
+
// with one that fans out to apx.log too, without rewriting every call site.
|
|
112
|
+
export function callableLogger(module) {
|
|
113
|
+
const fn = (message, meta) => logInfo(module, message, meta);
|
|
114
|
+
fn.info = (message, meta) => logInfo(module, message, meta);
|
|
115
|
+
fn.warn = (message, meta) => logWarn(module, message, meta);
|
|
116
|
+
fn.error = (message, meta) => logError(module, message, meta);
|
|
117
|
+
return fn;
|
|
118
|
+
}
|
package/src/daemon/api.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Express REST API for APX. See APC docs reference/apx-daemon.
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { randomUUID } from "node:crypto";
|
|
5
6
|
import { execFile } from "node:child_process";
|
|
@@ -1441,6 +1442,63 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
1441
1442
|
}
|
|
1442
1443
|
});
|
|
1443
1444
|
|
|
1445
|
+
// ---- Transcription chunk (shared: overlay, Telegram, any channel) -
|
|
1446
|
+
// POST /transcribe/chunk ← raw audio bytes (webm, ogg, wav, mp3 …)
|
|
1447
|
+
// Headers: X-Audio-Format, X-Language (ISO or "auto"), X-Provider (auto|local|openai)
|
|
1448
|
+
// Returns: { ok, text, backend, language, … }
|
|
1449
|
+
app.post("/transcribe/chunk", async (req, res) => {
|
|
1450
|
+
const chunks = [];
|
|
1451
|
+
req.on("data", (c) => chunks.push(c));
|
|
1452
|
+
req.on("end", async () => {
|
|
1453
|
+
const buf = Buffer.concat(chunks);
|
|
1454
|
+
if (!buf.length) return res.status(400).json({ ok: false, error: "empty body" });
|
|
1455
|
+
const format = req.headers["x-audio-format"] || "webm";
|
|
1456
|
+
const language = req.headers["x-language"] || "auto";
|
|
1457
|
+
const provider = req.headers["x-provider"];
|
|
1458
|
+
try {
|
|
1459
|
+
const { transcribeBuffer } = await import("./transcription.js");
|
|
1460
|
+
const result = await transcribeBuffer(buf, format, {
|
|
1461
|
+
language: language === "auto" ? undefined : language,
|
|
1462
|
+
beam_size: 3,
|
|
1463
|
+
...(provider ? { provider } : {}),
|
|
1464
|
+
});
|
|
1465
|
+
res.json(result);
|
|
1466
|
+
} catch (e) {
|
|
1467
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
req.on("error", (e) => res.status(500).json({ ok: false, error: e.message }));
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
// ---- Overlay channel (voice/floating window) ----------------------
|
|
1474
|
+
app.get("/overlay/status", (_req, res) => {
|
|
1475
|
+
// Lazy import to avoid hard dep
|
|
1476
|
+
import("./overlay-ws.js").then(({ overlayClients }) => {
|
|
1477
|
+
res.json({ ok: true, connected_clients: overlayClients.size });
|
|
1478
|
+
}).catch(() => res.json({ ok: true, connected_clients: 0 }));
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
// POST /overlay/message ← text sent by the overlay after transcription
|
|
1482
|
+
// Runs the super-agent and streams tokens back via WebSocket.
|
|
1483
|
+
app.post("/overlay/message", async (req, res) => {
|
|
1484
|
+
const { text, previousMessages = [] } = req.body || {};
|
|
1485
|
+
if (!text) return res.status(400).json({ error: "text required" });
|
|
1486
|
+
res.json({ ok: true }); // respond immediately; result comes via WebSocket
|
|
1487
|
+
|
|
1488
|
+
// Inline execution — the overlay plugin handles the heavy lift;
|
|
1489
|
+
// here we just trigger it if the plugin is registered.
|
|
1490
|
+
try {
|
|
1491
|
+
const overlayPlugin = plugins.instances.get("overlay");
|
|
1492
|
+
if (overlayPlugin?.handleMessage) {
|
|
1493
|
+
await overlayPlugin.handleMessage({ text, previousMessages });
|
|
1494
|
+
}
|
|
1495
|
+
} catch (e) {
|
|
1496
|
+
import("./overlay-ws.js").then(({ broadcastOverlay }) => {
|
|
1497
|
+
broadcastOverlay({ type: "error", message: e.message });
|
|
1498
|
+
}).catch(() => {});
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1444
1502
|
// ---- Admin --------------------------------------------------------
|
|
1445
1503
|
app.post("/admin/shutdown", (_req, res) => {
|
|
1446
1504
|
res.json({ ok: true });
|
|
@@ -11,7 +11,7 @@ function getKey(config) {
|
|
|
11
11
|
export default {
|
|
12
12
|
id: "anthropic",
|
|
13
13
|
|
|
14
|
-
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice, signal }) {
|
|
14
|
+
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice, signal, onToken }) {
|
|
15
15
|
const key = getKey(config);
|
|
16
16
|
if (!key) throw new Error("anthropic: no api_key (set ANTHROPIC_API_KEY or engines.anthropic.api_key)");
|
|
17
17
|
if (!model) throw new Error("anthropic: model required");
|
|
@@ -39,6 +39,65 @@ export default {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Streaming path — only when onToken provided AND no tool_choice=required
|
|
43
|
+
// (we can't stream tool-forced turns because tool_calls are embedded in SSE)
|
|
44
|
+
if (typeof onToken === "function" && toolChoice !== "required" && toolChoice !== "any") {
|
|
45
|
+
body.stream = true;
|
|
46
|
+
const res = await fetch(API_BASE, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
"content-type": "application/json",
|
|
50
|
+
"x-api-key": key,
|
|
51
|
+
"anthropic-version": API_VERSION,
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
signal,
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const err = await res.text().catch(() => "");
|
|
58
|
+
throw new Error(`anthropic ${res.status}: ${err.slice(0, 200)}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const decoder = new TextDecoder();
|
|
62
|
+
let text = "";
|
|
63
|
+
let inputTokens = 0;
|
|
64
|
+
let outputTokens = 0;
|
|
65
|
+
let stopReason = null;
|
|
66
|
+
let buf = "";
|
|
67
|
+
|
|
68
|
+
for await (const chunk of res.body) {
|
|
69
|
+
buf += decoder.decode(chunk, { stream: true });
|
|
70
|
+
const lines = buf.split("\n");
|
|
71
|
+
buf = lines.pop(); // keep incomplete last line
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
if (!line.startsWith("data: ")) continue;
|
|
74
|
+
const raw = line.slice(6).trim();
|
|
75
|
+
if (raw === "[DONE]") continue;
|
|
76
|
+
let evt;
|
|
77
|
+
try { evt = JSON.parse(raw); } catch { continue; }
|
|
78
|
+
if (evt.type === "content_block_delta" && evt.delta?.type === "text_delta") {
|
|
79
|
+
const t = evt.delta.text || "";
|
|
80
|
+
if (t) { text += t; onToken(t); }
|
|
81
|
+
} else if (evt.type === "message_delta") {
|
|
82
|
+
stopReason = evt.delta?.stop_reason || stopReason;
|
|
83
|
+
outputTokens = evt.usage?.output_tokens || outputTokens;
|
|
84
|
+
} else if (evt.type === "message_start") {
|
|
85
|
+
inputTokens = evt.message?.usage?.input_tokens || 0;
|
|
86
|
+
outputTokens = evt.message?.usage?.output_tokens || 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
text,
|
|
93
|
+
tool_uses: undefined,
|
|
94
|
+
stop_reason: stopReason,
|
|
95
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens },
|
|
96
|
+
raw: null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Non-streaming path (original)
|
|
42
101
|
const res = await fetch(API_BASE, {
|
|
43
102
|
method: "POST",
|
|
44
103
|
headers: {
|
|
@@ -46,7 +46,7 @@ export function getAdapter(provider) {
|
|
|
46
46
|
return a;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export async function callEngine({ modelId, system, messages, config, temperature, maxTokens, tools, toolChoice, signal }) {
|
|
49
|
+
export async function callEngine({ modelId, system, messages, config, temperature, maxTokens, tools, toolChoice, signal, onToken }) {
|
|
50
50
|
const { provider, model } = resolveProvider(modelId);
|
|
51
51
|
const adapter = getAdapter(provider);
|
|
52
52
|
const providerCfg = (config && config.engines && config.engines[provider]) || {};
|
|
@@ -60,6 +60,7 @@ export async function callEngine({ modelId, system, messages, config, temperatur
|
|
|
60
60
|
toolChoice,
|
|
61
61
|
config: providerCfg,
|
|
62
62
|
signal,
|
|
63
|
+
onToken,
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -8,15 +8,32 @@ function baseUrl(config) {
|
|
|
8
8
|
export default {
|
|
9
9
|
id: "ollama",
|
|
10
10
|
|
|
11
|
-
async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, tools, config = {}, signal }) {
|
|
11
|
+
async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, tools, toolChoice, config = {}, signal, onToken }) {
|
|
12
12
|
if (!model) throw new Error("ollama: model required");
|
|
13
13
|
|
|
14
|
+
// Ollama's /api/chat does not honor a tool_choice field. When the caller
|
|
15
|
+
// wants to force a tool call ("required" / "any") we inject a strong
|
|
16
|
+
// system-message hint instead so the model is much less likely to emit a
|
|
17
|
+
// text-only acknowledgement like "ok dame un minuto" without calling a tool.
|
|
18
|
+
const forceTool =
|
|
19
|
+
Array.isArray(tools) && tools.length > 0 &&
|
|
20
|
+
(toolChoice === "required" || toolChoice === "any");
|
|
21
|
+
|
|
22
|
+
let effectiveSystem = system;
|
|
23
|
+
if (forceTool) {
|
|
24
|
+
const hint =
|
|
25
|
+
"You MUST call one of the available tools to satisfy this turn. " +
|
|
26
|
+
"Do NOT reply with text-only acknowledgements (no 'ok', 'sure', 'on it', 'dame un minuto'). " +
|
|
27
|
+
"If you cannot decide which tool, pick the closest match and call it.";
|
|
28
|
+
effectiveSystem = system ? `${system}\n\n${hint}` : hint;
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
// The caller can pass `messages` as either:
|
|
15
32
|
// [{role, content}] — usual shape
|
|
16
33
|
// [{role, content, tool_calls?}, {role: "tool", tool_call_id?, content}, ...]
|
|
17
34
|
// We forward those fields straight through so the agent loop works.
|
|
18
35
|
const fullMessages = [];
|
|
19
|
-
if (
|
|
36
|
+
if (effectiveSystem) fullMessages.push({ role: "system", content: effectiveSystem });
|
|
20
37
|
for (const m of messages) {
|
|
21
38
|
const out = { role: m.role };
|
|
22
39
|
if (m.content !== undefined) {
|
|
@@ -30,6 +47,57 @@ export default {
|
|
|
30
47
|
fullMessages.push(out);
|
|
31
48
|
}
|
|
32
49
|
|
|
50
|
+
const url = `${baseUrl(config).replace(/\/$/, "")}/api/chat`;
|
|
51
|
+
|
|
52
|
+
// Streaming path — only when onToken provided AND no tools (Ollama streaming + tools is unreliable)
|
|
53
|
+
if (typeof onToken === "function" && (!tools || tools.length === 0)) {
|
|
54
|
+
const body = {
|
|
55
|
+
model,
|
|
56
|
+
messages: fullMessages,
|
|
57
|
+
stream: true,
|
|
58
|
+
options: { temperature, num_predict: maxTokens },
|
|
59
|
+
};
|
|
60
|
+
const res = await fetch(url, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "content-type": "application/json" },
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
signal,
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const t = await res.text();
|
|
68
|
+
throw new Error(`ollama ${res.status}: ${t}`);
|
|
69
|
+
}
|
|
70
|
+
const decoder = new TextDecoder();
|
|
71
|
+
let text = "";
|
|
72
|
+
let inputTokens = 0;
|
|
73
|
+
let outputTokens = 0;
|
|
74
|
+
let buf = "";
|
|
75
|
+
for await (const chunk of res.body) {
|
|
76
|
+
buf += decoder.decode(chunk, { stream: true });
|
|
77
|
+
const lines = buf.split("\n");
|
|
78
|
+
buf = lines.pop();
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (!line.trim()) continue;
|
|
81
|
+
let evt;
|
|
82
|
+
try { evt = JSON.parse(line); } catch { continue; }
|
|
83
|
+
const t = evt.message?.content || "";
|
|
84
|
+
if (t) { text += t; onToken(t); }
|
|
85
|
+
if (evt.done) {
|
|
86
|
+
inputTokens = evt.prompt_eval_count || 0;
|
|
87
|
+
outputTokens = evt.eval_count || 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
text,
|
|
93
|
+
tool_calls: null,
|
|
94
|
+
message: { role: "assistant", content: text },
|
|
95
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens },
|
|
96
|
+
raw: null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Non-streaming path (original)
|
|
33
101
|
const body = {
|
|
34
102
|
model,
|
|
35
103
|
messages: fullMessages,
|
|
@@ -40,7 +108,6 @@ export default {
|
|
|
40
108
|
body.tools = tools;
|
|
41
109
|
}
|
|
42
110
|
|
|
43
|
-
const url = `${baseUrl(config).replace(/\/$/, "")}/api/chat`;
|
|
44
111
|
const res = await fetch(url, {
|
|
45
112
|
method: "POST",
|
|
46
113
|
headers: { "content-type": "application/json" },
|
package/src/daemon/index.js
CHANGED
|
@@ -21,6 +21,8 @@ import { PluginManager } from "./plugins/index.js";
|
|
|
21
21
|
import { RoutineScheduler } from "./routines.js";
|
|
22
22
|
import { buildApi } from "./api.js";
|
|
23
23
|
import { triggerWakeup } from "./wakeup.js";
|
|
24
|
+
import { registerOverlayClient } from "./overlay-ws.js";
|
|
25
|
+
import { log as logToUnified } from "../core/logging.js";
|
|
24
26
|
|
|
25
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
28
|
const __dirname = path.dirname(__filename);
|
|
@@ -32,8 +34,41 @@ const PKG = JSON.parse(
|
|
|
32
34
|
// to ~/.apx/daemon.log via `stdio: ["ignore", out, out]`. So a single
|
|
33
35
|
// process.stdout.write reaches the file once. In foreground (npm start), it
|
|
34
36
|
// still prints to the console. No double-append.
|
|
37
|
+
//
|
|
38
|
+
// Beyond the legacy stdout sink we also fan out every line to the unified
|
|
39
|
+
// ~/.apx/logs/apx.log via core/logging.js so `apx log` and `apx log -f`
|
|
40
|
+
// see everything that any plugin/module writes through the daemon's log fn.
|
|
41
|
+
//
|
|
42
|
+
// Heuristic for level/module inference: messages prefixed "fatal:" /
|
|
43
|
+
// "uncaughtException:" / "error:" are ERROR; "warn:" / "could not" / "skipping"
|
|
44
|
+
// are WARN; everything else INFO. Plugins normally pass `plugin <id> ...` or
|
|
45
|
+
// `<id>[<name>] ...` — we use the first bracketed token (or first word before
|
|
46
|
+
// ":") as the module tag.
|
|
47
|
+
function inferLevel(msg) {
|
|
48
|
+
if (/^fatal:|^uncaughtException:|^error:|failed|crash/i.test(msg)) return "ERROR";
|
|
49
|
+
if (/^warn:|could not|skipping|orphan|broken pipe/i.test(msg)) return "WARN";
|
|
50
|
+
return "INFO";
|
|
51
|
+
}
|
|
52
|
+
function inferModule(msg) {
|
|
53
|
+
// "plugin telegram initialized" → telegram
|
|
54
|
+
const plug = msg.match(/^plugin\s+([a-z_-]+)/i);
|
|
55
|
+
if (plug) return plug[1];
|
|
56
|
+
// "telegram[default] ..." → telegram
|
|
57
|
+
const bracket = msg.match(/^([a-z_-]+)\[/i);
|
|
58
|
+
if (bracket) return bracket[1];
|
|
59
|
+
// "whisper: preloading ..." → whisper
|
|
60
|
+
const colon = msg.match(/^([a-z_-]+):\s/i);
|
|
61
|
+
if (colon) return colon[1];
|
|
62
|
+
// "overlay: ..." caught above; "loaded project ..." → daemon
|
|
63
|
+
return "daemon";
|
|
64
|
+
}
|
|
35
65
|
const log = (msg) => {
|
|
36
66
|
process.stdout.write(`[${new Date().toISOString()}] ${msg}\n`);
|
|
67
|
+
try {
|
|
68
|
+
logToUnified(inferLevel(msg), inferModule(msg), msg);
|
|
69
|
+
} catch {
|
|
70
|
+
// logger is best-effort, never throw
|
|
71
|
+
}
|
|
37
72
|
};
|
|
38
73
|
|
|
39
74
|
function ensureHome() {
|
|
@@ -170,6 +205,25 @@ async function main() {
|
|
|
170
205
|
scheduler.start();
|
|
171
206
|
// Fire wake-up message after a short delay so plugins (Telegram) are ready
|
|
172
207
|
setTimeout(() => triggerWakeup(cfg, log), 3000);
|
|
208
|
+
// Preload whisper-server in the background so first overlay transcription is fast.
|
|
209
|
+
// Adopts an existing one if already on the port; otherwise spawns fresh.
|
|
210
|
+
import("./transcription.js").then(({ preloadWhisperServer }) => {
|
|
211
|
+
preloadWhisperServer((m) => log(m));
|
|
212
|
+
}).catch(() => {});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Attach WebSocket upgrade for overlay channel on /overlay/ws
|
|
216
|
+
server.on("upgrade", async (req, socket, head) => {
|
|
217
|
+
if (req.url !== "/overlay/ws") { socket.destroy(); return; }
|
|
218
|
+
// Lazy-import ws to avoid hard dep on startup
|
|
219
|
+
let WebSocketServer;
|
|
220
|
+
try { ({ WebSocketServer } = await import("ws")); } catch {
|
|
221
|
+
socket.destroy(); return;
|
|
222
|
+
}
|
|
223
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
224
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
225
|
+
registerOverlayClient(ws);
|
|
226
|
+
});
|
|
173
227
|
});
|
|
174
228
|
|
|
175
229
|
server.on("error", (e) => {
|
|
@@ -185,6 +239,10 @@ async function main() {
|
|
|
185
239
|
scheduler.stop();
|
|
186
240
|
plugins.stopAll();
|
|
187
241
|
registries.shutdown();
|
|
242
|
+
// Best-effort shutdown of whisper-server subprocess.
|
|
243
|
+
import("./transcription.js").then(({ shutdownWhisperServer }) => {
|
|
244
|
+
shutdownWhisperServer().catch(() => {});
|
|
245
|
+
}).catch(() => {});
|
|
188
246
|
server.close(() => {
|
|
189
247
|
clearPid();
|
|
190
248
|
process.exit(0);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Singleton WebSocket hub for the overlay channel.
|
|
2
|
+
// Imported by api.js (to register connections) and by plugins/overlay.js (to broadcast).
|
|
3
|
+
|
|
4
|
+
const _clients = new Set(); // Set<WebSocket>
|
|
5
|
+
let _messageHandler = null; // (ws, data) => void — set by overlay plugin
|
|
6
|
+
|
|
7
|
+
export const overlayClients = _clients;
|
|
8
|
+
|
|
9
|
+
export function setOverlayMessageHandler(fn) {
|
|
10
|
+
_messageHandler = fn;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function registerOverlayClient(ws) {
|
|
14
|
+
_clients.add(ws);
|
|
15
|
+
ws.on("close", () => _clients.delete(ws));
|
|
16
|
+
ws.on("error", () => _clients.delete(ws));
|
|
17
|
+
ws.on("message", (raw) => {
|
|
18
|
+
if (typeof _messageHandler === "function") {
|
|
19
|
+
let data;
|
|
20
|
+
try { data = JSON.parse(raw.toString()); } catch { data = { type: "raw", raw: raw.toString() }; }
|
|
21
|
+
_messageHandler(ws, data);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function broadcastOverlay(msg) {
|
|
27
|
+
const payload = typeof msg === "string" ? msg : JSON.stringify(msg);
|
|
28
|
+
for (const ws of _clients) {
|
|
29
|
+
try {
|
|
30
|
+
if (ws.readyState === 1) ws.send(payload); // 1 = OPEN
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function sendToClient(ws, msg) {
|
|
36
|
+
const payload = typeof msg === "string" ? msg : JSON.stringify(msg);
|
|
37
|
+
try {
|
|
38
|
+
if (ws.readyState === 1) ws.send(payload);
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
// Plugins are discovered by static import here. Adding a new plugin = importing
|
|
14
14
|
// it and pushing into PLUGINS.
|
|
15
15
|
import telegramPlugin from "./telegram.js";
|
|
16
|
+
import overlayPlugin from "./overlay.js";
|
|
16
17
|
|
|
17
|
-
export const PLUGINS = [telegramPlugin];
|
|
18
|
+
export const PLUGINS = [telegramPlugin, overlayPlugin];
|
|
18
19
|
|
|
19
20
|
export class PluginManager {
|
|
20
21
|
constructor({ projects, config, log, registries }) {
|