@agentprojectcontext/apx 1.15.6 → 1.17.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 +46 -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-langchain.js +296 -0
- package/src/daemon/super-agent.js +115 -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,61 @@
|
|
|
1
|
+
import type { CommandModule, ArgumentsCamelCase } from "yargs";
|
|
2
|
+
import { getHttp } from "../http.js";
|
|
3
|
+
import { println, error, success, dim, bold, highlight } from "../ui.js";
|
|
4
|
+
|
|
5
|
+
export const statusCmd: CommandModule = {
|
|
6
|
+
command: "status",
|
|
7
|
+
describe: "Show APX system health and project overview",
|
|
8
|
+
handler: async (args: ArgumentsCamelCase<{ project?: string }>) => {
|
|
9
|
+
try {
|
|
10
|
+
const http = await getHttp();
|
|
11
|
+
|
|
12
|
+
// Daemon health
|
|
13
|
+
let daemonOk = false;
|
|
14
|
+
try {
|
|
15
|
+
const health = (await http.get("/health")) as { version?: string; uptime_s?: number };
|
|
16
|
+
println(
|
|
17
|
+
"\x1b[92m●\x1b[0m Daemon " +
|
|
18
|
+
dim(`v${health.version ?? "?"}`) +
|
|
19
|
+
" uptime " + dim(`${health.uptime_s ?? "?"}s`),
|
|
20
|
+
);
|
|
21
|
+
daemonOk = true;
|
|
22
|
+
} catch {
|
|
23
|
+
println("\x1b[91m✗\x1b[0m Daemon " + dim("not running — run: apx daemon start"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!daemonOk) { process.exit(1); return; }
|
|
27
|
+
|
|
28
|
+
// Projects
|
|
29
|
+
const projects = (await http.get("/projects")) as Array<{
|
|
30
|
+
id: string; name: string; path: string;
|
|
31
|
+
}>;
|
|
32
|
+
println("");
|
|
33
|
+
println(bold("Projects") + " " + dim(`(${projects?.length ?? 0})`));
|
|
34
|
+
if (!projects?.length) {
|
|
35
|
+
println(dim(" No projects. Run: apx init <path>"));
|
|
36
|
+
} else {
|
|
37
|
+
for (const p of projects) {
|
|
38
|
+
const isActive = !args.project || p.name === args.project || p.id === args.project;
|
|
39
|
+
println(
|
|
40
|
+
(isActive ? "\x1b[94m▶\x1b[0m" : " ") +
|
|
41
|
+
" " + highlight(p.name ?? p.id) +
|
|
42
|
+
" " + dim(p.path),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Engines
|
|
48
|
+
try {
|
|
49
|
+
const eng = (await http.get("/engines")) as { engines: string[] };
|
|
50
|
+
if (eng?.engines?.length) {
|
|
51
|
+
println("");
|
|
52
|
+
println(bold("Engines") + " " + dim(`(${eng.engines.length})`));
|
|
53
|
+
println(dim(" " + eng.engines.slice(0, 6).join(" ") + (eng.engines.length > 6 ? " …" : "")));
|
|
54
|
+
}
|
|
55
|
+
} catch { /* engines endpoint optional */ }
|
|
56
|
+
} catch (err: unknown) {
|
|
57
|
+
error(err instanceof Error ? err.message : String(err));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Typed wrapper around the APX HTTP client (src/cli/http.js).
|
|
2
|
+
// We use dynamic import since the base client is plain JS ESM.
|
|
3
|
+
|
|
4
|
+
export interface StreamEvent {
|
|
5
|
+
type: "event" | "chunk" | "final" | "error";
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type HttpModule = {
|
|
10
|
+
get(path: string, opts?: RequestInit): Promise<unknown>;
|
|
11
|
+
post(path: string, body: unknown, opts?: RequestInit): Promise<unknown>;
|
|
12
|
+
put(path: string, body: unknown, opts?: RequestInit): Promise<unknown>;
|
|
13
|
+
patch(path: string, body: unknown, opts?: RequestInit): Promise<unknown>;
|
|
14
|
+
delete(path: string, opts?: RequestInit): Promise<unknown>;
|
|
15
|
+
streamPost(
|
|
16
|
+
path: string,
|
|
17
|
+
body: unknown,
|
|
18
|
+
onEvent: (ev: StreamEvent) => void,
|
|
19
|
+
opts?: RequestInit,
|
|
20
|
+
): Promise<unknown>;
|
|
21
|
+
baseUrl(): string;
|
|
22
|
+
ping(timeoutMs?: number): Promise<boolean>;
|
|
23
|
+
createAbortController(): AbortController;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let _http: HttpModule | null = null;
|
|
27
|
+
|
|
28
|
+
export async function getHttp(): Promise<HttpModule> {
|
|
29
|
+
if (!_http) {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
31
|
+
// @ts-ignore – plain JS outside rootDir; HttpModule above provides typing
|
|
32
|
+
const m = (await import("../cli/http.js")) as { http: HttpModule };
|
|
33
|
+
_http = m.http;
|
|
34
|
+
}
|
|
35
|
+
return _http;
|
|
36
|
+
}
|
|
@@ -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" },
|