@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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Stateless helpers for the Telegram plugin. Extracted from index.js so the
|
|
2
|
+
// big poller class stays focused on lifecycle + message dispatch. Each
|
|
3
|
+
// function is pure (no `this`) — instances import them and call as needed.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { TELEGRAM_STATE_PATH } from "#core/config/index.js";
|
|
6
|
+
|
|
7
|
+
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build the channelMeta block the super-agent loop receives for a Telegram
|
|
11
|
+
* turn. The prompt template at src/core/agent/prompts/channels/telegram.md
|
|
12
|
+
* interpolates `{{projectBlock}}` and `{{routeBlock}}` verbatim, so we
|
|
13
|
+
* pre-render them as plain text (the template engine doesn't do conditionals).
|
|
14
|
+
*/
|
|
15
|
+
export function buildTelegramMeta({ channelName, author, chatId, target, routeToAgent }) {
|
|
16
|
+
const projectBlock = target
|
|
17
|
+
? `\nProject pin: **${target.name || "(unnamed)"}** (\`${target.path || "?"}\`).\n` +
|
|
18
|
+
"This Telegram channel belongs to that project. Default any " +
|
|
19
|
+
"project-scoped tool call (list_agents, list_tasks, list_mcps, " +
|
|
20
|
+
"list_skills, create_task, list_routines, …) to " +
|
|
21
|
+
`\`${target.name || target.path}\` without asking the user "which ` +
|
|
22
|
+
'project?". Only ask when they explicitly reference another project ' +
|
|
23
|
+
"by name."
|
|
24
|
+
: "";
|
|
25
|
+
const routeBlock = routeToAgent
|
|
26
|
+
? `\nMaster agent for this channel: **${routeToAgent}**. Prefer ` +
|
|
27
|
+
`delegating substantive work to that agent via call_agent({ project: ` +
|
|
28
|
+
`"${target?.name || target?.path || ""}", agent: "${routeToAgent}", ` +
|
|
29
|
+
"prompt: <user message> }) rather than answering yourself, unless " +
|
|
30
|
+
"the message is small-talk or a quick factual reply."
|
|
31
|
+
: "";
|
|
32
|
+
return {
|
|
33
|
+
channelName,
|
|
34
|
+
author,
|
|
35
|
+
chatId,
|
|
36
|
+
projectBlock,
|
|
37
|
+
routeBlock,
|
|
38
|
+
...(target ? {
|
|
39
|
+
projectId: String(target.id),
|
|
40
|
+
projectName: target.name || "",
|
|
41
|
+
projectPath: target.path || "",
|
|
42
|
+
} : {}),
|
|
43
|
+
...(routeToAgent ? { routeToAgent } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Load the cross-channel offset state from ~/.apx/telegram-state.json. */
|
|
48
|
+
export function loadState() {
|
|
49
|
+
if (!fs.existsSync(TELEGRAM_STATE_PATH)) return { channels: {} };
|
|
50
|
+
try {
|
|
51
|
+
const raw = JSON.parse(fs.readFileSync(TELEGRAM_STATE_PATH, "utf8"));
|
|
52
|
+
return { channels: raw.channels || {}, _legacy_offset: raw.offset || 0 };
|
|
53
|
+
} catch {
|
|
54
|
+
return { channels: {} };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Write the cross-channel offset state. Adds an `updated_at` timestamp. */
|
|
59
|
+
export function saveState(state) {
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
TELEGRAM_STATE_PATH,
|
|
62
|
+
JSON.stringify({ ...state, updated_at: nowIso() }, null, 2) + "\n"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function resolveBotToken(channel) {
|
|
67
|
+
return (
|
|
68
|
+
channel.bot_token ||
|
|
69
|
+
process.env.BOT_TELEGRAM_TOKEN ||
|
|
70
|
+
process.env.TELEGRAM_BOT_TOKEN ||
|
|
71
|
+
""
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resolveChatId(channel) {
|
|
76
|
+
return (
|
|
77
|
+
channel.chat_id ||
|
|
78
|
+
process.env.TELEGRAM_CHAT_ID ||
|
|
79
|
+
process.env.BOT_TELEGRAM_CHAT_ID ||
|
|
80
|
+
""
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function tokenSource(channel) {
|
|
85
|
+
if (channel.bot_token) return "config";
|
|
86
|
+
if (process.env.BOT_TELEGRAM_TOKEN) return "env:BOT_TELEGRAM_TOKEN";
|
|
87
|
+
if (process.env.TELEGRAM_BOT_TOKEN) return "env:TELEGRAM_BOT_TOKEN";
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the list of telegram channels to poll, honouring both the
|
|
93
|
+
* canonical telegram.channels[] and the legacy single-channel mode.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveChannels(globalConfig) {
|
|
96
|
+
const tg = globalConfig.telegram || {};
|
|
97
|
+
if (Array.isArray(tg.channels) && tg.channels.length > 0) {
|
|
98
|
+
return tg.channels.map((c, i) => ({
|
|
99
|
+
name: c.name || `channel-${i + 1}`,
|
|
100
|
+
bot_token: c.bot_token || "",
|
|
101
|
+
chat_id: c.chat_id || "",
|
|
102
|
+
route_to_agent: c.route_to_agent || "",
|
|
103
|
+
project: c.project || null,
|
|
104
|
+
respond_with_engine:
|
|
105
|
+
c.respond_with_engine !== undefined
|
|
106
|
+
? c.respond_with_engine
|
|
107
|
+
: tg.respond_with_engine !== false,
|
|
108
|
+
poll_interval_ms: c.poll_interval_ms || tg.poll_interval_ms || 1500,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
// Legacy single-channel mode
|
|
112
|
+
if (!tg.bot_token && !process.env.BOT_TELEGRAM_TOKEN && !process.env.TELEGRAM_BOT_TOKEN) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
name: "default",
|
|
118
|
+
bot_token: tg.bot_token || "",
|
|
119
|
+
chat_id: tg.chat_id || "",
|
|
120
|
+
route_to_agent: tg.route_to_agent || "",
|
|
121
|
+
project: null,
|
|
122
|
+
respond_with_engine: tg.respond_with_engine !== false,
|
|
123
|
+
poll_interval_ms: tg.poll_interval_ms || 1500,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function sleep(ms) {
|
|
129
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
130
|
+
}
|
package/src/core/config/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import fs from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { APX_HOME, CONFIG_PATH } from "./paths.js";
|
|
8
8
|
import { PERMISSION_MODES, DEFAULT_PERMISSION_MODE } from "../constants/permissions.js";
|
|
9
|
+
import { agentsMdFile, apcProjectFile } from "../apc/paths.js";
|
|
9
10
|
|
|
10
11
|
export {
|
|
11
12
|
APX_HOME,
|
|
@@ -395,10 +396,10 @@ export function effectiveHost(cfg) {
|
|
|
395
396
|
|
|
396
397
|
export function addProject(cfg, projectPath) {
|
|
397
398
|
const abs = path.resolve(projectPath);
|
|
398
|
-
if (!fs.existsSync(
|
|
399
|
+
if (!fs.existsSync(agentsMdFile(abs))) {
|
|
399
400
|
throw new Error(`not an APC project: ${abs} (no AGENTS.md)`);
|
|
400
401
|
}
|
|
401
|
-
if (!fs.existsSync(
|
|
402
|
+
if (!fs.existsSync(apcProjectFile(abs))) {
|
|
402
403
|
throw new Error(`not an APC project: ${abs} (no .apc/project.json)`);
|
|
403
404
|
}
|
|
404
405
|
const exists = cfg.projects.find((p) => path.resolve(p.path) === abs);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Secret redaction for the global config. Wraps any string secret with a
|
|
2
|
+
// `*** set *** (...<suffix>)` marker so the web admin can show "value is set"
|
|
3
|
+
// without leaking it, AND so PATCH callers can echo back the marker to mean
|
|
4
|
+
// "don't touch this one" — see isSecretMarker / mergeRedactedChannels below.
|
|
5
|
+
//
|
|
6
|
+
// The dotted paths in SECRET_PATHS are the single source of truth for "which
|
|
7
|
+
// keys are secrets". Anything new (a new engine api_key, a new TTS provider
|
|
8
|
+
// key, etc.) goes here and every redaction path picks it up.
|
|
9
|
+
|
|
10
|
+
const SECRET_MARKER_PREFIX = "*** set ***";
|
|
11
|
+
|
|
12
|
+
export const SECRET_PATHS = [
|
|
13
|
+
"engines.anthropic.api_key",
|
|
14
|
+
"engines.openai.api_key",
|
|
15
|
+
"engines.groq.api_key",
|
|
16
|
+
"engines.openrouter.api_key",
|
|
17
|
+
"engines.gemini.api_key",
|
|
18
|
+
"voice.tts.elevenlabs.api_key",
|
|
19
|
+
"voice.tts.openai.api_key",
|
|
20
|
+
"voice.tts.gemini.api_key",
|
|
21
|
+
"memory.embeddings.openai.api_key",
|
|
22
|
+
"memory.embeddings.gemini.api_key",
|
|
23
|
+
// Telegram bot tokens live inside an array — handled separately in redact()
|
|
24
|
+
// because dotted paths can't address array entries.
|
|
25
|
+
"telegram.channels.*.bot_token",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** Replace a secret string with the visible marker, preserving the last 5 chars. */
|
|
29
|
+
export function secretMarker(value) {
|
|
30
|
+
if (typeof value !== "string" || !value.length) return value;
|
|
31
|
+
const suffix = value.slice(-5);
|
|
32
|
+
return `${SECRET_MARKER_PREFIX} (...${suffix})`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** True when a value is the placeholder a redacted view sends back unchanged. */
|
|
36
|
+
export function isSecretMarker(value) {
|
|
37
|
+
return typeof value === "string" && value.startsWith(SECRET_MARKER_PREFIX);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Deep-copy of `cfg` with every secret string replaced by its marker. */
|
|
41
|
+
export function redactConfig(cfg) {
|
|
42
|
+
const out = JSON.parse(JSON.stringify(cfg || {}));
|
|
43
|
+
const mark = (val) => (typeof val === "string" && val.length ? secretMarker(val) : val);
|
|
44
|
+
|
|
45
|
+
for (const dotted of SECRET_PATHS) {
|
|
46
|
+
if (dotted.includes("*")) continue;
|
|
47
|
+
const parts = dotted.split(".");
|
|
48
|
+
let cur = out;
|
|
49
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
50
|
+
if (!cur[parts[i]] || typeof cur[parts[i]] !== "object") { cur = null; break; }
|
|
51
|
+
cur = cur[parts[i]];
|
|
52
|
+
}
|
|
53
|
+
if (cur && cur[parts[parts.length - 1]]) {
|
|
54
|
+
cur[parts[parts.length - 1]] = mark(cur[parts[parts.length - 1]]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const channels = out?.telegram?.channels;
|
|
58
|
+
if (Array.isArray(channels)) {
|
|
59
|
+
for (const ch of channels) {
|
|
60
|
+
if (ch && typeof ch.bot_token === "string" && ch.bot_token.length) {
|
|
61
|
+
ch.bot_token = mark(ch.bot_token);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Redact a single Telegram channel record. */
|
|
69
|
+
export function redactChannel(channel) {
|
|
70
|
+
if (!channel?.bot_token) return channel;
|
|
71
|
+
return { ...channel, bot_token: secretMarker(channel.bot_token) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Merge a PATCH-shape `nextChannels` against the prior on-disk list. Any
|
|
76
|
+
* incoming channel whose bot_token is missing or a marker takes the prior
|
|
77
|
+
* token verbatim — so a UI that echoes the redacted view back doesn't wipe
|
|
78
|
+
* the real secret.
|
|
79
|
+
*/
|
|
80
|
+
export function mergeRedactedChannels(nextChannels, priorChannels) {
|
|
81
|
+
if (!Array.isArray(nextChannels)) return nextChannels;
|
|
82
|
+
const priorByName = new Map(
|
|
83
|
+
(Array.isArray(priorChannels) ? priorChannels : [])
|
|
84
|
+
.filter((c) => c && typeof c.name === "string")
|
|
85
|
+
.map((c) => [c.name, c])
|
|
86
|
+
);
|
|
87
|
+
return nextChannels.map((channel) => {
|
|
88
|
+
if (!channel || typeof channel !== "object") return channel;
|
|
89
|
+
const prior = priorByName.get(channel.name);
|
|
90
|
+
if (prior?.bot_token && (channel.bot_token === undefined || isSecretMarker(channel.bot_token))) {
|
|
91
|
+
return { ...channel, bot_token: prior.bot_token };
|
|
92
|
+
}
|
|
93
|
+
return channel;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -16,4 +16,6 @@ export const CHANNELS = Object.freeze({
|
|
|
16
16
|
DECK: "deck", // Mobile cockpit dashboard
|
|
17
17
|
DESKTOP: "desktop", // Electron capsule (always voice mode)
|
|
18
18
|
CODE: "code", // `apx code` — terminal coding session
|
|
19
|
+
DIRECT: "direct", // Planned: 1:1 channel that isn't a chat platform
|
|
20
|
+
WHATSAPP: "whatsapp", // Planned: WhatsApp bot integration
|
|
19
21
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Code session modes. PLAN = read-only exploration (the agent proposes
|
|
2
|
+
// changes but never mutates); BUILD = unrestricted execution. The value
|
|
3
|
+
// lives in code-sessions.json (session.mode) and is what api/code.js,
|
|
4
|
+
// stores/code-sessions.js, and agent/prompts/modes/ all branch on.
|
|
5
|
+
export const CODE_MODES = Object.freeze({
|
|
6
|
+
PLAN: "plan",
|
|
7
|
+
BUILD: "build",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_CODE_MODE = CODE_MODES.BUILD;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// APX Deck manifest — the data model the companion clients (deck, desktop
|
|
2
|
+
// capsule) read on boot. Pure data + decoration; no HTTP or filesystem.
|
|
3
|
+
// host/daemon/api/deck.js wraps this for the /deck/manifest endpoint.
|
|
4
|
+
|
|
5
|
+
export const CORE_WIDGETS = [
|
|
6
|
+
{
|
|
7
|
+
id: "apx-current-project",
|
|
8
|
+
title: "Proyecto actual",
|
|
9
|
+
source: "apx",
|
|
10
|
+
desktop: "project",
|
|
11
|
+
kind: "context",
|
|
12
|
+
status: "available",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "apx-voice",
|
|
16
|
+
title: "Voz APX",
|
|
17
|
+
source: "apx",
|
|
18
|
+
desktop: "general",
|
|
19
|
+
kind: "voice",
|
|
20
|
+
status: "available",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "apx-agents",
|
|
24
|
+
title: "Agentes APX",
|
|
25
|
+
source: "apx",
|
|
26
|
+
desktop: "ai",
|
|
27
|
+
kind: "agents",
|
|
28
|
+
status: "available",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "apx-notes",
|
|
32
|
+
title: "Notas APX",
|
|
33
|
+
source: "apx",
|
|
34
|
+
desktop: "project",
|
|
35
|
+
kind: "capture",
|
|
36
|
+
status: "available",
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const EXTERNAL_WIDGETS = [
|
|
41
|
+
["docker", "Docker", "infra"],
|
|
42
|
+
["dokploy", "Dokploy", "infra"],
|
|
43
|
+
["factorial", "Factorial", "work"],
|
|
44
|
+
["telegram", "Telegram", "comms"],
|
|
45
|
+
["gmail", "Gmail", "comms"],
|
|
46
|
+
["outlook", "Outlook", "comms"],
|
|
47
|
+
["teams", "Teams", "comms"],
|
|
48
|
+
["whatsapp", "WhatsApp", "comms"],
|
|
49
|
+
["zen", "Zen Browser", "ai"],
|
|
50
|
+
["claude", "Claude", "ai"],
|
|
51
|
+
["chatgpt", "ChatGPT", "ai"],
|
|
52
|
+
["cursor", "Cursor", "ai"],
|
|
53
|
+
["codex", "Codex", "ai"],
|
|
54
|
+
].map(([id, title, desktop]) => ({
|
|
55
|
+
id,
|
|
56
|
+
title,
|
|
57
|
+
source: "external",
|
|
58
|
+
desktop,
|
|
59
|
+
kind: "plugin",
|
|
60
|
+
status: "not_configured",
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
export const DESKTOPS = [
|
|
64
|
+
{ id: "general", title: "Hoy" },
|
|
65
|
+
{ id: "project", title: "Proyecto" },
|
|
66
|
+
{ id: "ai", title: "IA" },
|
|
67
|
+
{ id: "comms", title: "Comunicaciones" },
|
|
68
|
+
{ id: "infra", title: "Infra" },
|
|
69
|
+
{ id: "work", title: "Tiempo laboral" },
|
|
70
|
+
{ id: "plugins", title: "Plugins" },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
export const SAFE_ACTIONS = [
|
|
74
|
+
{
|
|
75
|
+
id: "apx.copy_context",
|
|
76
|
+
title: "Copiar contexto APX",
|
|
77
|
+
risk: "safe",
|
|
78
|
+
endpoint: "/projects/:pid/agents",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "apx.voice_turn",
|
|
82
|
+
title: "Hablar con APX",
|
|
83
|
+
risk: "safe",
|
|
84
|
+
endpoint: "/voice/turn",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "apx.super_agent",
|
|
88
|
+
title: "Pedir acción a APX",
|
|
89
|
+
risk: "confirm",
|
|
90
|
+
endpoint: "/projects/:pid/super-agent/chat",
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// Widget ids the user is allowed to override. Keeps a rogue client from
|
|
95
|
+
// writing arbitrary keys into the global config under deck.widget_overrides.
|
|
96
|
+
// CORE_WIDGETS are intentionally NOT in here — they're built-in APX surfaces
|
|
97
|
+
// and don't make sense to disable.
|
|
98
|
+
export const TOGGLEABLE_WIDGETS = new Set(EXTERNAL_WIDGETS.map((w) => w.id));
|
|
99
|
+
|
|
100
|
+
function pickActiveProject(projectList) {
|
|
101
|
+
return projectList.find((project) => Number(project.id) !== 0) || projectList[0] || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Apply runtime status + user overrides to the static EXTERNAL_WIDGETS list.
|
|
106
|
+
*
|
|
107
|
+
* 1. user explicitly disabled it → "disabled" (sticky, regardless of plugin
|
|
108
|
+
* auto-detect)
|
|
109
|
+
* 2. daemon has a running plugin → "available"
|
|
110
|
+
* 3. user toggled it on but no plugin backing → "configured"
|
|
111
|
+
* 4. nothing → leave the static "not_configured" default
|
|
112
|
+
*/
|
|
113
|
+
export function decorateExternalWidgets(pluginStatus = {}, overrides = {}) {
|
|
114
|
+
return EXTERNAL_WIDGETS.map((widget) => {
|
|
115
|
+
const override = overrides[widget.id];
|
|
116
|
+
const status = pluginStatus[widget.id];
|
|
117
|
+
const decorated = { ...widget };
|
|
118
|
+
if (status) decorated.daemon_status = status;
|
|
119
|
+
if (override?.enabled === false) {
|
|
120
|
+
decorated.status = "disabled";
|
|
121
|
+
} else if (status) {
|
|
122
|
+
decorated.status = status.enabled === false ? "disabled" : "available";
|
|
123
|
+
} else if (override?.enabled === true) {
|
|
124
|
+
decorated.status = "configured";
|
|
125
|
+
}
|
|
126
|
+
// Always echo the user-toggle so the app can render the switch
|
|
127
|
+
// independently of the running/available bit.
|
|
128
|
+
decorated.user_enabled = override?.enabled ?? null;
|
|
129
|
+
return decorated;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build the full /deck/manifest response body.
|
|
135
|
+
*
|
|
136
|
+
* Inputs are *resolved* runtime values, not the live managers — caller is
|
|
137
|
+
* responsible for catching errors in projects.list()/plugins.status() and
|
|
138
|
+
* passing the resulting arrays/maps in (or empty defaults).
|
|
139
|
+
*/
|
|
140
|
+
export function buildDeckManifest({
|
|
141
|
+
projectList = [],
|
|
142
|
+
pluginStatus = {},
|
|
143
|
+
overrides = {},
|
|
144
|
+
version,
|
|
145
|
+
startedAt,
|
|
146
|
+
config,
|
|
147
|
+
}) {
|
|
148
|
+
const activeProject = pickActiveProject(projectList);
|
|
149
|
+
return {
|
|
150
|
+
status: "ok",
|
|
151
|
+
daemon: {
|
|
152
|
+
name: "apx",
|
|
153
|
+
version,
|
|
154
|
+
host: config?.host || "127.0.0.1",
|
|
155
|
+
port: config?.port || 7430,
|
|
156
|
+
uptime_s: Math.round((Date.now() - startedAt) / 1000),
|
|
157
|
+
started_at: new Date(startedAt).toISOString(),
|
|
158
|
+
},
|
|
159
|
+
deck: {
|
|
160
|
+
name: "apx-deck",
|
|
161
|
+
desktops: DESKTOPS,
|
|
162
|
+
widgets: [...CORE_WIDGETS, ...decorateExternalWidgets(pluginStatus, overrides)],
|
|
163
|
+
suggested_actions: SAFE_ACTIONS,
|
|
164
|
+
},
|
|
165
|
+
apx: {
|
|
166
|
+
active_project: activeProject,
|
|
167
|
+
projects: projectList,
|
|
168
|
+
plugins: pluginStatus,
|
|
169
|
+
endpoints: {
|
|
170
|
+
health: "/health",
|
|
171
|
+
projects: "/projects",
|
|
172
|
+
plugins: "/plugins",
|
|
173
|
+
voice_turn: "/voice/turn",
|
|
174
|
+
transcribe_chunk: "/transcribe/chunk",
|
|
175
|
+
super_agent_chat: "/projects/:pid/super-agent/chat",
|
|
176
|
+
super_agent_stream: "/projects/:pid/super-agent/chat/stream",
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
safety: {
|
|
180
|
+
direct_shell: false,
|
|
181
|
+
arbitrary_commands: false,
|
|
182
|
+
dangerous_actions_require_confirmation: true,
|
|
183
|
+
allowed_actions_only: true,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Live model catalogs per engine. Wraps each provider's "list models" endpoint
|
|
2
|
+
// behind one signature: listModels(engine, baseUrl?, apiKey?) → { models } or
|
|
3
|
+
// { error }. Pure transport — no daemon dependencies. Both the daemon HTTP
|
|
4
|
+
// adapter and CLI commands can reuse this.
|
|
5
|
+
import { fetchJsonWithTimeout } from "./_health.js";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_BASE = {
|
|
8
|
+
openai: "https://api.openai.com/v1",
|
|
9
|
+
groq: "https://api.groq.com/openai/v1",
|
|
10
|
+
openrouter: "https://openrouter.ai/api/v1",
|
|
11
|
+
gemini: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
12
|
+
anthropic: "https://api.anthropic.com/v1",
|
|
13
|
+
ollama: "http://localhost:11434",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Gemini's native models endpoint returns a much richer catalog than the
|
|
17
|
+
// OpenAI-compat shim (which only echoes back a handful). We always query the
|
|
18
|
+
// native URL regardless of the user's configured base_url.
|
|
19
|
+
const GEMINI_NATIVE_BASE = "https://generativelanguage.googleapis.com/v1beta";
|
|
20
|
+
|
|
21
|
+
export async function listModels(engine, baseUrl, apiKey) {
|
|
22
|
+
const base = String(baseUrl || DEFAULT_BASE[engine] || "").replace(/\/$/, "");
|
|
23
|
+
|
|
24
|
+
if (engine === "ollama") {
|
|
25
|
+
const b = base || process.env.OLLAMA_HOST || "http://localhost:11434";
|
|
26
|
+
const r = await fetchJsonWithTimeout(`${b}/api/tags`, { timeoutMs: 2500 });
|
|
27
|
+
if (!r.ok) return { error: r.reason || "no se pudo contactar Ollama" };
|
|
28
|
+
const list = Array.isArray(r.json?.models) ? r.json.models : [];
|
|
29
|
+
return { models: list.map((m) => m?.name).filter((n) => typeof n === "string" && n) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (engine === "anthropic") {
|
|
33
|
+
if (!apiKey) return { error: "falta api_key" };
|
|
34
|
+
const b = base || DEFAULT_BASE.anthropic;
|
|
35
|
+
const r = await fetchJsonWithTimeout(`${b}/models?limit=100`, {
|
|
36
|
+
timeoutMs: 5000,
|
|
37
|
+
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
38
|
+
});
|
|
39
|
+
if (!r.ok) return { error: r.reason || `HTTP ${r.status}` };
|
|
40
|
+
const data = Array.isArray(r.json?.data) ? r.json.data : [];
|
|
41
|
+
return { models: data.map((m) => m?.id).filter(Boolean) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (engine === "gemini") {
|
|
45
|
+
if (!apiKey) return { error: "falta api_key" };
|
|
46
|
+
// Native Gemini API returns rich metadata, including supportedGenerationMethods
|
|
47
|
+
// so we can drop embeddings/vision-only entries. Names come back as
|
|
48
|
+
// "models/<id>"; strip the prefix.
|
|
49
|
+
const r = await fetchJsonWithTimeout(
|
|
50
|
+
`${GEMINI_NATIVE_BASE}/models?key=${encodeURIComponent(apiKey)}&pageSize=200`,
|
|
51
|
+
{ timeoutMs: 5000 },
|
|
52
|
+
);
|
|
53
|
+
if (!r.ok) return { error: r.reason || `HTTP ${r.status}` };
|
|
54
|
+
const data = Array.isArray(r.json?.models) ? r.json.models : [];
|
|
55
|
+
const models = data
|
|
56
|
+
.filter((m) => {
|
|
57
|
+
const methods = m?.supportedGenerationMethods;
|
|
58
|
+
if (!Array.isArray(methods)) return true;
|
|
59
|
+
return methods.includes("generateContent");
|
|
60
|
+
})
|
|
61
|
+
.map((m) => {
|
|
62
|
+
const name = typeof m?.name === "string" ? m.name : "";
|
|
63
|
+
return name.startsWith("models/") ? name.slice("models/".length) : name;
|
|
64
|
+
})
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
return { models };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// openai-compatible family: openai, groq, openrouter, azure, custom
|
|
70
|
+
if (!apiKey) return { error: "falta api_key" };
|
|
71
|
+
if (!base) return { error: "falta base_url" };
|
|
72
|
+
const r = await fetchJsonWithTimeout(`${base}/models`, {
|
|
73
|
+
timeoutMs: 5000,
|
|
74
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
75
|
+
});
|
|
76
|
+
if (!r.ok) return { error: r.reason || `HTTP ${r.status}` };
|
|
77
|
+
const data = Array.isArray(r.json?.data)
|
|
78
|
+
? r.json.data
|
|
79
|
+
: Array.isArray(r.json?.models)
|
|
80
|
+
? r.json.models
|
|
81
|
+
: [];
|
|
82
|
+
return { models: data.map((m) => m?.id || m?.name).filter(Boolean) };
|
|
83
|
+
}
|
|
@@ -51,15 +51,24 @@ function toGeminiContents(messages) {
|
|
|
51
51
|
if (m.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
|
|
52
52
|
out.push({
|
|
53
53
|
role: "model",
|
|
54
|
-
parts: m.tool_calls.map((tc) =>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
parts: m.tool_calls.map((tc) => {
|
|
55
|
+
const part = {
|
|
56
|
+
functionCall: {
|
|
57
|
+
name: tc.function?.name || tc.name,
|
|
58
|
+
args:
|
|
59
|
+
typeof tc.function?.arguments === "string"
|
|
60
|
+
? safeParseJson(tc.function.arguments)
|
|
61
|
+
: tc.function?.arguments || tc.arguments || {},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
// Gemini 3.x thinking models require us to echo back the
|
|
65
|
+
// thoughtSignature that came attached to the original functionCall
|
|
66
|
+
// part, or the API rejects the next turn with 400. We captured it
|
|
67
|
+
// in the response parser; replay it verbatim when present.
|
|
68
|
+
const sig = tc._thoughtSignature || tc.thought_signature;
|
|
69
|
+
if (sig) part.thoughtSignature = sig;
|
|
70
|
+
return part;
|
|
71
|
+
}),
|
|
63
72
|
});
|
|
64
73
|
continue;
|
|
65
74
|
}
|
|
@@ -143,14 +152,22 @@ export default {
|
|
|
143
152
|
for (const p of parts) {
|
|
144
153
|
const fc = p.functionCall || p.function_call;
|
|
145
154
|
if (fc?.name) {
|
|
146
|
-
|
|
155
|
+
const tc = {
|
|
147
156
|
id: `gemini_${randomUUID().slice(0, 8)}`,
|
|
148
157
|
type: "function",
|
|
149
158
|
function: {
|
|
150
159
|
name: fc.name,
|
|
151
160
|
arguments: typeof fc.args === "string" ? fc.args : JSON.stringify(fc.args || {}),
|
|
152
161
|
},
|
|
153
|
-
}
|
|
162
|
+
};
|
|
163
|
+
// Thinking models (Gemini 3.x) attach a thoughtSignature to the part
|
|
164
|
+
// alongside the functionCall. We must replay it on the next request
|
|
165
|
+
// or the API 400s. Carry it on the tool_call so the next call to
|
|
166
|
+
// toGeminiContents() can put it back. Underscore prefix marks it as
|
|
167
|
+
// adapter-private metadata other engines should ignore.
|
|
168
|
+
const sig = p.thoughtSignature || p.thought_signature;
|
|
169
|
+
if (sig) tc._thoughtSignature = sig;
|
|
170
|
+
toolCalls.push(tc);
|
|
154
171
|
}
|
|
155
172
|
}
|
|
156
173
|
|
|
@@ -52,12 +52,22 @@ export async function callEngine({ modelId, system, messages, config, temperatur
|
|
|
52
52
|
const { provider, model } = resolveProvider(modelId);
|
|
53
53
|
const adapter = getAdapter(provider);
|
|
54
54
|
const providerCfg = (config && config.engines && config.engines[provider]) || {};
|
|
55
|
+
// The per-provider `default_max_tokens` set in the web admin (Provider modal
|
|
56
|
+
// slider) acts as a floor: callers may ask for more, but never less. This
|
|
57
|
+
// matters for "thinking" models (e.g. Gemini 3.x) whose internal reasoning
|
|
58
|
+
// tokens count against maxOutputTokens — too low a cap and the visible reply
|
|
59
|
+
// gets truncated mid-sentence. Fallback chain:
|
|
60
|
+
// caller value → provider cfg → 2048 (safe baseline that survives thinking
|
|
61
|
+
// models without truncating; non-thinking models just don't fill it).
|
|
62
|
+
const providerCap = Number(providerCfg.default_max_tokens) || 0;
|
|
63
|
+
const callerCap = Number(maxTokens) || 0;
|
|
64
|
+
const effectiveMaxTokens = Math.max(callerCap, providerCap) || 2048;
|
|
55
65
|
return adapter.chat({
|
|
56
66
|
system,
|
|
57
67
|
messages,
|
|
58
68
|
model,
|
|
59
69
|
temperature,
|
|
60
|
-
maxTokens,
|
|
70
|
+
maxTokens: effectiveMaxTokens,
|
|
61
71
|
tools,
|
|
62
72
|
toolChoice,
|
|
63
73
|
config: providerCfg,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Backend strings — English (en).
|
|
2
|
+
export default {
|
|
3
|
+
"telegram.heads_up": "On it — working on that… 🛠️",
|
|
4
|
+
"telegram.reset_ack": "Done, context cleared. Starting fresh. What do you need?",
|
|
5
|
+
"telegram.error_generic": "Something broke on my side — already logged.",
|
|
6
|
+
"telegram.fallback_listo": "Done.",
|
|
7
|
+
|
|
8
|
+
"common.unknown_error": "Something went wrong.",
|
|
9
|
+
};
|