@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,21 @@
|
|
|
1
|
+
# Role
|
|
2
|
+
You are the always-on agent for this APX install — the default voice when no project agent was named. Your real display name comes from the **User & identity** section below.
|
|
3
|
+
|
|
4
|
+
APX is the system you operate (its daemon, config, projects, registered agents, sessions, message logs). APC is the project layout (`.apc/`, `AGENTS.md`) you read on disk when working inside a project. You are NOT APX or APC — you are the agent that knows them well and acts through them.
|
|
5
|
+
|
|
6
|
+
You are not pinned to a single project. You move across every registered project freely, can delegate to a project's own agent (`call_agent`), dispatch work to an external runtime (`call_runtime` → claude-code, codex, opencode, …), or import a new agent from the vault (`list_vault_agents` → `import_agent`).
|
|
7
|
+
|
|
8
|
+
# Hierarchy
|
|
9
|
+
- **Self-run**: no agent named, or the user says "you/yourself/same/default" → act as yourself.
|
|
10
|
+
- **Delegate**: the user names a registered project agent → `call_agent`.
|
|
11
|
+
- **Dispatch**: the user names an external runtime → `call_runtime`.
|
|
12
|
+
|
|
13
|
+
# Projects
|
|
14
|
+
- The default workspace (`id=0`, name `default`) is APX home — it is the "global" scratchpad, not a user repo. Project-scoped work (routines, agent memory, code edits in a real repo) needs a REAL project; if the user asks for project-scoped work without naming one, ask which one.
|
|
15
|
+
- Register a project with `add_project` only — never hand-write `AGENTS.md` or `.apc/project.json` via shell.
|
|
16
|
+
- Identity changes (your name, the user's name, your personality) → `set_identity`, then confirm.
|
|
17
|
+
|
|
18
|
+
# Don't
|
|
19
|
+
- Don't tell the user to run an `apx …` command to get info you can fetch with a tool. You operate APX; run the tool yourself.
|
|
20
|
+
- Don't paste base64 / data URIs in chat — send media via `send_telegram` params or paths.
|
|
21
|
+
- Don't recite the registered-project list at the user; call a tool when they ask.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Action discipline (mandatory)
|
|
2
|
+
- NEVER acknowledge an action you will not execute in the same turn. If you say you will do something, the tool call must be in the same response.
|
|
3
|
+
- Empty acknowledgments ("Ok", "On it", "Give me a moment", "I'll do that now") are not valid standalone replies when a tool call is expected. Either call the tool in this turn, or explain WHY you can't (missing permission, unclear params, tool unavailable).
|
|
4
|
+
- If the user asks for multiple things, do them all in this turn using sequential tool calls.
|
|
5
|
+
- If a tool errors, retry with different arguments before asking the user.
|
|
6
|
+
|
|
7
|
+
# Chit-chat
|
|
8
|
+
- A pure greeting / thanks / "ok" with no actionable request → reply with `finish` only, no other tool call. Tools exist so you can act when needed, not so you must use one.
|
|
9
|
+
- A greeting that piggybacks a real request ("hola, listame las rutinas") is NOT chit-chat — handle the request normally.
|
|
10
|
+
- When in doubt, ask ONE short clarifying question via `finish` — never invent a topic to "be useful".
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# One reply per turn (voice / desktop / deck-voice)
|
|
2
|
+
Your reply will be spoken aloud, or shown briefly in a small surface. Produce ONE clean message per turn — no intro filler, no "ahora ejecuto X" before the tool, no restating after.
|
|
3
|
+
|
|
4
|
+
- Call any tools you need silently, then write the ONE answer with the result.
|
|
5
|
+
- Lead with the outcome. Keep it to 1–2 short sentences unless the user asks for detail.
|
|
6
|
+
- Greet at most once per conversation; if you already greeted, skip it.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Two-segment turns (text channels with visible history)
|
|
2
|
+
When you call a tool, the user sees two text segments — the intro before the tool runs, and the answer after it returns.
|
|
3
|
+
|
|
4
|
+
1. **Intro** — a short natural filler in the user's language BEFORE the tool runs. 2–8 words. NEVER contains the answer. Examples: "Reviso eso", "Dale, lo anoto", "Un momento, busco".
|
|
5
|
+
2. **Answer** — the substantive result AFTER the tool returns. Carries the data, the confirmation, or the next question.
|
|
6
|
+
|
|
7
|
+
Rules:
|
|
8
|
+
- The intro NEVER includes the substantive content. The tool hasn't run yet — you don't know the result.
|
|
9
|
+
- The answer NEVER restates the intro. They're complementary: filler + result.
|
|
10
|
+
- Greet at most ONCE per turn. If the intro greeted, the answer starts with the result.
|
|
11
|
+
- A turn with NO tool calls produces ONE segment — go straight to the answer.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MODE: build. Make the changes directly using your file and shell tools (read_file, write_file, edit_file, run_shell, …). Do not ask for confirmation and do not stop after one step — keep calling tools until the entire task is done, then briefly summarize what you changed and why. Prefer surgical edits over rewrites. When the user asks for a reusable script, snippet, or 'artifact' (something they want to keep and run later), put it under `artifacts/<name>` inside the project — it then shows up in the Artifacts tab. Don't drop reusable scripts at the project root. If a parameter you need is missing (API key, app id, target URL, …), call `ask_questions` ONCE with all your questions and stop — control returns to the user. Do not call ask_questions again in the same turn; you'll just get the same blank state back. Each question can be a string (free-text answer) OR an object {question, options:[{label, description}], multiSelect} for choices. Prefer 2–4 mutually-exclusive options when a question has a natural shortlist (yes/no, which-of-these, …); leave options empty for open-ended answers (API keys, names, free-form ideas). If the previous assistant turn already asked these same questions and the current user message is the compiled answers, DO NOT call ask_questions again — process the answers and proceed with the task.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MODE: plan. Investigate the codebase (read/list/search/grep) and propose an approach with the EXACT changes you would make (files + diffs/snippets). Do NOT write or edit files and do NOT run mutating shell commands — your editing tools are disabled in this mode. End with a concise, ordered plan.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Mode-specific system-prompt fragments for the Code module (plan / build).
|
|
2
|
+
// Each mode lives in its own .md sibling and is loaded once at boot.
|
|
3
|
+
//
|
|
4
|
+
// Why .md files instead of inline strings: the prompts are content, not code.
|
|
5
|
+
// Editing prompt copy shouldn't require touching code, and reviewing changes
|
|
6
|
+
// to behavior should be a doc-shaped diff, not a JS-shaped one.
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { CODE_MODES } from "#core/constants/code-modes.js";
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
function load(file) {
|
|
15
|
+
return fs.readFileSync(path.join(__dirname, file), "utf8").trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CODE_PLAN_GUIDANCE = load("code-plan.md");
|
|
19
|
+
const CODE_BUILD_GUIDANCE = load("code-build.md");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Return the system-prompt fragment that explains how this mode behaves to
|
|
23
|
+
* the agent. Falls back to BUILD when the mode is missing or unknown — build
|
|
24
|
+
* is the safer default for the Code module (no mid-edit "do nothing" stall).
|
|
25
|
+
*/
|
|
26
|
+
export function codeModeGuidance(mode) {
|
|
27
|
+
return mode === CODE_MODES.PLAN ? CODE_PLAN_GUIDANCE : CODE_BUILD_GUIDANCE;
|
|
28
|
+
}
|
|
@@ -44,7 +44,49 @@ export function readSelfMemoryForPrompt(limit = SELF_MEMORY_PROMPT_LIMIT) {
|
|
|
44
44
|
const body = readSelfMemory().trim();
|
|
45
45
|
if (!body) return "";
|
|
46
46
|
if (body.length <= limit) return body;
|
|
47
|
-
|
|
47
|
+
|
|
48
|
+
// The notebook grows chronologically (oldest day first), so a naive head
|
|
49
|
+
// slice injects the OLDEST notes and truncates the most recent — exactly
|
|
50
|
+
// backwards for "what's relevant now". Keep the file header + the NEWEST
|
|
51
|
+
// entries that fit `limit`, re-grouped under their date headings. The full
|
|
52
|
+
// file is always available via read_self_memory.
|
|
53
|
+
const firstLine = body.split("\n", 1)[0];
|
|
54
|
+
const header = firstLine.startsWith("# ") ? firstLine : notebookHeader();
|
|
55
|
+
const notice =
|
|
56
|
+
"_(most recent notes — older history truncated; call read_self_memory for the full notebook)_";
|
|
57
|
+
|
|
58
|
+
const entries = parseSelfMemoryEntries(body); // oldest → newest
|
|
59
|
+
if (!entries.length) {
|
|
60
|
+
// No structured bullets (free-form prose notebook) — fall back to the tail.
|
|
61
|
+
const tail = body.slice(-(limit - notice.length - 2)).replace(/^\S*\n/, "");
|
|
62
|
+
return `${notice}\n${tail.trim()}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let budget = limit - header.length - notice.length - 4;
|
|
66
|
+
const picked = [];
|
|
67
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
68
|
+
const e = entries[i];
|
|
69
|
+
const tag =
|
|
70
|
+
(e.time ? `[${e.time}]` : "") +
|
|
71
|
+
(e.channel && e.channel !== "memory" ? `[${e.channel}]` : "");
|
|
72
|
+
const bullet = `- ${tag ? tag + " " : ""}${e.text}`.replace(/\s+/g, " ").trim();
|
|
73
|
+
const cost = bullet.length + 14; // headroom for an occasional date heading
|
|
74
|
+
if (budget - cost < 0 && picked.length) break;
|
|
75
|
+
picked.push({ date: e.date, bullet });
|
|
76
|
+
budget -= cost;
|
|
77
|
+
}
|
|
78
|
+
picked.reverse(); // back to chronological order (newest at the bottom)
|
|
79
|
+
|
|
80
|
+
const out = [header, notice];
|
|
81
|
+
let lastDate = "";
|
|
82
|
+
for (const p of picked) {
|
|
83
|
+
if (p.date && p.date !== lastDate) {
|
|
84
|
+
out.push("", `## ${p.date}`);
|
|
85
|
+
lastDate = p.date;
|
|
86
|
+
}
|
|
87
|
+
out.push(p.bullet);
|
|
88
|
+
}
|
|
89
|
+
return out.join("\n").trim();
|
|
48
90
|
}
|
|
49
91
|
|
|
50
92
|
// HH:MM (UTC) for the current time — used to tag notes per the cross-channel
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// Persistent vector index for the skill inspector.
|
|
2
|
+
//
|
|
3
|
+
// Why a store instead of re-embedding every turn:
|
|
4
|
+
// - The inspector scores the user prompt against every known skill on every
|
|
5
|
+
// turn. Even with the in-process cache in rag.js, a daemon restart pays the
|
|
6
|
+
// cold cost. A JSON-backed store survives restarts and makes `apx skills
|
|
7
|
+
// index` a real, observable operation (progress bar, totals).
|
|
8
|
+
// - It also unlocks "chunked" descriptions later: today we embed just the
|
|
9
|
+
// condensed description; tomorrow we can index the SKILL.md body itself.
|
|
10
|
+
//
|
|
11
|
+
// Format (~/.apx/skills/.index.json):
|
|
12
|
+
// {
|
|
13
|
+
// embedder: "tf" | "ollama:nomic-embed-text" | "openai:text-embedding-3-small" | ...,
|
|
14
|
+
// dim: 256 | 768 | ...,
|
|
15
|
+
// updated_at: "2026-06-13T...",
|
|
16
|
+
// items: {
|
|
17
|
+
// "<slug>": {
|
|
18
|
+
// slug, source, file, mtime_ms,
|
|
19
|
+
// desc_hash, desc, desc_vector: [..],
|
|
20
|
+
// // future: chunks: [{ text, vector }]
|
|
21
|
+
// }
|
|
22
|
+
// }
|
|
23
|
+
// }
|
|
24
|
+
//
|
|
25
|
+
// Invariants:
|
|
26
|
+
// - All vectors in the file share the same embedder tag and dim. Switching
|
|
27
|
+
// embedder invalidates the entire index — we rebuild from scratch.
|
|
28
|
+
// - A skill whose source file has a different mtime than what's recorded is
|
|
29
|
+
// re-embedded the next time `ensureIndex` runs. A skill that disappeared
|
|
30
|
+
// is dropped.
|
|
31
|
+
// - Reads NEVER throw into the daemon: a corrupted file is treated as empty.
|
|
32
|
+
//
|
|
33
|
+
// Concurrency: the daemon is single-process for now; we use a simple write-
|
|
34
|
+
// then-rename to avoid half-written files, no advisory lock. If a future
|
|
35
|
+
// multi-process arrangement is added, swap in proper file locking here.
|
|
36
|
+
|
|
37
|
+
import fs from "node:fs";
|
|
38
|
+
import path from "node:path";
|
|
39
|
+
import os from "node:os";
|
|
40
|
+
|
|
41
|
+
import { embedOne } from "#core/memory/embeddings.js";
|
|
42
|
+
import { listSkills } from "./loader.js";
|
|
43
|
+
import { condenseSkillDescription } from "./catalog.js";
|
|
44
|
+
|
|
45
|
+
const INDEX_PATH = path.join(os.homedir(), ".apx", "skills", ".index.json");
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Disk I/O
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
function emptyIndex() {
|
|
52
|
+
return { embedder: null, dim: null, updated_at: null, items: {} };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function indexPath() {
|
|
56
|
+
return INDEX_PATH;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function readIndex() {
|
|
60
|
+
try {
|
|
61
|
+
if (!fs.existsSync(INDEX_PATH)) return emptyIndex();
|
|
62
|
+
const raw = fs.readFileSync(INDEX_PATH, "utf8");
|
|
63
|
+
const parsed = JSON.parse(raw);
|
|
64
|
+
if (!parsed || typeof parsed !== "object" || !parsed.items) return emptyIndex();
|
|
65
|
+
return parsed;
|
|
66
|
+
} catch {
|
|
67
|
+
return emptyIndex();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function writeIndex(idx) {
|
|
72
|
+
fs.mkdirSync(path.dirname(INDEX_PATH), { recursive: true });
|
|
73
|
+
const tmp = INDEX_PATH + ".tmp";
|
|
74
|
+
fs.writeFileSync(tmp, JSON.stringify(idx, null, 2));
|
|
75
|
+
fs.renameSync(tmp, INDEX_PATH);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Hashing
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function descHashOf(text) {
|
|
83
|
+
let h = 0;
|
|
84
|
+
const s = String(text || "");
|
|
85
|
+
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
|
|
86
|
+
return h;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function fileMtimeMs(file) {
|
|
90
|
+
try { return fs.statSync(file).mtimeMs; } catch { return 0; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Build / refresh
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Decide what work the next ensureIndex() call would do — without actually
|
|
99
|
+
* embedding anything. Used by `apx skills index` to render the progress bar
|
|
100
|
+
* and by the inspector startup probe to decide whether to bother rebuilding.
|
|
101
|
+
*
|
|
102
|
+
* Returns: { existing, missing, stale, gone, total } — `existing` are the
|
|
103
|
+
* slugs that already have an up-to-date vector, `missing` need a first embed,
|
|
104
|
+
* `stale` had their description rewritten, `gone` are slugs in the index but
|
|
105
|
+
* no longer on disk.
|
|
106
|
+
*/
|
|
107
|
+
export function planIndex({ projectPath, currentEmbedder } = {}) {
|
|
108
|
+
const skills = listSkills({ projectPath });
|
|
109
|
+
const idx = readIndex();
|
|
110
|
+
const embedderChanged = currentEmbedder && idx.embedder && idx.embedder !== currentEmbedder;
|
|
111
|
+
|
|
112
|
+
const existing = [];
|
|
113
|
+
const missing = [];
|
|
114
|
+
const stale = [];
|
|
115
|
+
const slugsSeen = new Set();
|
|
116
|
+
|
|
117
|
+
for (const s of skills) {
|
|
118
|
+
slugsSeen.add(s.slug);
|
|
119
|
+
const desc = condenseSkillDescription(s.description);
|
|
120
|
+
const hash = descHashOf(desc + "|" + s.file);
|
|
121
|
+
const mtime = fileMtimeMs(s.file);
|
|
122
|
+
const hit = idx.items?.[s.slug];
|
|
123
|
+
|
|
124
|
+
if (embedderChanged || !hit || !Array.isArray(hit.desc_vector)) {
|
|
125
|
+
missing.push(s.slug);
|
|
126
|
+
} else if (hit.desc_hash !== hash || hit.mtime_ms !== mtime) {
|
|
127
|
+
stale.push(s.slug);
|
|
128
|
+
} else {
|
|
129
|
+
existing.push(s.slug);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const gone = Object.keys(idx.items || {}).filter((slug) => !slugsSeen.has(slug));
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
existing,
|
|
137
|
+
missing,
|
|
138
|
+
stale,
|
|
139
|
+
gone,
|
|
140
|
+
total: skills.length,
|
|
141
|
+
embedderChanged: !!embedderChanged,
|
|
142
|
+
embedder: idx.embedder,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Bring the on-disk index up to date with the current skill catalog. Skills
|
|
148
|
+
* with unchanged file+desc are skipped (cheap). Skills with new/changed
|
|
149
|
+
* content are re-embedded. Skills that disappeared are dropped. When the
|
|
150
|
+
* embedder differs from the file's tag, the whole index is rebuilt.
|
|
151
|
+
*
|
|
152
|
+
* @param opts.projectPath also scan this project's .apc/skills
|
|
153
|
+
* @param opts.embedOpts forwarded to embedOne (globalConfig, provider, ...)
|
|
154
|
+
* @param opts.onProgress called as ({ done, total, slug, action })
|
|
155
|
+
* @param opts.force rebuild every slug from scratch
|
|
156
|
+
* @returns { embedder, dim, items, changed: { added, refreshed, removed, kept } }
|
|
157
|
+
*/
|
|
158
|
+
export async function ensureIndex({ projectPath, embedOpts = {}, onProgress, force = false } = {}) {
|
|
159
|
+
const skills = listSkills({ projectPath });
|
|
160
|
+
const idxBefore = readIndex();
|
|
161
|
+
|
|
162
|
+
// Probe the embedder once. If TF fallback wins, every skill embeds offline —
|
|
163
|
+
// no per-skill provider timeout cost. Tag is "<provider>:<model>" or "tf".
|
|
164
|
+
const probe = await embedOne("probe", embedOpts);
|
|
165
|
+
const embedder = embedderTag(probe);
|
|
166
|
+
const dim = probe.vector.length;
|
|
167
|
+
|
|
168
|
+
const embedderChanged = !force && idxBefore.embedder && idxBefore.embedder !== embedder;
|
|
169
|
+
const items = embedderChanged || force ? {} : structuredClone(idxBefore.items || {});
|
|
170
|
+
|
|
171
|
+
const added = [];
|
|
172
|
+
const refreshed = [];
|
|
173
|
+
const kept = [];
|
|
174
|
+
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
let done = 0;
|
|
177
|
+
for (const s of skills) {
|
|
178
|
+
seen.add(s.slug);
|
|
179
|
+
const desc = condenseSkillDescription(s.description);
|
|
180
|
+
const hash = descHashOf(desc + "|" + s.file);
|
|
181
|
+
const mtime = fileMtimeMs(s.file);
|
|
182
|
+
|
|
183
|
+
const prev = items[s.slug];
|
|
184
|
+
const upToDate = prev
|
|
185
|
+
&& Array.isArray(prev.desc_vector)
|
|
186
|
+
&& prev.desc_hash === hash
|
|
187
|
+
&& prev.mtime_ms === mtime;
|
|
188
|
+
|
|
189
|
+
let action;
|
|
190
|
+
if (upToDate) {
|
|
191
|
+
kept.push(s.slug);
|
|
192
|
+
action = "kept";
|
|
193
|
+
} else {
|
|
194
|
+
const out = await embedOne(desc, embedOpts);
|
|
195
|
+
const vector = Array.isArray(out?.vector) ? out.vector : [];
|
|
196
|
+
items[s.slug] = {
|
|
197
|
+
slug: s.slug,
|
|
198
|
+
source: s.source,
|
|
199
|
+
file: s.file,
|
|
200
|
+
mtime_ms: mtime,
|
|
201
|
+
desc_hash: hash,
|
|
202
|
+
desc,
|
|
203
|
+
desc_vector: vector,
|
|
204
|
+
};
|
|
205
|
+
if (prev) {
|
|
206
|
+
refreshed.push(s.slug);
|
|
207
|
+
action = "refreshed";
|
|
208
|
+
} else {
|
|
209
|
+
added.push(s.slug);
|
|
210
|
+
action = "added";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
done += 1;
|
|
215
|
+
try { onProgress?.({ done, total: skills.length, slug: s.slug, action }); }
|
|
216
|
+
catch { /* progress callback errors must not break indexing */ }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const removed = Object.keys(items).filter((slug) => !seen.has(slug));
|
|
220
|
+
for (const slug of removed) delete items[slug];
|
|
221
|
+
|
|
222
|
+
const next = {
|
|
223
|
+
embedder,
|
|
224
|
+
dim,
|
|
225
|
+
updated_at: new Date().toISOString(),
|
|
226
|
+
items,
|
|
227
|
+
};
|
|
228
|
+
writeIndex(next);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
embedder,
|
|
232
|
+
dim,
|
|
233
|
+
items,
|
|
234
|
+
changed: { added, refreshed, removed, kept },
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Self-healing background refresh
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
// Single in-flight guard so concurrent turns don't stack reindexes. Module-
|
|
243
|
+
// scoped: one daemon process = one refresh at a time.
|
|
244
|
+
let refreshInFlight = null;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* If the on-disk index is out of date relative to the live catalog (a skill
|
|
248
|
+
* was added/edited/removed, or the embedder changed), kick a background
|
|
249
|
+
* rebuild — WITHOUT blocking the caller. The current turn keeps using whatever
|
|
250
|
+
* is already indexed; the next turn sees the fresh vectors.
|
|
251
|
+
*
|
|
252
|
+
* This is what makes "drop a SKILL.md and it just works" true: the inspector
|
|
253
|
+
* calls this every turn (fire-and-forget), and the daemon calls it on startup.
|
|
254
|
+
*
|
|
255
|
+
* Returns a small descriptor of what it decided to do (handy for logging/tests).
|
|
256
|
+
*/
|
|
257
|
+
export function backgroundRefreshIfStale({ projectPath, embedOpts = {}, currentEmbedder, onDone } = {}) {
|
|
258
|
+
if (refreshInFlight) return { started: false, reason: "in_flight" };
|
|
259
|
+
|
|
260
|
+
let plan;
|
|
261
|
+
try {
|
|
262
|
+
plan = planIndex({ projectPath, currentEmbedder });
|
|
263
|
+
} catch {
|
|
264
|
+
return { started: false, reason: "plan_failed" };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const work = plan.missing.length + plan.stale.length + plan.gone.length;
|
|
268
|
+
if (work === 0 && !plan.embedderChanged) {
|
|
269
|
+
return { started: false, reason: "fresh" };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
refreshInFlight = ensureIndex({ projectPath, embedOpts, force: plan.embedderChanged })
|
|
273
|
+
.then((out) => {
|
|
274
|
+
try { onDone?.(out); } catch { /* best-effort */ }
|
|
275
|
+
return out;
|
|
276
|
+
})
|
|
277
|
+
.catch(() => null)
|
|
278
|
+
.finally(() => { refreshInFlight = null; });
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
started: true,
|
|
282
|
+
missing: plan.missing.length,
|
|
283
|
+
stale: plan.stale.length,
|
|
284
|
+
gone: plan.gone.length,
|
|
285
|
+
embedderChanged: plan.embedderChanged,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Await any in-flight background refresh (used by tests / graceful shutdown). */
|
|
290
|
+
export async function awaitRefresh() {
|
|
291
|
+
if (refreshInFlight) { try { await refreshInFlight; } catch { /* ignore */ } }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Helpers
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
export function embedderTag(probe) {
|
|
299
|
+
if (!probe) return "tf";
|
|
300
|
+
if (probe.embedder) return probe.embedder;
|
|
301
|
+
return "tf";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Delete the on-disk index. Used by `apx skills index --reset`. */
|
|
305
|
+
export function clearIndex() {
|
|
306
|
+
try { fs.unlinkSync(INDEX_PATH); } catch { /* missing is fine */ }
|
|
307
|
+
}
|
|
@@ -3,4 +3,18 @@ export { condenseSkillDescription, buildSkillsHintBlock } from "./catalog.js";
|
|
|
3
3
|
export { tryResolveSkillCommand } from "./trigger.js";
|
|
4
4
|
export { suggestSkillForPrompt, clearSkillVectorCache } from "./rag.js";
|
|
5
5
|
export { listSkills, loadSkill, SKILL_LOCATIONS } from "./loader.js";
|
|
6
|
-
|
|
6
|
+
export {
|
|
7
|
+
inspectPromptForSkills,
|
|
8
|
+
isInspectorEnabled,
|
|
9
|
+
INSPECTOR_DEFAULTS,
|
|
10
|
+
summarizeTrace,
|
|
11
|
+
} from "./inspector.js";
|
|
12
|
+
export {
|
|
13
|
+
ensureIndex,
|
|
14
|
+
planIndex,
|
|
15
|
+
readIndex,
|
|
16
|
+
clearIndex,
|
|
17
|
+
indexPath,
|
|
18
|
+
backgroundRefreshIfStale,
|
|
19
|
+
awaitRefresh,
|
|
20
|
+
} from "./index-store.js";
|