@agentprojectcontext/apx 1.33.1 → 1.35.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/apx/SKILL.md +49 -61
- package/src/core/agent/a2a/reply.js +48 -0
- package/src/core/agent/build-agent-system.js +136 -59
- 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 +178 -124
- package/src/core/agent/prompts/channels/code.md +12 -10
- package/src/core/agent/prompts/channels/desktop.md +5 -32
- package/src/core/agent/prompts/channels/telegram.md +4 -15
- package/src/core/agent/prompts/channels/web_code.md +11 -11
- package/src/core/agent/prompts/core/agent-base.md +24 -0
- package/src/core/agent/prompts/core/project-agent.md +11 -0
- package/src/core/agent/prompts/core/super-agent.md +21 -0
- package/src/core/agent/prompts/discipline/action.md +10 -0
- package/src/core/agent/prompts/discipline/single-segment.md +6 -0
- package/src/core/agent/prompts/discipline/two-segment.md +11 -0
- 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/self-memory.js +43 -1
- package/src/core/agent/skills/index-store.js +307 -0
- package/src/core/agent/skills/index.js +15 -1
- package/src/core/agent/skills/inspector.js +317 -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/super-agent.js +7 -1
- package/src/core/agent/tools/handlers/_git.js +50 -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/git-diff.js +44 -0
- package/src/core/agent/tools/handlers/git-log.js +38 -0
- package/src/core/agent/tools/handlers/git-show.js +34 -0
- package/src/core/agent/tools/handlers/git-status.js +61 -0
- 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 +169 -0
- package/src/core/agent/tools/registry-bridge.js +6 -14
- package/src/core/agent/tools/registry.js +103 -69
- package/src/core/apc/context-copy.js +27 -0
- package/src/core/apc/notes.js +19 -0
- package/src/core/apc/parser.js +12 -5
- package/src/core/apc/paths.js +87 -0
- package/src/core/apc/scaffold.js +82 -76
- package/src/core/apc/skill-sync.js +10 -0
- package/src/{host/daemon/plugins → core/channels}/telegram/dispatch.js +38 -16
- package/src/core/config/index.js +24 -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/{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 +83 -0
- package/src/core/runtime-skills/apx-agency-agents/SKILL.md +125 -0
- package/src/core/runtime-skills/apx-agent/SKILL.md +97 -0
- package/src/core/runtime-skills/apx-mcp/SKILL.md +111 -0
- package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +169 -0
- package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +20 -29
- package/src/core/runtime-skills/apx-routine/SKILL.md +127 -0
- package/src/core/runtime-skills/apx-runtime/SKILL.md +99 -0
- package/src/core/runtime-skills/apx-sessions/SKILL.md +232 -0
- package/src/core/runtime-skills/apx-skill-builder/SKILL.md +129 -0
- package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +18 -21
- package/src/core/runtime-skills/apx-telegram/SKILL.md +120 -0
- package/src/core/runtime-skills/apx-voice/SKILL.md +117 -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 -80
- 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/skills.js +140 -6
- package/src/host/daemon/api/super-agent.js +56 -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 +20 -3
- package/src/host/daemon/plugins/telegram/index.js +9 -9
- 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/branding.js +53 -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 +290 -55
- package/src/interfaces/cli/index.js +84 -2
- package/src/interfaces/web/dist/assets/index-C0fm31dY.js +618 -0
- package/src/interfaces/web/dist/assets/index-C0fm31dY.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-UcAqlBO6.css +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 +2 -1
- 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 +37 -4
- 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 +73 -4
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +222 -0
- 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/hooks/useChat.ts +19 -0
- package/src/interfaces/web/src/i18n/en.ts +175 -7
- package/src/interfaces/web/src/i18n/es.ts +180 -15
- package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
- package/src/interfaces/web/src/lib/api/skills.ts +70 -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/SettingsScreen.tsx +6 -2
- 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 +15 -0
- package/skills/apx-agency-agents/SKILL.md +0 -141
- package/skills/apx-agent/SKILL.md +0 -100
- package/skills/apx-mcp-builder/SKILL.md +0 -183
- package/skills/apx-routine/SKILL.md +0 -140
- package/skills/apx-runtime/SKILL.md +0 -117
- package/skills/apx-sessions/SKILL.md +0 -281
- package/skills/apx-skill-builder/SKILL.md +0 -153
- package/skills/apx-telegram/SKILL.md +0 -131
- package/skills/apx-voice/SKILL.md +0 -137
- package/src/core/agent/prompts/action-discipline.md +0 -24
- package/src/core/agent/prompts/super-agent-base.md +0 -42
- package/src/host/daemon/transcription.js +0 -538
- package/src/host/daemon/whisper-transcribe.py +0 -73
- package/src/interfaces/web/dist/assets/index-Aaiw8BZN.css +0 -1
- package/src/interfaces/web/dist/assets/index-DPqtjDjh.js +0 -602
- package/src/interfaces/web/dist/assets/index-DPqtjDjh.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/helpers.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/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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Variable management per project. Reads/writes the two APX-owned scopes
|
|
2
|
+
// (project = <storagePath>/vars.json, global = ~/.apx/vars.json) and
|
|
3
|
+
// surfaces a merged effective view with sources annotated.
|
|
4
|
+
//
|
|
5
|
+
// GET /projects/:pid/vars -> { project, global, effective, sources }
|
|
6
|
+
// values masked unless ?reveal=1
|
|
7
|
+
// GET /projects/:pid/vars/:name -> { name, scope, value, masked }
|
|
8
|
+
// ?reveal=1 unmasks
|
|
9
|
+
// POST /projects/:pid/vars -> { ok, name, scope }
|
|
10
|
+
// body { name, value, scope }
|
|
11
|
+
// scope defaults to "project" (or
|
|
12
|
+
// "global" if pid=0).
|
|
13
|
+
// DELETE /projects/:pid/vars/:name?scope=… 204
|
|
14
|
+
//
|
|
15
|
+
// pid=0 (base project) is the conventional bucket for editing global vars
|
|
16
|
+
// from the web UI; project scope is rejected there because there is no
|
|
17
|
+
// storagePath that belongs to a real project.
|
|
18
|
+
import {
|
|
19
|
+
loadAllVars,
|
|
20
|
+
readGlobalVars,
|
|
21
|
+
readProjectVars,
|
|
22
|
+
setVar,
|
|
23
|
+
deleteVar,
|
|
24
|
+
maskValue,
|
|
25
|
+
} from "#core/vars/index.js";
|
|
26
|
+
|
|
27
|
+
function normalizeScope(raw, { isBase }) {
|
|
28
|
+
if (!raw) return isBase ? "global" : "project";
|
|
29
|
+
const s = String(raw).toLowerCase();
|
|
30
|
+
if (s === "project" || s === "global") return s;
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function maskAll(obj) {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const [k, v] of Object.entries(obj)) out[k] = maskValue(v);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function register(app, { project }) {
|
|
41
|
+
app.get("/projects/:pid/vars", (req, res) => {
|
|
42
|
+
const p = project(req, res);
|
|
43
|
+
if (!p) return;
|
|
44
|
+
const reveal = req.query?.reveal === "1" || req.query?.reveal === "true";
|
|
45
|
+
const { project: proj, global, effective, sources } = loadAllVars({
|
|
46
|
+
storagePath: p.storagePath,
|
|
47
|
+
});
|
|
48
|
+
res.json({
|
|
49
|
+
scope_hint: String(p.id) === "0" ? "global" : "project",
|
|
50
|
+
project: reveal ? proj : maskAll(proj),
|
|
51
|
+
global: reveal ? global : maskAll(global),
|
|
52
|
+
effective: reveal ? effective : maskAll(effective),
|
|
53
|
+
sources,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
app.get("/projects/:pid/vars/:name", (req, res) => {
|
|
58
|
+
const p = project(req, res);
|
|
59
|
+
if (!p) return;
|
|
60
|
+
const name = req.params.name;
|
|
61
|
+
const proj = p.storagePath ? readProjectVars(p.storagePath) : {};
|
|
62
|
+
const global = readGlobalVars();
|
|
63
|
+
let scope = null;
|
|
64
|
+
let value = null;
|
|
65
|
+
if (Object.prototype.hasOwnProperty.call(proj, name)) {
|
|
66
|
+
scope = "project";
|
|
67
|
+
value = proj[name];
|
|
68
|
+
} else if (Object.prototype.hasOwnProperty.call(global, name)) {
|
|
69
|
+
scope = "global";
|
|
70
|
+
value = global[name];
|
|
71
|
+
} else {
|
|
72
|
+
return res.status(404).json({ error: `variable "${name}" not found` });
|
|
73
|
+
}
|
|
74
|
+
const reveal = req.query?.reveal === "1" || req.query?.reveal === "true";
|
|
75
|
+
res.json({
|
|
76
|
+
name,
|
|
77
|
+
scope,
|
|
78
|
+
value: reveal ? value : maskValue(value),
|
|
79
|
+
masked: !reveal,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
app.post("/projects/:pid/vars", (req, res) => {
|
|
84
|
+
const p = project(req, res);
|
|
85
|
+
if (!p) return;
|
|
86
|
+
const { name, value } = req.body || {};
|
|
87
|
+
if (!name || typeof name !== "string") {
|
|
88
|
+
return res.status(400).json({ error: "name required" });
|
|
89
|
+
}
|
|
90
|
+
if (value === undefined || value === null) {
|
|
91
|
+
return res.status(400).json({ error: "value required" });
|
|
92
|
+
}
|
|
93
|
+
const isBase = String(p.id) === "0";
|
|
94
|
+
const scope = normalizeScope(req.body?.scope, { isBase });
|
|
95
|
+
if (scope === null) {
|
|
96
|
+
return res
|
|
97
|
+
.status(400)
|
|
98
|
+
.json({ error: `unknown scope "${req.body?.scope}" (use project|global)` });
|
|
99
|
+
}
|
|
100
|
+
if (scope === "project" && (!p.storagePath || isBase)) {
|
|
101
|
+
return res.status(400).json({
|
|
102
|
+
error: "project scope is not available for the base workspace — use scope=global",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
setVar({ storagePath: p.storagePath, scope, name, value });
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return res.status(400).json({ error: e.message });
|
|
109
|
+
}
|
|
110
|
+
res.status(201).json({ ok: true, name, scope });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
app.delete("/projects/:pid/vars/:name", (req, res) => {
|
|
114
|
+
const p = project(req, res);
|
|
115
|
+
if (!p) return;
|
|
116
|
+
const isBase = String(p.id) === "0";
|
|
117
|
+
const scope = normalizeScope(req.query?.scope, { isBase });
|
|
118
|
+
if (scope === null) {
|
|
119
|
+
return res
|
|
120
|
+
.status(400)
|
|
121
|
+
.json({ error: `unknown scope "${req.query?.scope}" (use project|global)` });
|
|
122
|
+
}
|
|
123
|
+
if (scope === "project" && (!p.storagePath || isBase)) {
|
|
124
|
+
return res.status(400).json({
|
|
125
|
+
error: "project scope is not available for the base workspace",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
let removed;
|
|
129
|
+
try {
|
|
130
|
+
removed = deleteVar({ storagePath: p.storagePath, scope, name: req.params.name });
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return res.status(400).json({ error: e.message });
|
|
133
|
+
}
|
|
134
|
+
if (!removed) return res.status(404).end();
|
|
135
|
+
res.status(204).end();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -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()) {
|