@agentprojectcontext/apx 1.33.0 → 1.34.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 +1 -1
- package/skills/apc-context/SKILL.md +2 -5
- package/skills/apx/SKILL.md +49 -61
- package/src/core/agent/a2a/reply.js +48 -0
- package/src/core/agent/build-agent-system.js +4 -3
- package/src/core/agent/channels/voice-context.js +98 -0
- package/src/core/agent/memory.js +2 -1
- package/src/core/agent/prompt-builder.js +2 -1
- package/src/core/agent/prompts/modes/code-build.md +1 -0
- package/src/core/agent/prompts/modes/code-plan.md +1 -0
- package/src/core/agent/prompts/modes/index.js +28 -0
- package/src/core/agent/skills/loader.js +22 -18
- package/src/core/agent/stream/turn-accumulator.js +73 -0
- package/src/core/agent/suggestions.js +37 -0
- package/src/core/agent/tools/handlers/add-project.js +5 -2
- package/src/core/agent/tools/handlers/call-runtime.js +3 -2
- package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
- package/src/core/agent/tools/helpers.js +2 -2
- package/src/core/agent/tools/names.js +138 -0
- package/src/core/agent/tools/registry-bridge.js +6 -14
- package/src/core/agent/tools/registry.js +68 -65
- package/src/core/apc/context-copy.js +27 -0
- package/src/core/apc/notes.js +19 -0
- package/src/core/apc/parser.js +13 -6
- package/src/core/apc/paths.js +87 -0
- package/src/core/apc/scaffold.js +82 -74
- package/src/core/apc/skill-sync.js +13 -1
- package/src/core/channels/telegram/dispatch.js +595 -0
- package/src/core/channels/telegram/helpers.js +130 -0
- package/src/core/config/index.js +3 -2
- package/src/core/config/redact.js +95 -0
- package/src/core/constants/channels.js +2 -0
- package/src/core/constants/code-modes.js +10 -0
- package/src/core/constants/index.js +1 -0
- package/src/core/deck/manifest.js +186 -0
- package/src/core/engines/catalog.js +83 -0
- package/src/core/engines/gemini.js +28 -11
- package/src/core/engines/index.js +11 -1
- package/src/core/{tools → http-tools}/browser.js +0 -1
- package/src/core/{tools → http-tools}/fetch.js +0 -1
- package/src/core/{tools → http-tools}/glob.js +0 -1
- package/src/core/{tools → http-tools}/grep.js +0 -1
- package/src/core/{tools → http-tools}/registry.js +0 -1
- package/src/core/{tools → http-tools}/search.js +0 -1
- package/src/core/i18n/en.js +9 -0
- package/src/core/i18n/es.js +12 -0
- package/src/core/i18n/index.js +54 -0
- package/src/core/i18n/pt.js +9 -0
- package/src/core/identity/telegram.js +2 -1
- package/src/core/mcp/runner.js +272 -14
- package/src/core/mcp/sources.js +3 -2
- package/src/core/routines/index.js +16 -0
- package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
- package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
- package/src/core/runtime-skills/apx/SKILL.md +95 -0
- package/src/core/runtime-skills/apx-mcp/SKILL.md +116 -0
- package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
- package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
- package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
- package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
- package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
- package/src/core/stores/code-sessions.js +50 -2
- package/src/core/stores/routine-memory.js +1 -1
- package/src/core/stores/sessions-search.js +121 -0
- package/src/core/stores/sessions.js +38 -0
- package/src/core/vars/index.js +14 -0
- package/src/core/vars/interpolate.js +86 -0
- package/src/core/vars/sources.js +151 -0
- package/src/core/voice/audio-decode.js +38 -0
- package/src/core/voice/transcription.js +225 -0
- package/src/host/daemon/api/admin-config.js +5 -82
- package/src/host/daemon/api/agents.js +5 -5
- package/src/host/daemon/api/code.js +17 -169
- package/src/host/daemon/api/config.js +3 -4
- package/src/host/daemon/api/conversations.js +8 -29
- package/src/host/daemon/api/deck.js +37 -404
- package/src/host/daemon/api/engines.js +1 -50
- package/src/host/daemon/api/exec.js +1 -1
- package/src/host/daemon/api/mcps.js +32 -0
- package/src/host/daemon/api/routines.js +1 -1
- package/src/host/daemon/api/runtimes.js +4 -3
- package/src/host/daemon/api/sessions-search.js +24 -140
- package/src/host/daemon/api/sessions.js +12 -30
- package/src/host/daemon/api/shared.js +2 -1
- package/src/host/daemon/api/telegram.js +1 -11
- package/src/host/daemon/api/tools.js +6 -6
- package/src/host/daemon/api/transcribe.js +2 -2
- package/src/host/daemon/api/vars.js +137 -0
- package/src/host/daemon/api/voice.js +13 -290
- package/src/host/daemon/api.js +2 -0
- package/src/host/daemon/db.js +6 -6
- package/src/host/daemon/deck-exec.js +148 -0
- package/src/host/daemon/index.js +3 -3
- package/src/host/daemon/plugins/telegram/index.js +24 -687
- package/src/host/daemon/routines-scheduler.js +64 -0
- package/src/host/daemon/smoke.js +3 -2
- package/src/host/daemon/whisper-server.js +225 -0
- package/src/interfaces/cli/commands/agent.js +3 -2
- package/src/interfaces/cli/commands/command.js +2 -3
- package/src/interfaces/cli/commands/messages.js +6 -2
- package/src/interfaces/cli/commands/pair.js +5 -4
- package/src/interfaces/cli/commands/search.js +1 -1
- package/src/interfaces/cli/commands/sessions.js +3 -2
- package/src/interfaces/cli/commands/skills.js +36 -55
- package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +1 -0
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js +613 -0
- package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +182 -182
- package/src/interfaces/web/src/components/ModelCombobox.tsx +44 -8
- package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
- package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
- package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
- package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
- package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
- package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
- package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
- package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
- package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
- package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
- package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
- package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
- package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
- package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +5 -4
- package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
- package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
- package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
- package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
- package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
- package/src/interfaces/web/src/constants/index.ts +1 -1
- package/src/interfaces/web/src/i18n/en.ts +174 -7
- package/src/interfaces/web/src/i18n/es.ts +179 -15
- package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
- package/src/interfaces/web/src/lib/api/vars.ts +38 -0
- package/src/interfaces/web/src/lib/api.ts +1 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
- package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
- package/src/interfaces/web/src/types/daemon.ts +5 -0
- package/src/host/daemon/transcription.js +0 -538
- package/src/host/daemon/whisper-transcribe.py +0 -73
- package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +0 -1
- package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +0 -602
- package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js.map +0 -1
- /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
- /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
- /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
- /package/src/core/{tools → http-tools}/index.js +0 -0
- /package/{skills → src/core/runtime-skills}/apx-agency-agents/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-agent/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-mcp-builder/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-routine/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-runtime/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-sessions/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-skill-builder/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-telegram/SKILL.md +0 -0
- /package/{skills → src/core/runtime-skills}/apx-voice/SKILL.md +0 -0
- /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
- /package/src/{host/daemon → core/stores}/conversations.js +0 -0
- /package/src/{host/daemon → core/util}/thinking.js +0 -0
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// eslint-disable-next-line -- import below
|
|
2
|
-
import { CHANNELS } from "#core/constants/channels.js";
|
|
3
1
|
// Daemon HTTP routes for the unified "voice" channel.
|
|
4
2
|
//
|
|
5
3
|
// POST /voice/turn { audio?: <base64 or path>, format?, text?, agent?,
|
|
@@ -17,268 +15,21 @@ import { CHANNELS } from "#core/constants/channels.js";
|
|
|
17
15
|
//
|
|
18
16
|
// Channel/agent surfaces (Telegram plugin, overlay) can keep their own
|
|
19
17
|
// pipelines, but they should delegate here when they want a hablada reply.
|
|
18
|
+
//
|
|
19
|
+
// Domain logic (channel context, suggestion parsing, audio decoding) lives in
|
|
20
|
+
// core/. This file is just glue: parse request → call core → format response.
|
|
20
21
|
import fs from "node:fs";
|
|
21
22
|
import path from "node:path";
|
|
22
|
-
import os from "node:os";
|
|
23
|
-
import { randomUUID } from "node:crypto";
|
|
24
23
|
import { readConfig } from "#core/config/index.js";
|
|
25
24
|
import { synthesize } from "#core/voice/tts.js";
|
|
26
|
-
import { transcribe } from "
|
|
25
|
+
import { transcribe } from "#core/voice/transcription.js";
|
|
26
|
+
import { decodeAudioInput } from "#core/voice/audio-decode.js";
|
|
27
27
|
import { runSuperAgent, isSuperAgentEnabled } from "#core/agent/super-agent.js";
|
|
28
|
+
import { buildVoiceChannelContext } from "#core/agent/channels/voice-context.js";
|
|
29
|
+
import { extractSuggestions } from "#core/agent/suggestions.js";
|
|
28
30
|
import { appendGlobalMessage } from "#core/stores/messages.js";
|
|
29
31
|
import { appendErrorTrace, previewText } from "#core/logging.js";
|
|
30
32
|
|
|
31
|
-
// ── Channel-aware pre-processor ────────────────────────────────────
|
|
32
|
-
//
|
|
33
|
-
// Each surface that talks to the super-agent (voice overlay on the
|
|
34
|
-
// deck, deck buttons, telegram, raw API) has different ergonomics:
|
|
35
|
-
// what the response will look like, how long it can be, whether the
|
|
36
|
-
// UI can render structured suggestions. `buildChannelContext` is the
|
|
37
|
-
// single place where those decisions live — voice.js passes the
|
|
38
|
-
// channel string from the request body and gets back the context
|
|
39
|
-
// note + system suffix to feed into the super-agent.
|
|
40
|
-
//
|
|
41
|
-
// The shape is intentionally tiny: contextNote becomes the
|
|
42
|
-
// `contextNote` field on the super-agent call (gets prepended to the
|
|
43
|
-
// prompt), systemSuffix is concatenated onto the system prompt to
|
|
44
|
-
// teach the model surface-specific output rules (e.g. trailing
|
|
45
|
-
// ```suggestions JSON block``` on voice/deck).
|
|
46
|
-
function buildChannelContext(channel, { projectId, language = "es" } = {}) {
|
|
47
|
-
const base = {
|
|
48
|
-
contextNote: "",
|
|
49
|
-
systemSuffix: "",
|
|
50
|
-
wantsSuggestions: false,
|
|
51
|
-
// Channel id forwarded to runSuperAgent so the matching channels/*.md
|
|
52
|
-
// formatting block is injected.
|
|
53
|
-
channel: "",
|
|
54
|
-
// Forwarded as channelMeta; { voice: true } flags spoken mode.
|
|
55
|
-
channelMeta: {},
|
|
56
|
-
};
|
|
57
|
-
// Project resolution hint.
|
|
58
|
-
// - per-project mic (projectId set): use it imperatively, don't ask.
|
|
59
|
-
// - global deck mic (no projectId): default to project id=0
|
|
60
|
-
// ("default") for actions unless the user names a project out
|
|
61
|
-
// loud. Either way, don't pester the user with "¿en qué
|
|
62
|
-
// proyecto?" — pick a sensible default and act.
|
|
63
|
-
const projectHint = projectId
|
|
64
|
-
? `\nThe active project is id=${projectId}. For ANY task/note/list ` +
|
|
65
|
-
`action, pass project_id=${projectId} automatically. Do NOT ask the ` +
|
|
66
|
-
`user which project — only switch if they explicitly name another.`
|
|
67
|
-
: `\nThis is the GLOBAL mic (no project in focus). For task/note/list ` +
|
|
68
|
-
`actions, default to project_id=0 ("default") UNLESS the user names ` +
|
|
69
|
-
`a project out loud (e.g. "en evolution-registry…", "en el proyecto ` +
|
|
70
|
-
`apx…") — then resolve that project by name. Never ask "¿en qué ` +
|
|
71
|
-
`proyecto?"; pick the default and act.`;
|
|
72
|
-
// Hard language directive — without this the model defaults to its
|
|
73
|
-
// training-bias English on short Spanish prompts, especially when
|
|
74
|
-
// the user mixes English-ish product names ("aicrm").
|
|
75
|
-
const langDirective = language === "es"
|
|
76
|
-
? "IMPORTANT: Reply ALWAYS in Spanish (rioplatense/Argentina). The user speaks Spanish."
|
|
77
|
-
: `IMPORTANT: Reply in language "${language}".`;
|
|
78
|
-
|
|
79
|
-
// Surface mapping. Channels are surfaces; "voice" is NOT a surface — it's the
|
|
80
|
-
// spoken MODE of the deck. All channel FORMATTING now lives in the
|
|
81
|
-
// channels/*.md + modes/voice.md blocks (injected by buildSuperAgentSystem);
|
|
82
|
-
// contextNote here carries ONLY per-request dynamic bits (language + project).
|
|
83
|
-
// incoming "voice" → deck surface, spoken
|
|
84
|
-
// incoming "deck" → deck surface, text card
|
|
85
|
-
// incoming "desktop" → desktop module, spoken (voice-first)
|
|
86
|
-
const dynamicNote = `${langDirective}${projectHint}`;
|
|
87
|
-
switch (channel) {
|
|
88
|
-
case "voice":
|
|
89
|
-
return { ...base, contextNote: dynamicNote, systemSuffix: SUGGESTIONS_INSTRUCTION, wantsSuggestions: true, channel: CHANNELS.DECK, channelMeta: { voice: true } };
|
|
90
|
-
case "deck":
|
|
91
|
-
return { ...base, contextNote: dynamicNote, systemSuffix: SUGGESTIONS_INSTRUCTION, wantsSuggestions: true, channel: CHANNELS.DECK, channelMeta: {} };
|
|
92
|
-
case "desktop":
|
|
93
|
-
return { ...base, contextNote: dynamicNote, systemSuffix: SUGGESTIONS_INSTRUCTION, wantsSuggestions: true, channel: CHANNELS.DESKTOP, channelMeta: { voice: true } };
|
|
94
|
-
case "telegram":
|
|
95
|
-
// Format rules live in channels/telegram.md; keep only the dynamic note.
|
|
96
|
-
return { ...base, contextNote: dynamicNote, channel: CHANNELS.TELEGRAM, channelMeta: {} };
|
|
97
|
-
default:
|
|
98
|
-
return { ...base, contextNote: dynamicNote, channel: channel || "api", channelMeta: {} };
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Balanced suffix. An earlier, more aggressive version ("EJECUTA, no
|
|
103
|
-
// narres — LLAMÁ A LA TOOL") made Gemini call tools for EVERYTHING,
|
|
104
|
-
// even "hola" → it fired send_telegram("hola"). The rule below gates
|
|
105
|
-
// tool use on a *clear* action request and explicitly tells the model
|
|
106
|
-
// to just talk for chit-chat.
|
|
107
|
-
const SUGGESTIONS_INSTRUCTION = `
|
|
108
|
-
|
|
109
|
-
# Cuándo usar tools
|
|
110
|
-
SOLO llamá una tool cuando el usuario pide CLARAMENTE una acción
|
|
111
|
-
concreta: "creá una tarea …", "mandá un telegram …", "listá …",
|
|
112
|
-
"abrí …", "marcá como hecha …". En esos casos ejecutá la tool (no
|
|
113
|
-
digas "lo voy a hacer" — hacelo) y después confirmá en una frase corta
|
|
114
|
-
en castellano lo que YA hiciste.
|
|
115
|
-
|
|
116
|
-
Si el mensaje es un saludo, una pregunta, o charla ("hola", "cómo
|
|
117
|
-
andás", "qué podés hacer") NO llames ninguna tool: respondé en texto,
|
|
118
|
-
breve, en castellano.
|
|
119
|
-
|
|
120
|
-
Nunca llames la misma tool dos veces en el mismo turno.
|
|
121
|
-
|
|
122
|
-
# Sugerencias (opcional)
|
|
123
|
-
Al final, en su propia línea, podés agregar un bloque fenced
|
|
124
|
-
\`suggestions\` con 2-3 próximos pasos. El usuario NO lo ve (la deck lo
|
|
125
|
-
quita):
|
|
126
|
-
\`\`\`suggestions
|
|
127
|
-
[{"label":"Ver tareas","command":"deck.view:tasks"}]
|
|
128
|
-
\`\`\`
|
|
129
|
-
Si no hay próximos pasos útiles, omití el bloque.`;
|
|
130
|
-
|
|
131
|
-
// Pull the trailing ```suggestions ... ``` block off the agent's
|
|
132
|
-
// reply. Returns { cleanText, suggestions[] } — cleanText is the
|
|
133
|
-
// reply with the block removed so the user (and TTS) never sees it.
|
|
134
|
-
const SUGGESTIONS_BLOCK_RE = /\n*```\s*suggestions\s*\n([\s\S]*?)\n?```\s*$/i;
|
|
135
|
-
|
|
136
|
-
function extractSuggestions(text) {
|
|
137
|
-
if (typeof text !== "string" || !text) return { cleanText: text || "", suggestions: [] };
|
|
138
|
-
const m = SUGGESTIONS_BLOCK_RE.exec(text);
|
|
139
|
-
if (!m) return { cleanText: text, suggestions: [] };
|
|
140
|
-
const cleanText = text.slice(0, m.index).trim();
|
|
141
|
-
let suggestions = [];
|
|
142
|
-
try {
|
|
143
|
-
const parsed = JSON.parse(m[1]);
|
|
144
|
-
if (Array.isArray(parsed)) {
|
|
145
|
-
suggestions = parsed
|
|
146
|
-
.filter((s) => s && typeof s === "object" && typeof s.label === "string")
|
|
147
|
-
.slice(0, 4)
|
|
148
|
-
.map((s) => ({
|
|
149
|
-
label: String(s.label).slice(0, 48),
|
|
150
|
-
...(typeof s.command === "string" ? { command: s.command.slice(0, 96) } : {}),
|
|
151
|
-
}));
|
|
152
|
-
}
|
|
153
|
-
} catch {
|
|
154
|
-
// Malformed JSON — drop suggestions silently rather than fail the
|
|
155
|
-
// turn. Better UX to show the reply without chips than an error.
|
|
156
|
-
}
|
|
157
|
-
return { cleanText, suggestions };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ── Voice intent classifier ────────────────────────────────────────
|
|
161
|
-
//
|
|
162
|
-
// A very small, regex-based router that catches a handful of "verb-y"
|
|
163
|
-
// utterances we want to short-circuit instead of sending to the LLM.
|
|
164
|
-
// Right now: "crear tarea ...", "creá tarea ...", "agregá una tarea ...",
|
|
165
|
-
// "nueva tarea ..." → POST to the project's tasks store directly.
|
|
166
|
-
//
|
|
167
|
-
// The classifier always returns either:
|
|
168
|
-
// - { handled: false } → caller falls through to the super-agent
|
|
169
|
-
// - { handled: true, reply: "...", meta?: {...} } → caller returns
|
|
170
|
-
//
|
|
171
|
-
// Keeping it dependency-free + sync lets us run it before any heavy
|
|
172
|
-
// work in the voice handler.
|
|
173
|
-
const TASK_INTENT_RE = new RegExp(
|
|
174
|
-
// Optional polite preamble: "podés / por favor"
|
|
175
|
-
"^\\s*(?:por favor|porfa|porfis|dale|che|apx)?[,!\\s]*" +
|
|
176
|
-
// Either:
|
|
177
|
-
// (a) verb cluster + optional article + "tarea"
|
|
178
|
-
// (b) standalone "nueva|otra tarea" (no verb)
|
|
179
|
-
"(?:" +
|
|
180
|
-
// (a) verbs — with optional clitic pronouns (-me, -te, -le)
|
|
181
|
-
"(?:crea[r]?|cre[áa]|agreg[áa](?:me|le)?|agreg[uú]e|sum[áa](?:me)?|" +
|
|
182
|
-
"anot[áa](?:me)?|an[oó]tame|guard[áa](?:me)?|met[ée](?:me)?|" +
|
|
183
|
-
"añad[íi]|añad[ée]|pone(?:me|le)?|recor[dáa]me)" +
|
|
184
|
-
"\\s+(?:una|la|el|esta|ese|otra|otro)?\\s*" +
|
|
185
|
-
"(?:tarea|task|pendiente|todo|to-do)" +
|
|
186
|
-
"|" +
|
|
187
|
-
// (b) "nueva tarea X" / "otra tarea X" without a verb
|
|
188
|
-
"(?:nueva|otra|nuevo)\\s+(?:tarea|task|pendiente)" +
|
|
189
|
-
")" +
|
|
190
|
-
// Optional connectors before the title
|
|
191
|
-
"\\s+(?:que\\s+(?:diga|sea|es)|para|de|sobre|llamada|titulada|titul[áa]da|:|-|de:)?\\s*",
|
|
192
|
-
"i"
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
function extractTaskTitle(text) {
|
|
196
|
-
if (typeof text !== "string") return null;
|
|
197
|
-
const cleaned = text.trim().replace(/^[«"']|[«"'.,;:!?]+$/g, "");
|
|
198
|
-
if (!cleaned) return null;
|
|
199
|
-
const m = TASK_INTENT_RE.exec(cleaned);
|
|
200
|
-
if (!m) return null;
|
|
201
|
-
const title = cleaned.slice(m[0].length).trim().replace(/[.!?]+$/, "");
|
|
202
|
-
if (!title) return null;
|
|
203
|
-
return title;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function pickIntentProject({ projects, hintId }) {
|
|
207
|
-
if (!projects?.list) return null;
|
|
208
|
-
const list = projects.list();
|
|
209
|
-
if (hintId !== undefined && hintId !== null) {
|
|
210
|
-
const hit = list.find((p) => String(p.id) === String(hintId));
|
|
211
|
-
if (hit) return hit;
|
|
212
|
-
}
|
|
213
|
-
// Prefer the first non-default real project; fall back to default.
|
|
214
|
-
return list.find((p) => Number(p.id) !== 0) || list[0] || null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function tryVoiceTaskIntent({ projects, userText, hintProjectId }) {
|
|
218
|
-
const title = extractTaskTitle(userText);
|
|
219
|
-
if (!title) return { handled: false };
|
|
220
|
-
const listEntry = pickIntentProject({ projects, hintId: hintProjectId });
|
|
221
|
-
if (!listEntry) {
|
|
222
|
-
return {
|
|
223
|
-
handled: true,
|
|
224
|
-
reply: "No hay proyectos APX registrados. Agregá uno con `apx project add` y volvé a intentar.",
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
// projects.list() returns flat entries without storagePath; the
|
|
228
|
-
// resolver returns the full record. We need that for the JSONL store.
|
|
229
|
-
const project = projects.get(listEntry.id) || listEntry;
|
|
230
|
-
if (!project?.storagePath) {
|
|
231
|
-
return {
|
|
232
|
-
handled: true,
|
|
233
|
-
reply: `No pude crear la tarea: no encuentro el storage del proyecto ${project?.name || listEntry.name}.`,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
try {
|
|
237
|
-
const { createTask } = await import("#core/stores/tasks.js");
|
|
238
|
-
const task = createTask(project.storagePath, {
|
|
239
|
-
title,
|
|
240
|
-
source: "voice",
|
|
241
|
-
});
|
|
242
|
-
// Resolver may strip the human-readable name; fall back to the
|
|
243
|
-
// list entry which always has it.
|
|
244
|
-
const displayName = project.name || listEntry.name || `proyecto #${project.id}`;
|
|
245
|
-
return {
|
|
246
|
-
handled: true,
|
|
247
|
-
reply: `Listo. Anoté "${title}" en ${displayName}.`,
|
|
248
|
-
meta: { task_id: task.id, project_id: project.id },
|
|
249
|
-
};
|
|
250
|
-
} catch (e) {
|
|
251
|
-
return {
|
|
252
|
-
handled: true,
|
|
253
|
-
reply: `No pude crear la tarea: ${e.message || "error desconocido"}`,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async function decodeAudioInput({ audio, format = "webm" }) {
|
|
259
|
-
if (!audio) return null;
|
|
260
|
-
// A short string that starts with "/" and exists on disk is treated as a
|
|
261
|
-
// path. Anything else is interpreted as base64 (optionally with a data:
|
|
262
|
-
// prefix).
|
|
263
|
-
if (
|
|
264
|
-
typeof audio === "string" &&
|
|
265
|
-
audio.length < 1024 &&
|
|
266
|
-
audio.startsWith("/") &&
|
|
267
|
-
fs.existsSync(audio)
|
|
268
|
-
) {
|
|
269
|
-
return { path: audio, cleanup: false };
|
|
270
|
-
}
|
|
271
|
-
let b64 = audio;
|
|
272
|
-
const m = /^data:[^;]+;base64,(.+)$/.exec(b64);
|
|
273
|
-
if (m) b64 = m[1];
|
|
274
|
-
const buf = Buffer.from(b64, "base64");
|
|
275
|
-
if (!buf.length) throw new Error("voice/turn: decoded audio is empty");
|
|
276
|
-
const ext = String(format || "webm").replace(/^\./, "");
|
|
277
|
-
const tmp = path.join(os.tmpdir(), `apx-voice-${Date.now()}-${randomUUID()}.${ext}`);
|
|
278
|
-
fs.writeFileSync(tmp, buf);
|
|
279
|
-
return { path: tmp, cleanup: true };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
33
|
export function register(app, { projects, plugins, registries }) {
|
|
283
34
|
// GET /voice/tts?path=<abs>
|
|
284
35
|
//
|
|
@@ -345,45 +96,20 @@ export function register(app, { projects, plugins, registries }) {
|
|
|
345
96
|
});
|
|
346
97
|
}
|
|
347
98
|
|
|
348
|
-
// ── 1.5 Intent classifier (DISABLED) ──────────────────────────
|
|
349
|
-
// We used to regex-match "creá una tarea X" here and short-circuit
|
|
350
|
-
// the LLM. That fired far too eagerly — any sentence containing
|
|
351
|
-
// those words became a task title, even when the user's actual
|
|
352
|
-
// intent was different ("explicame cómo funciona crear una
|
|
353
|
-
// tarea" would create a bogus task).
|
|
354
|
-
//
|
|
355
|
-
// The right path is the agent's own tool calling: the super-agent
|
|
356
|
-
// already has `create_task`, `send_telegram`, `list_tasks`, etc.
|
|
357
|
-
// in its CORE schema (see super-agent-tools/index.js). We let
|
|
358
|
-
// the model decide; the system prompt below pushes it toward
|
|
359
|
-
// tool use instead of narrating the action in prose.
|
|
360
|
-
const intentResult = { handled: false };
|
|
361
99
|
let replyText = "";
|
|
362
100
|
const previousMessages = Array.isArray(body.previousMessages)
|
|
363
101
|
? body.previousMessages
|
|
364
102
|
: [];
|
|
365
103
|
const channel = body.channel || "voice";
|
|
366
|
-
|
|
367
|
-
let intentMeta = null;
|
|
368
104
|
let suggestions = [];
|
|
369
105
|
let toolsUsed = [];
|
|
370
|
-
|
|
106
|
+
|
|
107
|
+
const channelCtx = buildVoiceChannelContext(channel, {
|
|
371
108
|
projectId: body.projectId,
|
|
372
109
|
language: body.language && body.language !== "auto" ? body.language : "es",
|
|
373
110
|
});
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
intentMeta = intentResult.meta || null;
|
|
377
|
-
// Intent shortcut bypasses the LLM, so no model-generated
|
|
378
|
-
// suggestions either; we hand-craft a couple based on the
|
|
379
|
-
// outcome so the chips area isn't empty.
|
|
380
|
-
if (intentMeta?.task_id) {
|
|
381
|
-
suggestions = [
|
|
382
|
-
{ label: "Ver tareas", command: "deck.view:tasks" },
|
|
383
|
-
{ label: "Anotar otra", command: "voice.again" },
|
|
384
|
-
];
|
|
385
|
-
}
|
|
386
|
-
} else if (isSuperAgentEnabled(cfg)) {
|
|
111
|
+
|
|
112
|
+
if (isSuperAgentEnabled(cfg)) {
|
|
387
113
|
try {
|
|
388
114
|
const result = await runSuperAgent({
|
|
389
115
|
globalConfig: cfg,
|
|
@@ -423,9 +149,7 @@ export function register(app, { projects, plugins, registries }) {
|
|
|
423
149
|
// text to TTS — synthesize a generic confirmation so the
|
|
424
150
|
// user gets audible feedback that something happened.
|
|
425
151
|
if (!replyText && raw) {
|
|
426
|
-
replyText = suggestions.length
|
|
427
|
-
? "Listo."
|
|
428
|
-
: raw;
|
|
152
|
+
replyText = suggestions.length ? "Listo." : raw;
|
|
429
153
|
}
|
|
430
154
|
} else {
|
|
431
155
|
replyText = raw;
|
|
@@ -493,10 +217,9 @@ export function register(app, { projects, plugins, registries }) {
|
|
|
493
217
|
reply_mime: tts.mime,
|
|
494
218
|
provider: tts.provider,
|
|
495
219
|
tts_error: tts.error || undefined,
|
|
496
|
-
intent: intentMeta || undefined,
|
|
497
220
|
suggestions: suggestions.length ? suggestions : undefined,
|
|
498
221
|
tools_used: toolsUsed.length ? toolsUsed : undefined,
|
|
499
|
-
channel
|
|
222
|
+
channel,
|
|
500
223
|
});
|
|
501
224
|
} catch (e) {
|
|
502
225
|
res.status(500).json({ error: e.message });
|
package/src/host/daemon/api.js
CHANGED
|
@@ -18,6 +18,7 @@ import { register as registerProjects } from "./api/projects.js";
|
|
|
18
18
|
import { register as registerAgents } from "./api/agents.js";
|
|
19
19
|
import { register as registerSessions } from "./api/sessions.js";
|
|
20
20
|
import { register as registerMcps } from "./api/mcps.js";
|
|
21
|
+
import { register as registerVars } from "./api/vars.js";
|
|
21
22
|
import { register as registerMessages } from "./api/messages.js";
|
|
22
23
|
import { register as registerTelegram } from "./api/telegram.js";
|
|
23
24
|
import { register as registerPlugins } from "./api/plugins.js";
|
|
@@ -106,6 +107,7 @@ export function buildApi({
|
|
|
106
107
|
registerAgents(app, ctx);
|
|
107
108
|
registerSessions(app, ctx);
|
|
108
109
|
registerMcps(app, ctx);
|
|
110
|
+
registerVars(app, ctx);
|
|
109
111
|
registerMessages(app, ctx);
|
|
110
112
|
registerEngines(app, ctx);
|
|
111
113
|
registerSkills(app, ctx);
|
package/src/host/daemon/db.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { appendMessageToFs } from "#core/stores/messages.js";
|
|
6
6
|
import { effectiveConfig } from "./project-config.js";
|
|
7
7
|
import { readAgents } from "#core/apc/parser.js";
|
|
8
|
+
import { apcDir, apcProjectFile, apcAgentsDir, apcCommandsDir } from "#core/apc/paths.js";
|
|
8
9
|
import { getOrCreateApxId } from "#core/apc/scaffold.js";
|
|
9
10
|
import {
|
|
10
11
|
ensureProjectStorage,
|
|
@@ -30,12 +31,12 @@ export class ProjectManager {
|
|
|
30
31
|
register(projectPath) {
|
|
31
32
|
const abs = path.resolve(projectPath);
|
|
32
33
|
if (this.byPath.has(abs)) return this.byPath.get(abs);
|
|
33
|
-
const projectJson =
|
|
34
|
+
const projectJson = apcProjectFile(abs);
|
|
34
35
|
if (!fs.existsSync(projectJson)) {
|
|
35
36
|
throw new Error(`not an APC project: ${abs}`);
|
|
36
37
|
}
|
|
37
38
|
// Ensure directories exist for projects initialized before they were added.
|
|
38
|
-
fs.mkdirSync(
|
|
39
|
+
fs.mkdirSync(apcCommandsDir(abs), { recursive: true });
|
|
39
40
|
|
|
40
41
|
// Resolve stable APX storage ID (read from .apc/project.json).
|
|
41
42
|
const apxId = getOrCreateApxId(abs);
|
|
@@ -72,9 +73,8 @@ export class ProjectManager {
|
|
|
72
73
|
if (this.byId.has(0)) return this.byId.get(0);
|
|
73
74
|
// Create a minimal APC-compatible structure inside the storage root so that
|
|
74
75
|
// readAgents() and other parser functions work without a separate project dir.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const projectJson = path.join(apcDir, "project.json");
|
|
76
|
+
fs.mkdirSync(apcAgentsDir(DEFAULT_PROJECT_STORE), { recursive: true });
|
|
77
|
+
const projectJson = apcProjectFile(DEFAULT_PROJECT_STORE);
|
|
78
78
|
if (!fs.existsSync(projectJson)) {
|
|
79
79
|
fs.writeFileSync(
|
|
80
80
|
projectJson,
|
|
@@ -113,7 +113,7 @@ export class ProjectManager {
|
|
|
113
113
|
let kind = e.id === 0 ? "default" : "other";
|
|
114
114
|
try {
|
|
115
115
|
const meta = JSON.parse(
|
|
116
|
-
fs.readFileSync(
|
|
116
|
+
fs.readFileSync(apcProjectFile(e.path), "utf8")
|
|
117
117
|
);
|
|
118
118
|
if (meta.name) name = meta.name;
|
|
119
119
|
if (typeof meta.kind === "string" && meta.kind.trim()) {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// /deck/exec implementation.
|
|
2
|
+
//
|
|
3
|
+
// All shell spawning sits behind this helper so api/deck.js stays a thin HTTP
|
|
4
|
+
// adapter. The OS abstraction is intentionally tiny: pick the "opener" command
|
|
5
|
+
// for the platform and pass `target` as a single arg (no shell). For
|
|
6
|
+
// app-launching on macOS we use `open -a <App>`.
|
|
7
|
+
//
|
|
8
|
+
// Stays in host/daemon/ because it's pure process orchestration (spawn child
|
|
9
|
+
// processes), not domain logic.
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
const MAC_APPS = {
|
|
13
|
+
// Whitelisted mac app names. Adding here is the only way the deck can
|
|
14
|
+
// launch something — we never honour a free-form `app` string.
|
|
15
|
+
claude: "Claude",
|
|
16
|
+
chatgpt: "ChatGPT",
|
|
17
|
+
cursor: "Cursor",
|
|
18
|
+
vscode: "Visual Studio Code",
|
|
19
|
+
zen: "Zen Browser",
|
|
20
|
+
terminal: "Terminal",
|
|
21
|
+
iterm: "iTerm",
|
|
22
|
+
finder: "Finder",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function platformOpener() {
|
|
26
|
+
if (process.platform === "darwin") return "open";
|
|
27
|
+
if (process.platform === "win32") return "start";
|
|
28
|
+
return "xdg-open";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function spawnDetached(cmd, args) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
34
|
+
let settled = false;
|
|
35
|
+
const done = (err) => {
|
|
36
|
+
if (settled) return;
|
|
37
|
+
settled = true;
|
|
38
|
+
err ? reject(err) : resolve();
|
|
39
|
+
};
|
|
40
|
+
child.on("error", done);
|
|
41
|
+
// Give the process a tick to fail-fast (bad binary); otherwise detach.
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
try { child.unref(); } catch {}
|
|
44
|
+
done(null);
|
|
45
|
+
}, 250);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Pipe `text` into the platform clipboard (pbcopy / xclip / clip). */
|
|
50
|
+
export async function copyToClipboard(text) {
|
|
51
|
+
const platform = process.platform;
|
|
52
|
+
const cmd =
|
|
53
|
+
platform === "darwin" ? "pbcopy" :
|
|
54
|
+
platform === "win32" ? "clip" :
|
|
55
|
+
"xclip";
|
|
56
|
+
const args = platform === "linux" ? ["-selection", "clipboard"] : [];
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
const child = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
59
|
+
child.on("error", reject);
|
|
60
|
+
child.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`))));
|
|
61
|
+
child.stdin.end(text);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Dispatch one /deck/exec action. `ctx.projects` is the daemon's
|
|
67
|
+
* ProjectManager — used to resolve numeric project ids to absolute paths.
|
|
68
|
+
*
|
|
69
|
+
* Supported kinds:
|
|
70
|
+
* open_app { target: "<appKey>" } — mac only
|
|
71
|
+
* open_path { target: "<absPath>" | "<projectId>" } — opens in Finder/default
|
|
72
|
+
* open_path_in { target: "<projectId>", app: "<appKey>" } — mac only
|
|
73
|
+
* open_url { target: "https://..." }
|
|
74
|
+
* copy_clipboard { text: "..." }
|
|
75
|
+
*/
|
|
76
|
+
export async function runDeckExec({ kind, target, appHint, text, ctx }) {
|
|
77
|
+
const platform = process.platform;
|
|
78
|
+
|
|
79
|
+
// Resolve a project id (number or "<n>") into an absolute path via
|
|
80
|
+
// the daemon's project manager. Returns null when the id is bogus.
|
|
81
|
+
const projectPath = (idOrPath) => {
|
|
82
|
+
if (!idOrPath) return null;
|
|
83
|
+
const str = String(idOrPath);
|
|
84
|
+
if (str.startsWith("/")) return str;
|
|
85
|
+
if (!/^\d+$/.test(str)) return null;
|
|
86
|
+
const p = ctx.projects?.get?.(parseInt(str, 10));
|
|
87
|
+
return p?.path || null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (kind === "open_app") {
|
|
91
|
+
if (platform !== "darwin") throw new Error("open_app only implemented on macOS for now");
|
|
92
|
+
const appName = MAC_APPS[String(target || "").toLowerCase()];
|
|
93
|
+
if (!appName) throw new Error(`unknown app: ${target}`);
|
|
94
|
+
// Two-step launch:
|
|
95
|
+
// 1. `open -a` ensures the app is running (no-op if already up).
|
|
96
|
+
// 2. AppleScript `activate` brings it to the foreground across
|
|
97
|
+
// Spaces / Stage Manager, which `open` alone often skips when
|
|
98
|
+
// the app was already running in the background.
|
|
99
|
+
await spawnDetached("open", ["-a", appName]);
|
|
100
|
+
try {
|
|
101
|
+
await new Promise((resolve) => {
|
|
102
|
+
const child = spawn("osascript", [
|
|
103
|
+
"-e",
|
|
104
|
+
`tell application "${appName}" to activate`,
|
|
105
|
+
], { stdio: "ignore" });
|
|
106
|
+
child.on("close", () => resolve());
|
|
107
|
+
child.on("error", () => resolve());
|
|
108
|
+
setTimeout(() => { try { child.kill(); } catch {} ; resolve(); }, 600);
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
// osascript missing or refused — `open -a` already ran.
|
|
112
|
+
}
|
|
113
|
+
return { app: appName };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (kind === "open_path") {
|
|
117
|
+
const resolved = projectPath(target);
|
|
118
|
+
if (!resolved) throw new Error(`open_path: invalid target ${target}`);
|
|
119
|
+
await spawnDetached(platformOpener(), [resolved]);
|
|
120
|
+
return { path: resolved };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (kind === "open_path_in") {
|
|
124
|
+
if (platform !== "darwin") throw new Error("open_path_in only implemented on macOS for now");
|
|
125
|
+
const resolved = projectPath(target);
|
|
126
|
+
if (!resolved) throw new Error(`open_path_in: invalid target ${target}`);
|
|
127
|
+
const appName = MAC_APPS[String(appHint || "").toLowerCase()];
|
|
128
|
+
if (!appName) throw new Error(`open_path_in: unknown app ${appHint}`);
|
|
129
|
+
await spawnDetached("open", ["-a", appName, resolved]);
|
|
130
|
+
return { app: appName, path: resolved };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (kind === "open_url") {
|
|
134
|
+
if (!target || !/^https?:\/\//i.test(String(target))) {
|
|
135
|
+
throw new Error("open_url: target must be http(s) URL");
|
|
136
|
+
}
|
|
137
|
+
await spawnDetached(platformOpener(), [String(target)]);
|
|
138
|
+
return { url: target };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (kind === "copy_clipboard") {
|
|
142
|
+
if (typeof text !== "string") throw new Error("copy_clipboard: text required");
|
|
143
|
+
await copyToClipboard(text);
|
|
144
|
+
return { bytes: text.length };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new Error(`unknown kind: ${kind}`);
|
|
148
|
+
}
|
package/src/host/daemon/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { ProjectManager } from "./db.js";
|
|
19
19
|
import { McpRegistry } from "#core/mcp/runner.js";
|
|
20
20
|
import { PluginManager } from "./plugins/index.js";
|
|
21
|
-
import { RoutineScheduler } from "./routines.js";
|
|
21
|
+
import { RoutineScheduler } from "./routines-scheduler.js";
|
|
22
22
|
import { buildApi } from "./api.js";
|
|
23
23
|
import { createTokenStore } from "./token-store.js";
|
|
24
24
|
import { triggerWakeup } from "./wakeup.js";
|
|
@@ -222,7 +222,7 @@ async function main() {
|
|
|
222
222
|
setTimeout(() => triggerWakeup(cfg, log), 3000);
|
|
223
223
|
// Preload whisper-server in the background so first desktop transcription is fast.
|
|
224
224
|
// Adopts an existing one if already on the port; otherwise spawns fresh.
|
|
225
|
-
import("./
|
|
225
|
+
import("./whisper-server.js").then(({ preloadWhisperServer }) => {
|
|
226
226
|
preloadWhisperServer((m) => log(m));
|
|
227
227
|
}).catch(() => {});
|
|
228
228
|
});
|
|
@@ -257,7 +257,7 @@ async function main() {
|
|
|
257
257
|
stopMemory();
|
|
258
258
|
registries.shutdown();
|
|
259
259
|
// Best-effort shutdown of whisper-server subprocess.
|
|
260
|
-
import("./
|
|
260
|
+
import("./whisper-server.js").then(({ shutdownWhisperServer }) => {
|
|
261
261
|
shutdownWhisperServer().catch(() => {});
|
|
262
262
|
}).catch(() => {});
|
|
263
263
|
server.close(() => {
|