@agentprojectcontext/apx 1.31.2 → 1.32.2
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 +6 -1
- package/skills/apc-context/SKILL.md +5 -2
- package/skills/apx/SKILL.md +3 -3
- package/skills/apx-agency-agents/SKILL.md +5 -5
- package/skills/apx-agent/SKILL.md +7 -7
- package/skills/apx-mcp/SKILL.md +6 -4
- package/skills/apx-mcp-builder/SKILL.md +4 -7
- package/skills/apx-project/SKILL.md +4 -5
- package/skills/apx-routine/SKILL.md +14 -12
- package/skills/apx-runtime/SKILL.md +5 -3
- package/skills/apx-sessions/SKILL.md +5 -5
- package/skills/apx-skill-builder/SKILL.md +10 -6
- package/skills/apx-task/SKILL.md +8 -8
- package/skills/apx-telegram/SKILL.md +23 -7
- package/skills/apx-voice/SKILL.md +8 -6
- package/src/core/{agent-system.js → agent/build-agent-system.js} +10 -12
- package/src/core/agent/constants.js +5 -0
- package/src/core/agent/index.js +0 -2
- package/src/core/{agent-memory.js → agent/memory.js} +2 -2
- package/src/core/agent/model-router.js +21 -43
- package/src/core/agent/prompt-builder.js +17 -63
- package/src/core/agent/prompts/action-discipline.md +17 -0
- package/src/core/agent/prompts/channels/code.md +8 -12
- package/src/core/agent/prompts/channels/desktop.md +6 -4
- package/src/core/agent/prompts/channels/routine.md +10 -1
- package/src/core/agent/prompts/channels/telegram.md +5 -0
- package/src/core/agent/prompts/channels/web_code.md +20 -0
- package/src/core/agent/prompts/modes/voice.md +2 -2
- package/src/core/agent/prompts/super-agent-base.md +2 -2
- package/src/core/agent/run-agent.js +66 -36
- package/src/core/agent/runtime-bridge.js +42 -0
- package/src/core/agent/self-memory.js +19 -9
- package/src/core/agent/skills/catalog.js +65 -0
- package/src/core/agent/skills/index.js +6 -0
- package/src/{host/daemon/skills-loader.js → core/agent/skills/loader.js} +3 -3
- package/src/core/agent/skills/rag.js +91 -0
- package/src/core/agent/skills/trigger.js +71 -0
- package/src/{host/daemon → core/agent}/super-agent.js +5 -5
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/add-project.js +3 -4
- package/src/core/agent/tools/handlers/ask-questions.js +115 -0
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-agent.js +2 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-mcp.js +1 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/call-runtime.js +10 -11
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/create-task.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/discover-tools.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/edit-file.js +1 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/import-agent.js +4 -5
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-agents.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-skills.js +7 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-tasks.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-vault-agents.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/load-skill.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-agent-memory.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-self-memory.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/remember.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/run-shell.js +1 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-messages.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-sessions.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/send-telegram.js +0 -2
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-identity.js +1 -3
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/set-permission-mode.js +1 -3
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/tail-messages.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/transcribe-audio.js +1 -1
- package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/write-file.js +1 -2
- package/src/core/agent/tools/helpers.js +74 -0
- package/src/{host/daemon/super-agent-tools → core/agent/tools}/registry-bridge.js +3 -3
- package/src/{host/daemon/super-agent-tools/index.js → core/agent/tools/registry.js} +31 -32
- package/src/core/apc/agents-vault.js +37 -0
- package/src/core/{scaffold.js → apc/scaffold.js} +4 -5
- package/src/core/{config.js → config/index.js} +21 -27
- package/src/core/config/paths.js +32 -0
- package/src/core/constants/actors.js +8 -0
- package/src/core/constants/channels.js +19 -0
- package/src/core/constants/index.js +5 -0
- package/src/core/constants/permissions.js +17 -0
- package/src/core/constants/roles.js +9 -0
- package/src/core/engines/_streaming.js +63 -0
- package/src/core/engines/anthropic.js +11 -22
- package/src/core/engines/ollama.js +7 -16
- package/src/core/identity/index.js +8 -0
- package/src/core/{identity.js → identity/self.js} +5 -5
- package/src/core/{telegram-identity.js → identity/telegram.js} +1 -1
- package/src/core/logging.js +1 -1
- package/src/core/mascot.js +1 -1
- package/src/core/memory/active-threads.js +10 -10
- package/src/core/memory/broker.js +9 -9
- package/src/core/memory/compactor.js +2 -2
- package/src/core/memory/index.js +2 -2
- package/src/core/memory/indexer.js +1 -1
- package/src/core/{code-sessions-store.js → stores/code-sessions.js} +3 -7
- package/src/core/{messages-store.js → stores/messages.js} +6 -4
- package/src/core/stores/routine-memory.js +71 -0
- package/src/core/{routines-store.js → stores/routines.js} +1 -3
- package/src/core/stores/runtime-sessions.js +99 -0
- package/src/core/{tasks-store.js → stores/tasks.js} +3 -8
- package/src/core/update-check.js +1 -1
- package/src/core/util/ids.js +14 -0
- package/src/core/util/index.js +2 -0
- package/src/core/util/text-similarity.js +52 -0
- package/src/core/util/time.js +9 -0
- package/src/core/voice/tts.js +1 -1
- package/src/host/daemon/api/admin-config.js +4 -3
- package/src/host/daemon/api/admin.js +1 -1
- package/src/host/daemon/api/agents.js +4 -25
- package/src/host/daemon/api/artifacts.js +118 -1
- package/src/host/daemon/api/code.js +60 -16
- package/src/host/daemon/api/confirm.js +1 -1
- package/src/host/daemon/api/connections.js +2 -2
- package/src/host/daemon/api/conversations.js +2 -2
- package/src/host/daemon/api/deck.js +1 -1
- package/src/host/daemon/api/desktop.js +1 -1
- package/src/host/daemon/api/embeddings.js +4 -4
- package/src/host/daemon/api/engines.js +2 -2
- package/src/host/daemon/api/exec.js +3 -3
- package/src/host/daemon/api/identity.js +1 -1
- package/src/host/daemon/api/mcps.js +1 -1
- package/src/host/daemon/api/messages.js +1 -1
- package/src/host/daemon/api/runtimes.js +9 -8
- package/src/host/daemon/api/sessions-search.js +1 -1
- package/src/host/daemon/api/sessions.js +2 -2
- package/src/host/daemon/api/shared.js +5 -4
- package/src/host/daemon/api/skills.js +30 -0
- package/src/host/daemon/api/super-agent.js +29 -9
- package/src/host/daemon/api/tasks.js +2 -2
- package/src/host/daemon/api/telegram.js +1 -1
- package/src/host/daemon/api/tools.js +6 -6
- package/src/host/daemon/api/tts.js +2 -2
- package/src/host/daemon/api/voice.js +14 -12
- package/src/host/daemon/api.js +2 -0
- package/src/host/daemon/compact.js +1 -1
- package/src/host/daemon/db.js +4 -4
- package/src/host/daemon/desktop-ws.js +1 -1
- package/src/host/daemon/index.js +4 -4
- package/src/host/daemon/plugins/{desktop.js → desktop/index.js} +45 -6
- package/src/host/daemon/plugins/index.js +2 -2
- package/src/host/daemon/plugins/telegram/ask.js +309 -0
- package/src/host/daemon/plugins/{telegram.js → telegram/index.js} +390 -191
- package/src/host/daemon/plugins/telegram/media.js +162 -0
- package/src/host/daemon/projects-helpers.js +54 -0
- package/src/host/daemon/routines.js +28 -12
- package/src/host/daemon/smoke.js +2 -2
- package/src/host/daemon/token-store.js +1 -1
- package/src/host/daemon/transcription.js +2 -2
- package/src/host/daemon/wakeup.js +2 -2
- package/src/interfaces/cli/commands/agent.js +3 -3
- package/src/interfaces/cli/commands/artifact.js +99 -0
- package/src/interfaces/cli/commands/command.js +1 -1
- package/src/interfaces/cli/commands/config.js +3 -2
- package/src/interfaces/cli/commands/desktop.js +1 -1
- package/src/interfaces/cli/commands/exec.js +2 -1
- package/src/interfaces/cli/commands/identity.js +2 -2
- package/src/interfaces/cli/commands/init.js +1 -1
- package/src/interfaces/cli/commands/mcp.js +1 -1
- package/src/interfaces/cli/commands/memory.js +2 -2
- package/src/interfaces/cli/commands/model.js +16 -6
- package/src/interfaces/cli/commands/project.js +1 -1
- package/src/interfaces/cli/commands/routine.js +58 -0
- package/src/interfaces/cli/commands/search.js +1 -1
- package/src/interfaces/cli/commands/session.js +4 -4
- package/src/interfaces/cli/commands/setup.js +4 -3
- package/src/interfaces/cli/commands/skills.js +25 -4
- package/src/interfaces/cli/commands/status.js +1 -1
- package/src/interfaces/cli/commands/sys.js +11 -4
- package/src/interfaces/cli/commands/update.js +1 -1
- package/src/interfaces/cli/index.js +8 -4
- package/src/interfaces/cli/postinstall.js +2 -2
- package/src/interfaces/cli/terminal-chat/renderer.js +22 -2
- package/src/interfaces/mcp-server/index.js +1 -1
- package/src/interfaces/tui/component/prompt/index.tsx +3 -1
- package/src/interfaces/tui/context/sdk-apx.tsx +47 -7
- package/src/interfaces/tui/context/sync-apx.tsx +20 -2
- package/src/interfaces/tui/context/sync.tsx +2 -1
- package/src/interfaces/tui/routes/session/index.tsx +151 -136
- package/src/interfaces/tui/routes/session/sidebar-apx.tsx +37 -15
- package/src/interfaces/tui/run.ts +2 -0
- package/src/interfaces/web/dist/assets/index-34U_Mp1M.css +1 -0
- package/src/interfaces/web/dist/assets/index-BkybwwRn.js +570 -0
- package/src/interfaces/web/dist/assets/index-BkybwwRn.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +9 -9
- package/src/interfaces/web/src/App.tsx +51 -32
- package/src/interfaces/web/src/components/RobyBubble.tsx +12 -6
- package/src/interfaces/web/src/components/UiSelect.tsx +1 -1
- package/src/interfaces/web/src/components/chat/AskQuestionsCard.tsx +72 -0
- package/src/interfaces/web/src/components/chat/InlineAskPanel.tsx +399 -0
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
- package/src/interfaces/web/src/components/chat/MessageList.tsx +2 -1
- package/src/interfaces/web/src/components/chat/SkillPicker.tsx +77 -0
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +230 -0
- package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +1 -1
- package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +40 -17
- package/src/interfaces/web/src/components/common/TabLayout.tsx +9 -5
- package/src/interfaces/web/src/components/common/TabNav.tsx +3 -3
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +4 -2
- package/src/interfaces/web/src/hooks/useChat.ts +47 -2
- package/src/interfaces/web/src/hooks/useNavCollapseCtx.tsx +59 -0
- package/src/interfaces/web/src/hooks/usePersonaName.ts +11 -0
- package/src/interfaces/web/src/i18n/en.ts +27 -7
- package/src/interfaces/web/src/i18n/es.ts +27 -7
- package/src/interfaces/web/src/lib/api/artifacts.ts +47 -0
- package/src/interfaces/web/src/lib/api/skills.ts +25 -0
- package/src/interfaces/web/src/lib/api.ts +2 -0
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +41 -20
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +5 -18
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +1 -8
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +39 -40
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +27 -9
- package/src/skills/apc-context/SKILL.md +159 -0
- package/src/core/agent/ghost-guard.js +0 -24
- package/src/core/agent/prompts/channels/terminal.md +0 -16
- package/src/host/daemon/apc-runtime-context.js +0 -124
- package/src/host/daemon/super-agent-tools/helpers.js +0 -124
- package/src/host/daemon/super-agent-tools/tools/ask-questions.js +0 -32
- package/src/host/daemon/tool-call-parser.js +0 -2
- package/src/interfaces/web/dist/assets/index-BDUsA6L6.css +0 -1
- package/src/interfaces/web/dist/assets/index-BV615I9p.js +0 -548
- package/src/interfaces/web/dist/assets/index-BV615I9p.js.map +0 -1
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-files.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-mcps.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/list-projects.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/read-file.js +0 -0
- /package/src/{host/daemon/super-agent-tools/tools → core/agent/tools/handlers}/search-files.js +0 -0
- /package/src/core/agent/{pseudo-tools.js → tools/pseudo-tools.js} +0 -0
- /package/src/core/agent/{tool-call-parser.js → tools/tool-call-parser.js} +0 -0
- /package/src/core/{parser.js → apc/parser.js} +0 -0
- /package/src/core/{apc-skill-sync.js → apc/skill-sync.js} +0 -0
- /package/src/core/{artifacts-store.js → stores/artifacts.js} +0 -0
- /package/src/{host/daemon → core/stores}/engine-sessions.js +0 -0
- /package/src/core/{session-store.js → stores/sessions.js} +0 -0
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
// search / glob / grep → filesystem-bounded
|
|
5
5
|
// registry → /:name wildcard, MOUNT LAST so it
|
|
6
6
|
// doesn't shadow the specific paths
|
|
7
|
-
import { buildBrowserRouter } from "
|
|
8
|
-
import { buildFetchRouter } from "
|
|
9
|
-
import { buildSearchRouter } from "
|
|
10
|
-
import { buildRegistryRouter } from "
|
|
11
|
-
import { buildGlobRouter } from "
|
|
12
|
-
import { buildGrepRouter } from "
|
|
7
|
+
import { buildBrowserRouter } from "#core/tools/browser.js";
|
|
8
|
+
import { buildFetchRouter } from "#core/tools/fetch.js";
|
|
9
|
+
import { buildSearchRouter } from "#core/tools/search.js";
|
|
10
|
+
import { buildRegistryRouter } from "#core/tools/registry.js";
|
|
11
|
+
import { buildGlobRouter } from "#core/tools/glob.js";
|
|
12
|
+
import { buildGrepRouter } from "#core/tools/grep.js";
|
|
13
13
|
|
|
14
14
|
export function register(app, { express, projects, registries }) {
|
|
15
15
|
app.use("/tools/fetch", buildFetchRouter(express));
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
//
|
|
9
9
|
// Audio files land under ~/.apx/tmp/tts/<uuid>.<ext>. The caller (CLI,
|
|
10
10
|
// Telegram plugin, overlay) is responsible for picking them up.
|
|
11
|
-
import { synthesize, listProviders } from "
|
|
12
|
-
import { readConfig } from "
|
|
11
|
+
import { synthesize, listProviders } from "#core/voice/tts.js";
|
|
12
|
+
import { readConfig } from "#core/config/index.js";
|
|
13
13
|
|
|
14
14
|
export function register(app) {
|
|
15
15
|
app.post("/tts/say", async (req, res) => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// eslint-disable-next-line -- import below
|
|
2
|
+
import { CHANNELS } from "#core/constants/channels.js";
|
|
1
3
|
// Daemon HTTP routes for the unified "voice" channel.
|
|
2
4
|
//
|
|
3
5
|
// POST /voice/turn { audio?: <base64 or path>, format?, text?, agent?,
|
|
@@ -19,12 +21,12 @@ import fs from "node:fs";
|
|
|
19
21
|
import path from "node:path";
|
|
20
22
|
import os from "node:os";
|
|
21
23
|
import { randomUUID } from "node:crypto";
|
|
22
|
-
import { readConfig } from "
|
|
23
|
-
import { synthesize } from "
|
|
24
|
+
import { readConfig } from "#core/config/index.js";
|
|
25
|
+
import { synthesize } from "#core/voice/tts.js";
|
|
24
26
|
import { transcribe } from "../transcription.js";
|
|
25
|
-
import { runSuperAgent, isSuperAgentEnabled } from "
|
|
26
|
-
import { appendGlobalMessage } from "
|
|
27
|
-
import { appendErrorTrace, previewText } from "
|
|
27
|
+
import { runSuperAgent, isSuperAgentEnabled } from "#core/agent/super-agent.js";
|
|
28
|
+
import { appendGlobalMessage } from "#core/stores/messages.js";
|
|
29
|
+
import { appendErrorTrace, previewText } from "#core/logging.js";
|
|
28
30
|
|
|
29
31
|
// ── Channel-aware pre-processor ────────────────────────────────────
|
|
30
32
|
//
|
|
@@ -84,14 +86,14 @@ function buildChannelContext(channel, { projectId, language = "es" } = {}) {
|
|
|
84
86
|
const dynamicNote = `${langDirective}${projectHint}`;
|
|
85
87
|
switch (channel) {
|
|
86
88
|
case "voice":
|
|
87
|
-
return { ...base, contextNote: dynamicNote, systemSuffix: SUGGESTIONS_INSTRUCTION, wantsSuggestions: true, channel:
|
|
89
|
+
return { ...base, contextNote: dynamicNote, systemSuffix: SUGGESTIONS_INSTRUCTION, wantsSuggestions: true, channel: CHANNELS.DECK, channelMeta: { voice: true } };
|
|
88
90
|
case "deck":
|
|
89
|
-
return { ...base, contextNote: dynamicNote, systemSuffix: SUGGESTIONS_INSTRUCTION, wantsSuggestions: true, channel:
|
|
91
|
+
return { ...base, contextNote: dynamicNote, systemSuffix: SUGGESTIONS_INSTRUCTION, wantsSuggestions: true, channel: CHANNELS.DECK, channelMeta: {} };
|
|
90
92
|
case "desktop":
|
|
91
|
-
return { ...base, contextNote: dynamicNote, systemSuffix: SUGGESTIONS_INSTRUCTION, wantsSuggestions: true, channel:
|
|
93
|
+
return { ...base, contextNote: dynamicNote, systemSuffix: SUGGESTIONS_INSTRUCTION, wantsSuggestions: true, channel: CHANNELS.DESKTOP, channelMeta: { voice: true } };
|
|
92
94
|
case "telegram":
|
|
93
95
|
// Format rules live in channels/telegram.md; keep only the dynamic note.
|
|
94
|
-
return { ...base, contextNote: dynamicNote, channel:
|
|
96
|
+
return { ...base, contextNote: dynamicNote, channel: CHANNELS.TELEGRAM, channelMeta: {} };
|
|
95
97
|
default:
|
|
96
98
|
return { ...base, contextNote: dynamicNote, channel: channel || "api", channelMeta: {} };
|
|
97
99
|
}
|
|
@@ -232,7 +234,7 @@ async function tryVoiceTaskIntent({ projects, userText, hintProjectId }) {
|
|
|
232
234
|
};
|
|
233
235
|
}
|
|
234
236
|
try {
|
|
235
|
-
const { createTask } = await import("
|
|
237
|
+
const { createTask } = await import("#core/stores/tasks.js");
|
|
236
238
|
const task = createTask(project.storagePath, {
|
|
237
239
|
title,
|
|
238
240
|
source: "voice",
|
|
@@ -511,8 +513,8 @@ export function register(app, { projects, plugins, registries }) {
|
|
|
511
513
|
}
|
|
512
514
|
|
|
513
515
|
// Note for plugin authors:
|
|
514
|
-
// Desktop (src/host/daemon/plugins/desktop.js) and Telegram
|
|
515
|
-
// (src/host/daemon/plugins/telegram.js) currently implement their own
|
|
516
|
+
// Desktop (src/host/daemon/plugins/desktop/index.js) and Telegram
|
|
517
|
+
// (src/host/daemon/plugins/telegram/index.js) currently implement their own
|
|
516
518
|
// STT → agent → render pipelines. To get spoken replies via APX they can
|
|
517
519
|
// POST to /voice/turn (or call `synthesize()` directly) instead of
|
|
518
520
|
// re-implementing TTS. This module intentionally does NOT migrate them —
|
package/src/host/daemon/api.js
CHANGED
|
@@ -22,6 +22,7 @@ import { register as registerMessages } from "./api/messages.js";
|
|
|
22
22
|
import { register as registerTelegram } from "./api/telegram.js";
|
|
23
23
|
import { register as registerPlugins } from "./api/plugins.js";
|
|
24
24
|
import { register as registerEngines } from "./api/engines.js";
|
|
25
|
+
import { register as registerSkills } from "./api/skills.js";
|
|
25
26
|
import { register as registerExec } from "./api/exec.js";
|
|
26
27
|
import { register as registerSuperAgent } from "./api/super-agent.js";
|
|
27
28
|
import { register as registerCode } from "./api/code.js";
|
|
@@ -107,6 +108,7 @@ export function buildApi({
|
|
|
107
108
|
registerMcps(app, ctx);
|
|
108
109
|
registerMessages(app, ctx);
|
|
109
110
|
registerEngines(app, ctx);
|
|
111
|
+
registerSkills(app, ctx);
|
|
110
112
|
registerExec(app, ctx);
|
|
111
113
|
registerSuperAgent(app, ctx);
|
|
112
114
|
registerConfirm(app, ctx);
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import fs from "node:fs";
|
|
26
26
|
import path from "node:path";
|
|
27
27
|
import { parseConversation } from "./conversations.js";
|
|
28
|
-
import { callEngine } from "
|
|
28
|
+
import { callEngine } from "#core/engines/index.js";
|
|
29
29
|
|
|
30
30
|
const KEEP_LAST = 6;
|
|
31
31
|
|
package/src/host/daemon/db.js
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
// Projects are identified by path; no SQLite — filesystem is the source of truth.
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { appendMessageToFs } from "
|
|
5
|
+
import { appendMessageToFs } from "#core/stores/messages.js";
|
|
6
6
|
import { effectiveConfig } from "./project-config.js";
|
|
7
|
-
import { readAgents } from "
|
|
8
|
-
import { getOrCreateApxId } from "
|
|
7
|
+
import { readAgents } from "#core/apc/parser.js";
|
|
8
|
+
import { getOrCreateApxId } from "#core/apc/scaffold.js";
|
|
9
9
|
import {
|
|
10
10
|
ensureProjectStorage,
|
|
11
11
|
DEFAULT_PROJECT_ID,
|
|
12
12
|
DEFAULT_PROJECT_STORE,
|
|
13
|
-
} from "
|
|
13
|
+
} from "#core/config/index.js";
|
|
14
14
|
|
|
15
15
|
export class ProjectManager {
|
|
16
16
|
constructor(globalConfig = {}) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Singleton WebSocket hub for the desktop channel.
|
|
2
|
-
// Imported by api.js (to register connections) and by plugins/desktop.js (to broadcast).
|
|
2
|
+
// Imported by api.js (to register connections) and by plugins/desktop/index.js (to broadcast).
|
|
3
3
|
|
|
4
4
|
const _clients = new Set(); // Set<WebSocket>
|
|
5
5
|
let _messageHandler = null; // (ws, data) => void — set by desktop plugin
|
package/src/host/daemon/index.js
CHANGED
|
@@ -14,17 +14,17 @@ import {
|
|
|
14
14
|
LOG_PATH,
|
|
15
15
|
APX_HOME,
|
|
16
16
|
TOKEN_PATH,
|
|
17
|
-
} from "
|
|
17
|
+
} from "#core/config/index.js";
|
|
18
18
|
import { ProjectManager } from "./db.js";
|
|
19
|
-
import { McpRegistry } from "
|
|
19
|
+
import { McpRegistry } from "#core/mcp/runner.js";
|
|
20
20
|
import { PluginManager } from "./plugins/index.js";
|
|
21
21
|
import { RoutineScheduler } from "./routines.js";
|
|
22
22
|
import { buildApi } from "./api.js";
|
|
23
23
|
import { createTokenStore } from "./token-store.js";
|
|
24
24
|
import { triggerWakeup } from "./wakeup.js";
|
|
25
25
|
import { registerDesktopClient } from "./desktop-ws.js";
|
|
26
|
-
import { log as logToUnified } from "
|
|
27
|
-
import { initMemory, stopMemory } from "
|
|
26
|
+
import { log as logToUnified } from "#core/logging.js";
|
|
27
|
+
import { initMemory, stopMemory } from "#core/memory/index.js";
|
|
28
28
|
|
|
29
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
30
30
|
const __dirname = path.dirname(__filename);
|
|
@@ -21,11 +21,13 @@ import {
|
|
|
21
21
|
broadcastDesktop,
|
|
22
22
|
sendToClient,
|
|
23
23
|
setDesktopMessageHandler,
|
|
24
|
-
} from "
|
|
25
|
-
import { runSuperAgent, isSuperAgentEnabled } from "
|
|
26
|
-
import { appendGlobalMessage } from "
|
|
24
|
+
} from "../../desktop-ws.js";
|
|
25
|
+
import { runSuperAgent, isSuperAgentEnabled } from "#core/agent/super-agent.js";
|
|
26
|
+
import { appendGlobalMessage } from "#core/stores/messages.js";
|
|
27
|
+
import { CHANNELS } from "#core/constants/channels.js";
|
|
28
|
+
import { tryResolveSkillCommand } from "#core/agent/skills/trigger.js";
|
|
27
29
|
|
|
28
|
-
const CHANNEL =
|
|
30
|
+
const CHANNEL = CHANNELS.DESKTOP;
|
|
29
31
|
|
|
30
32
|
export default {
|
|
31
33
|
id: "desktop",
|
|
@@ -128,12 +130,15 @@ async function _handleMessage({ ws, text, previousMessages }, { projects, config
|
|
|
128
130
|
|
|
129
131
|
log(`desktop: super-agent turn start — model=${cfg.model || config?.super_agent?.model || "(default)"} text="${text.slice(0, 60)}"`);
|
|
130
132
|
const t0 = Date.now();
|
|
133
|
+
const slashed = tryResolveSkillCommand(text);
|
|
134
|
+
const slashedPrompt = slashed.handled ? slashed.prompt : text;
|
|
131
135
|
const result = await runSuperAgent({
|
|
132
136
|
globalConfig: config,
|
|
133
137
|
projects,
|
|
134
138
|
plugins,
|
|
135
|
-
prompt:
|
|
136
|
-
channel:
|
|
139
|
+
prompt: slashedPrompt,
|
|
140
|
+
channel: CHANNELS.DESKTOP,
|
|
141
|
+
...(slashed.handled ? { contextNote: slashed.contextNote } : {}),
|
|
137
142
|
channelMeta: { voice: true }, // desktop module is voice-first → spoken mode
|
|
138
143
|
previousMessages: history.slice(0, -1),
|
|
139
144
|
overrideModel: cfg.model || null,
|
|
@@ -146,6 +151,15 @@ async function _handleMessage({ ws, text, previousMessages }, { projects, config
|
|
|
146
151
|
_send(ws, { type: "tool_start", name: t.tool, args: t.args });
|
|
147
152
|
} else if (event.type === "tool_result") {
|
|
148
153
|
_send(ws, { type: "tool_done", name: event.trace.tool });
|
|
154
|
+
// ask_questions on desktop is voice-first: there's no inline-keyboard
|
|
155
|
+
// UI to render, so we turn the structured questions into a spoken
|
|
156
|
+
// segment. The user voice-replies on the next turn and the super-agent
|
|
157
|
+
// sees that reply in its history. Each option is announced inline so
|
|
158
|
+
// TTS reads them aloud naturally.
|
|
159
|
+
if (event.trace?.tool === "ask_questions") {
|
|
160
|
+
const segments = formatAskQuestionsForVoice(event.trace.args?.questions);
|
|
161
|
+
if (segments) emitSegment(segments);
|
|
162
|
+
}
|
|
149
163
|
} else if (event.type === "assistant_text" && event.text) {
|
|
150
164
|
// A complete assistant text segment (e.g. the "I'll check…" intro
|
|
151
165
|
// emitted right before a tool runs). Ship it as its own message.
|
|
@@ -193,6 +207,31 @@ async function _handleMessage({ ws, text, previousMessages }, { projects, config
|
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
209
|
|
|
210
|
+
// Build a voice-friendly transcript of an ask_questions tool call so the
|
|
211
|
+
// desktop's TTS reads the prompt aloud and the bubble shows what was asked.
|
|
212
|
+
// Single question + options reads as "<question> Opciones: A; B; C."
|
|
213
|
+
// Multiple questions are numbered. Free-text questions just speak the prompt.
|
|
214
|
+
function formatAskQuestionsForVoice(raw) {
|
|
215
|
+
if (!Array.isArray(raw) || raw.length === 0) return null;
|
|
216
|
+
const lines = [];
|
|
217
|
+
raw.forEach((rawQ, idx) => {
|
|
218
|
+
const q = typeof rawQ === "string" ? { question: rawQ } : (rawQ || {});
|
|
219
|
+
const text = typeof q.question === "string" ? q.question.trim() : "";
|
|
220
|
+
if (!text) return;
|
|
221
|
+
const prefix = raw.length > 1 ? `${idx + 1}. ` : "";
|
|
222
|
+
const opts = Array.isArray(q.options) ? q.options : [];
|
|
223
|
+
const optLabels = opts
|
|
224
|
+
.map((o) => (typeof o === "string" ? o : (o && typeof o.label === "string" ? o.label : "")))
|
|
225
|
+
.filter(Boolean);
|
|
226
|
+
let line = `${prefix}${text}`;
|
|
227
|
+
if (optLabels.length > 0) {
|
|
228
|
+
line += ` Opciones: ${optLabels.join("; ")}.`;
|
|
229
|
+
}
|
|
230
|
+
lines.push(line);
|
|
231
|
+
});
|
|
232
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
196
235
|
function _send(ws, msg) {
|
|
197
236
|
if (ws) {
|
|
198
237
|
sendToClient(ws, msg);
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
//
|
|
13
13
|
// Plugins are discovered by static import here. Adding a new plugin = importing
|
|
14
14
|
// it and pushing into PLUGINS.
|
|
15
|
-
import telegramPlugin from "./telegram.js";
|
|
16
|
-
import desktopPlugin from "./desktop.js";
|
|
15
|
+
import telegramPlugin from "./telegram/index.js";
|
|
16
|
+
import desktopPlugin from "./desktop/index.js";
|
|
17
17
|
|
|
18
18
|
export const PLUGINS = [telegramPlugin, desktopPlugin];
|
|
19
19
|
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Telegram ask_questions integration.
|
|
2
|
+
//
|
|
3
|
+
// When the super-agent ends a turn with an `ask_questions` tool call, the
|
|
4
|
+
// telegram plugin calls into this module instead of sending the bare reply
|
|
5
|
+
// text. We render each question as a Telegram message with an inline keyboard
|
|
6
|
+
// (one button per option, plus skip/cancel), keep the in-flight state in
|
|
7
|
+
// memory keyed by chat_id, and resume by feeding the compiled answers back
|
|
8
|
+
// to the super-agent as a synthetic user prompt.
|
|
9
|
+
//
|
|
10
|
+
// State is intentionally process-local: an ask flow that started before a
|
|
11
|
+
// daemon restart simply dies; the user can re-issue the original prompt.
|
|
12
|
+
|
|
13
|
+
import { performance } from "node:perf_hooks";
|
|
14
|
+
|
|
15
|
+
const ASK_TTL_MS = 30 * 60_000; // 30 min — abandoned flows GC'd after this
|
|
16
|
+
|
|
17
|
+
const STORE = new Map(); // chat_id (string) → AskState
|
|
18
|
+
|
|
19
|
+
// AskState shape:
|
|
20
|
+
// {
|
|
21
|
+
// chatId, projectId, authorId,
|
|
22
|
+
// correlationId, // short id used in callback_data to dedupe restarts
|
|
23
|
+
// questions: AskQuestion[],
|
|
24
|
+
// answers: { picked: Set<number>, text: string, skipped: boolean }[],
|
|
25
|
+
// index: number,
|
|
26
|
+
// messageId: number|null, // last sent question message (for edit/disable)
|
|
27
|
+
// createdAt, lastTouchedAt,
|
|
28
|
+
// resume: (compiled: string) => Promise<void>, // called when flow completes
|
|
29
|
+
// }
|
|
30
|
+
|
|
31
|
+
function emptyAnswer() {
|
|
32
|
+
return { picked: new Set(), text: "", skipped: false };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function genCorrelationId() {
|
|
36
|
+
// Time-derived monotonically-ish id, kept short for Telegram's 64-byte
|
|
37
|
+
// callback_data limit. No Date.now() in workflows but we're in normal Node.
|
|
38
|
+
return Math.floor(performance.now() * 1000).toString(36) + Math.floor(Math.random() * 36 ** 4).toString(36);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Normalize whatever shape the model passed (strings or {question,...} objs)
|
|
42
|
+
// into the canonical question record. Identical contract to the web side
|
|
43
|
+
// (InlineAskPanel.tsx normalizeQuestionClient).
|
|
44
|
+
export function normalizeQuestion(q) {
|
|
45
|
+
if (typeof q === "string") {
|
|
46
|
+
return { question: q, options: [], multiSelect: false, allowText: true };
|
|
47
|
+
}
|
|
48
|
+
if (!q || typeof q !== "object") return null;
|
|
49
|
+
const text = typeof q.question === "string" ? q.question : "";
|
|
50
|
+
if (!text) return null;
|
|
51
|
+
const rawOptions = Array.isArray(q.options) ? q.options : [];
|
|
52
|
+
const options = rawOptions
|
|
53
|
+
.map((o) => {
|
|
54
|
+
if (typeof o === "string") return { label: o };
|
|
55
|
+
if (o && typeof o === "object" && typeof o.label === "string") {
|
|
56
|
+
return {
|
|
57
|
+
label: o.label,
|
|
58
|
+
description: typeof o.description === "string" ? o.description : undefined,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
})
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
return {
|
|
65
|
+
question: text,
|
|
66
|
+
header: typeof q.header === "string" ? q.header : undefined,
|
|
67
|
+
options,
|
|
68
|
+
multiSelect: q.multiSelect === true,
|
|
69
|
+
allowText: q.allowText === false ? false : true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Pull the most recent ask_questions tool call out of a super-agent trace.
|
|
74
|
+
// Returns the normalized question list, or null when the turn didn't ask.
|
|
75
|
+
export function extractAskQuestionsFromTrace(trace) {
|
|
76
|
+
if (!Array.isArray(trace)) return null;
|
|
77
|
+
for (let i = trace.length - 1; i >= 0; i--) {
|
|
78
|
+
const t = trace[i];
|
|
79
|
+
if (t && t.tool === "ask_questions") {
|
|
80
|
+
const raw = (t.args && Array.isArray(t.args.questions)) ? t.args.questions : [];
|
|
81
|
+
const normalized = raw.map(normalizeQuestion).filter(Boolean);
|
|
82
|
+
return normalized.length > 0 ? normalized : null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Compile collected answers into a single user-message string. Mirrors the
|
|
89
|
+
// shape produced by the web InlineAskPanel.compileAnswers so the super-agent
|
|
90
|
+
// sees consistent input across surfaces.
|
|
91
|
+
export function compileAnswers(state) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
state.questions.forEach((q, i) => {
|
|
94
|
+
const a = state.answers[i] || emptyAnswer();
|
|
95
|
+
if (a.skipped) {
|
|
96
|
+
lines.push(`- ${q.question}\n → (omitido)`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const parts = [];
|
|
100
|
+
if (q.options && q.options.length > 0) {
|
|
101
|
+
const labels = [...a.picked]
|
|
102
|
+
.sort((x, y) => x - y)
|
|
103
|
+
.map((idx) => q.options[idx]?.label)
|
|
104
|
+
.filter(Boolean);
|
|
105
|
+
if (labels.length > 0) parts.push(labels.join(", "));
|
|
106
|
+
}
|
|
107
|
+
const text = (a.text || "").trim();
|
|
108
|
+
if (text) {
|
|
109
|
+
parts.push(q.options && q.options.length > 0 ? `(Otro: ${text})` : text);
|
|
110
|
+
}
|
|
111
|
+
const answerText = parts.length > 0 ? parts.join(" ") : "(sin respuesta)";
|
|
112
|
+
lines.push(`- ${q.question}\n → ${answerText}`);
|
|
113
|
+
});
|
|
114
|
+
return lines.join("\n");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Build the Telegram InlineKeyboardMarkup for one question. Single-select:
|
|
118
|
+
// pressing an option commits immediately; the keyboard disappears via
|
|
119
|
+
// editMessageReplyMarkup. Multi-select: each press toggles a check on the
|
|
120
|
+
// label; a "✓ Confirmar" row commits. No options: keyboard has only a Saltar
|
|
121
|
+
// row, and the user is expected to reply with text.
|
|
122
|
+
export function buildKeyboard(state) {
|
|
123
|
+
const cid = state.correlationId;
|
|
124
|
+
const q = state.questions[state.index];
|
|
125
|
+
const a = state.answers[state.index] || emptyAnswer();
|
|
126
|
+
const rows = [];
|
|
127
|
+
|
|
128
|
+
if (Array.isArray(q.options) && q.options.length > 0) {
|
|
129
|
+
q.options.forEach((opt, i) => {
|
|
130
|
+
const picked = a.picked.has(i);
|
|
131
|
+
const label = q.multiSelect
|
|
132
|
+
? `${picked ? "☑" : "☐"} ${opt.label}`
|
|
133
|
+
: opt.label;
|
|
134
|
+
rows.push([
|
|
135
|
+
{ text: label, callback_data: `apx:ask:${cid}:opt:${i}` },
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
if (q.multiSelect) {
|
|
139
|
+
rows.push([{ text: "✓ Confirmar", callback_data: `apx:ask:${cid}:next` }]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Control row: skip + cancel. Plus a back arrow when we're past Q1.
|
|
144
|
+
const controls = [];
|
|
145
|
+
if (state.index > 0) controls.push({ text: "◀︎ Atrás", callback_data: `apx:ask:${cid}:back` });
|
|
146
|
+
controls.push({ text: "Omitir", callback_data: `apx:ask:${cid}:skip` });
|
|
147
|
+
controls.push({ text: "Cerrar", callback_data: `apx:ask:${cid}:cancel` });
|
|
148
|
+
rows.push(controls);
|
|
149
|
+
|
|
150
|
+
return { inline_keyboard: rows };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Plain-text body of the question message: header (N/M) + question + a hint
|
|
154
|
+
// for free-text questions.
|
|
155
|
+
export function formatQuestionText(state) {
|
|
156
|
+
const q = state.questions[state.index];
|
|
157
|
+
const total = state.questions.length;
|
|
158
|
+
const head = total > 1 ? `[${state.index + 1}/${total}] ` : "";
|
|
159
|
+
const hasOptions = Array.isArray(q.options) && q.options.length > 0;
|
|
160
|
+
const hint = hasOptions
|
|
161
|
+
? (q.multiSelect
|
|
162
|
+
? "\n\n_Multi-selección: tocá las opciones que quieras y después Confirmar._"
|
|
163
|
+
: "\n\n_Tocá una opción para responder._")
|
|
164
|
+
: "\n\n_Respondé con un mensaje de texto._";
|
|
165
|
+
return `❓ ${head}${q.question}${hint}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---- Store API ------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
export function saveState(chatId, state) {
|
|
171
|
+
STORE.set(String(chatId), { ...state, lastTouchedAt: Date.now() });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function getState(chatId) {
|
|
175
|
+
const s = STORE.get(String(chatId));
|
|
176
|
+
if (!s) return null;
|
|
177
|
+
if (Date.now() - s.lastTouchedAt > ASK_TTL_MS) {
|
|
178
|
+
STORE.delete(String(chatId));
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return s;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function clearState(chatId) {
|
|
185
|
+
STORE.delete(String(chatId));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function hasPendingFreeText(chatId) {
|
|
189
|
+
const s = getState(chatId);
|
|
190
|
+
if (!s) return false;
|
|
191
|
+
const q = s.questions[s.index];
|
|
192
|
+
if (!q) return false;
|
|
193
|
+
return !(Array.isArray(q.options) && q.options.length > 0);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Apply a user text reply to the currently-pending free-text question.
|
|
197
|
+
// Returns the updated state (caller decides whether to advance) or null if
|
|
198
|
+
// there was no pending free-text question.
|
|
199
|
+
export function applyTextAnswer(chatId, text) {
|
|
200
|
+
const s = getState(chatId);
|
|
201
|
+
if (!s) return null;
|
|
202
|
+
const q = s.questions[s.index];
|
|
203
|
+
const hasOptions = Array.isArray(q.options) && q.options.length > 0;
|
|
204
|
+
if (hasOptions) return null; // multi/single-select questions are answered via callback only
|
|
205
|
+
const ans = s.answers[s.index] || emptyAnswer();
|
|
206
|
+
ans.text = (text || "").trim();
|
|
207
|
+
ans.skipped = false;
|
|
208
|
+
s.answers[s.index] = ans;
|
|
209
|
+
saveState(chatId, s);
|
|
210
|
+
return s;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Apply a callback_query button press. Returns one of:
|
|
214
|
+
// { action: "advance", state } — render the next question
|
|
215
|
+
// { action: "redraw", state } — same question, refresh the keyboard (toggle)
|
|
216
|
+
// { action: "done", state, compiled } — last question answered
|
|
217
|
+
// { action: "cancel", state } — user closed the panel
|
|
218
|
+
// null — callback wasn't ours
|
|
219
|
+
//
|
|
220
|
+
// callback_data scheme: apx:ask:<correlationId>:<verb>[:<arg>]
|
|
221
|
+
// verbs: opt:<i>, next, back, skip, cancel
|
|
222
|
+
export function applyCallback(chatId, data) {
|
|
223
|
+
const s = getState(chatId);
|
|
224
|
+
if (!s) return null;
|
|
225
|
+
if (typeof data !== "string" || !data.startsWith("apx:ask:")) return null;
|
|
226
|
+
const rest = data.slice("apx:ask:".length); // <corr>:<verb>[:<arg>]
|
|
227
|
+
const [corr, verb, arg] = rest.split(":");
|
|
228
|
+
if (corr !== s.correlationId) {
|
|
229
|
+
// Stale button from a previous flow.
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const q = s.questions[s.index];
|
|
233
|
+
const ans = s.answers[s.index] || emptyAnswer();
|
|
234
|
+
|
|
235
|
+
if (verb === "opt") {
|
|
236
|
+
const optIdx = Number.parseInt(arg, 10);
|
|
237
|
+
if (!Number.isFinite(optIdx) || optIdx < 0 || optIdx >= (q.options?.length || 0)) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
if (q.multiSelect) {
|
|
241
|
+
// Toggle and stay on the same question.
|
|
242
|
+
if (ans.picked.has(optIdx)) ans.picked.delete(optIdx);
|
|
243
|
+
else ans.picked.add(optIdx);
|
|
244
|
+
ans.skipped = false;
|
|
245
|
+
s.answers[s.index] = ans;
|
|
246
|
+
saveState(chatId, s);
|
|
247
|
+
return { action: "redraw", state: s };
|
|
248
|
+
}
|
|
249
|
+
// Single-select: commit + advance.
|
|
250
|
+
ans.picked = new Set([optIdx]);
|
|
251
|
+
ans.skipped = false;
|
|
252
|
+
s.answers[s.index] = ans;
|
|
253
|
+
return advance(s);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (verb === "next") return advance(s);
|
|
257
|
+
if (verb === "back") {
|
|
258
|
+
if (s.index > 0) {
|
|
259
|
+
s.index -= 1;
|
|
260
|
+
saveState(chatId, s);
|
|
261
|
+
}
|
|
262
|
+
return { action: "advance", state: s };
|
|
263
|
+
}
|
|
264
|
+
if (verb === "skip") {
|
|
265
|
+
s.answers[s.index] = { picked: new Set(), text: "", skipped: true };
|
|
266
|
+
return advance(s);
|
|
267
|
+
}
|
|
268
|
+
if (verb === "cancel") {
|
|
269
|
+
clearState(chatId);
|
|
270
|
+
return { action: "cancel", state: s };
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function advance(s) {
|
|
276
|
+
if (s.index >= s.questions.length - 1) {
|
|
277
|
+
const compiled = compileAnswers(s);
|
|
278
|
+
clearState(s.chatId);
|
|
279
|
+
return { action: "done", state: s, compiled };
|
|
280
|
+
}
|
|
281
|
+
s.index += 1;
|
|
282
|
+
saveState(s.chatId, s);
|
|
283
|
+
return { action: "advance", state: s };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Build the initial state and persist it. Caller must follow up with the
|
|
287
|
+
// first sendMessage (use formatQuestionText + buildKeyboard).
|
|
288
|
+
export function startFlow({ chatId, projectId, authorId, questions, resume }) {
|
|
289
|
+
const state = {
|
|
290
|
+
chatId: String(chatId),
|
|
291
|
+
projectId: projectId != null ? String(projectId) : null,
|
|
292
|
+
authorId: authorId != null ? String(authorId) : null,
|
|
293
|
+
correlationId: genCorrelationId(),
|
|
294
|
+
questions,
|
|
295
|
+
answers: questions.map(() => emptyAnswer()),
|
|
296
|
+
index: 0,
|
|
297
|
+
messageId: null,
|
|
298
|
+
createdAt: Date.now(),
|
|
299
|
+
lastTouchedAt: Date.now(),
|
|
300
|
+
resume,
|
|
301
|
+
};
|
|
302
|
+
saveState(chatId, state);
|
|
303
|
+
return state;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Test-only: clear the global store between unit tests.
|
|
307
|
+
export function _reset() {
|
|
308
|
+
STORE.clear();
|
|
309
|
+
}
|