@agentprojectcontext/apx 1.34.0 → 1.36.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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/skills/apx/SKILL.md +1 -1
  3. package/src/core/agent/build-agent-system.js +134 -58
  4. package/src/core/agent/channels/voice-context.js +4 -4
  5. package/src/core/agent/prompt-builder.js +176 -123
  6. package/src/core/agent/prompts/channels/code.md +12 -10
  7. package/src/core/agent/prompts/channels/desktop.md +5 -32
  8. package/src/core/agent/prompts/channels/telegram.md +4 -15
  9. package/src/core/agent/prompts/channels/web_code.md +11 -11
  10. package/src/core/agent/prompts/core/agent-base.md +24 -0
  11. package/src/core/agent/prompts/core/project-agent.md +11 -0
  12. package/src/core/agent/prompts/core/super-agent.md +21 -0
  13. package/src/core/agent/prompts/discipline/action.md +10 -0
  14. package/src/core/agent/prompts/discipline/single-segment.md +6 -0
  15. package/src/core/agent/prompts/discipline/two-segment.md +11 -0
  16. package/src/core/agent/self-memory.js +43 -1
  17. package/src/core/agent/skills/index-store.js +307 -0
  18. package/src/core/agent/skills/index.js +15 -1
  19. package/src/core/agent/skills/inspector.js +317 -0
  20. package/src/core/agent/super-agent.js +7 -1
  21. package/src/core/agent/tools/handlers/_git.js +50 -0
  22. package/src/core/agent/tools/handlers/git-diff.js +44 -0
  23. package/src/core/agent/tools/handlers/git-log.js +38 -0
  24. package/src/core/agent/tools/handlers/git-show.js +34 -0
  25. package/src/core/agent/tools/handlers/git-status.js +61 -0
  26. package/src/core/agent/tools/names.js +31 -0
  27. package/src/core/agent/tools/registry.js +36 -5
  28. package/src/core/config/index.js +21 -0
  29. package/src/core/runtime-skills/apx/SKILL.md +27 -39
  30. package/src/core/runtime-skills/apx-agency-agents/SKILL.md +40 -56
  31. package/src/core/runtime-skills/apx-agent/SKILL.md +27 -30
  32. package/src/core/runtime-skills/apx-mcp/SKILL.md +31 -36
  33. package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +37 -51
  34. package/src/core/runtime-skills/apx-project/SKILL.md +20 -29
  35. package/src/core/runtime-skills/apx-routine/SKILL.md +34 -47
  36. package/src/core/runtime-skills/apx-runtime/SKILL.md +32 -50
  37. package/src/core/runtime-skills/apx-sessions/SKILL.md +96 -145
  38. package/src/core/runtime-skills/apx-skill-builder/SKILL.md +53 -77
  39. package/src/core/runtime-skills/apx-task/SKILL.md +18 -21
  40. package/src/core/runtime-skills/apx-telegram/SKILL.md +43 -54
  41. package/src/core/runtime-skills/apx-voice/SKILL.md +36 -56
  42. package/src/core/stores/conversations.js +27 -2
  43. package/src/host/daemon/api/exec.js +2 -0
  44. package/src/host/daemon/api/skills.js +140 -6
  45. package/src/host/daemon/api/super-agent.js +56 -1
  46. package/src/host/daemon/index.js +17 -0
  47. package/src/interfaces/cli/branding.js +53 -0
  48. package/src/interfaces/cli/commands/skills.js +254 -0
  49. package/src/interfaces/cli/index.js +84 -2
  50. package/src/interfaces/web/dist/assets/index-Cm0KyPoZ.css +1 -0
  51. package/src/interfaces/web/dist/assets/index-DJKA763h.js +628 -0
  52. package/src/interfaces/web/dist/assets/index-DJKA763h.js.map +1 -0
  53. package/src/interfaces/web/dist/index.html +2 -2
  54. package/src/interfaces/web/src/App.tsx +0 -1
  55. package/src/interfaces/web/src/components/chat/ChatList.tsx +412 -0
  56. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +21 -1
  57. package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
  58. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +69 -1
  59. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +222 -0
  60. package/src/interfaces/web/src/hooks/useChat.ts +54 -2
  61. package/src/interfaces/web/src/i18n/en.ts +12 -1
  62. package/src/interfaces/web/src/i18n/es.ts +12 -1
  63. package/src/interfaces/web/src/lib/api/agents.ts +1 -1
  64. package/src/interfaces/web/src/lib/api/skills.ts +70 -0
  65. package/src/interfaces/web/src/screens/ProjectScreen.tsx +3 -5
  66. package/src/interfaces/web/src/screens/SettingsScreen.tsx +12 -6
  67. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +1 -1
  68. package/src/interfaces/web/src/screens/project/ChatTab.tsx +120 -87
  69. package/src/interfaces/web/src/types/daemon.ts +10 -0
  70. package/src/core/agent/prompts/action-discipline.md +0 -24
  71. package/src/core/agent/prompts/super-agent-base.md +0 -42
  72. package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +0 -1
  73. package/src/interfaces/web/dist/assets/index-M4FspaCH.js +0 -613
  74. package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +0 -1
  75. package/src/interfaces/web/src/screens/project/ThreadsTab.tsx +0 -100
@@ -1,44 +1,39 @@
1
1
  ---
2
2
  name: apx-voice
3
3
  scope: optional
4
- description: How APX handles voice TTS engines (Piper local, ElevenLabs / OpenAI / Gemini cloud), the unified /voice/turn channel, and apx voice CLI. Load when the user wants to speak with APX, configure a voice engine, or troubleshoot silent output.
4
+ description: APX TTSPiper (local), ElevenLabs/OpenAI/Gemini (cloud), unified /voice/turn channel, `apx voice` CLI. Load when the user wants to speak with APX, configure a voice engine, or troubleshoot silent output.
5
5
  ---
6
6
 
7
7
  # apx-voice
8
8
 
9
- APX has a Text-to-Speech (TTS) facade in `core/voice/` with five engines. STT (speech-to-text) lives separately in `host/daemon/transcription.js` (Whisper). The "voice channel" combines both for a full mic→agent→speaker turn.
9
+ TTS facade in `core/voice/` with five engines. STT lives separately in `host/daemon/transcription.js` (Whisper). The "voice channel" combines both for mic→agent→speaker.
10
10
 
11
11
  ## Engines
12
12
 
13
- | id | Local? | Needs key? | Quality | Notes |
14
- |---|---|---|---|---|
15
- | `piper` | yes | no | Good (es_AR-daniela-high recommended) | Local, offline. Requires `piper` CLI + `.onnx` voice model. |
16
- | `elevenlabs` | no | yes | Excellent | Free tier 10k chars/mo. `eleven_multilingual_v2`. |
17
- | `openai` | no | yes | Good | Reuses `engines.openai.api_key`. `tts-1`. |
18
- | `gemini` | no | yes | Good | Returns raw L16 PCM — APX wraps in WAV automatically. |
19
- | `mock` | yes | no | Silent | Silent WAV; placeholder for tests. |
13
+ | id | Local? | Needs key? | Notes |
14
+ |---|---|---|---|
15
+ | `piper` | yes | no | Local, offline. Requires `piper` CLI + `.onnx` model. es_AR-daniela-high recommended. |
16
+ | `elevenlabs` | no | yes | Excellent. Free tier 10k chars/mo. `eleven_multilingual_v2`. |
17
+ | `openai` | no | yes | Reuses `engines.openai.api_key`. `tts-1`. |
18
+ | `gemini` | no | yes | Returns raw L16 PCM — APX wraps in WAV automatically. |
19
+ | `mock` | yes | no | Silent WAV; placeholder for tests. |
20
20
 
21
- `auto` provider probes in order: piper → elevenlabs → openai → gemini → mock.
21
+ `auto` probes: piper → elevenlabs → openai → gemini → mock.
22
22
 
23
23
  ## Concrete CLI calls
24
24
 
25
25
  ```bash
26
- # Inspect what's configured + available
27
- apx voice providers
28
-
29
- # Synthesize and play
26
+ apx voice providers # what's configured + available
30
27
  apx voice say "Hello from APX" --provider piper
31
- apx voice say "Hello from APX" --provider gemini
32
- apx voice say "Hello from APX" --provider gemini --voice Aoede # pick a specific voice
33
- apx voice say "..." --no-play # generate WAV, don't play
28
+ apx voice say "Hello from APX" --provider gemini --voice Aoede
29
+ apx voice say "..." --no-play # generate WAV, don't play
34
30
 
35
- # Listen (mic → STT)
36
- apx voice listen # records until silence (sox) or Ctrl+C
37
- apx voice listen --seconds 5 # fixed-duration capture
38
- apx voice listen --provider <id> # override the STT/transcription provider
31
+ apx voice listen # mic → STT, records until silence (sox) or Ctrl+C
32
+ apx voice listen --seconds 5 # fixed-duration capture
33
+ apx voice listen --provider <id> # override STT provider
39
34
  ```
40
35
 
41
- Playback uses system binaries (`afplay`, `paplay`, `aplay`, `play`, `ffplay`) — APX doesn't bundle an audio runtime. If none is found, you get the file path and no playback.
36
+ Playback uses system binaries (`afplay`, `paplay`, `aplay`, `play`, `ffplay`). If none found, you get the file path and no playback.
42
37
 
43
38
  ## Configuration
44
39
 
@@ -60,9 +55,7 @@ Playback uses system binaries (`afplay`, `paplay`, `aplay`, `play`, `ffplay`)
60
55
 
61
56
  `apx config set voice.tts.provider <name>` to switch.
62
57
 
63
- ## Quick setup paths
64
-
65
- ### Piper local (recommended, no internet)
58
+ ## Quick setup: Piper local (recommended, no internet)
66
59
 
67
60
  ```bash
68
61
  # 1. Install binary (macOS arm64)
@@ -70,21 +63,18 @@ curl -L https://github.com/rhasspy/piper/releases/latest/download/piper_macos_aa
70
63
  -o /tmp/piper.tar.gz
71
64
  sudo tar xzf /tmp/piper.tar.gz -C /usr/local/bin --strip-components=1
72
65
 
73
- # 2. Voice model (es_AR — Argentine Spanish, voice "daniela")
74
- mkdir -p ~/.apx/voices
75
- cd ~/.apx/voices
66
+ # 2. Voice model (es_AR, "daniela")
67
+ mkdir -p ~/.apx/voices && cd ~/.apx/voices
76
68
  curl -LO https://huggingface.co/rhasspy/piper-voices/resolve/main/es/es_AR/daniela/high/es_AR-daniela-high.onnx
77
69
  curl -LO https://huggingface.co/rhasspy/piper-voices/resolve/main/es/es_AR/daniela/high/es_AR-daniela-high.onnx.json
78
70
 
79
- # 3. APX config
71
+ # 3. Configure + test
80
72
  apx config set voice.tts.provider piper
81
73
  apx config set voice.tts.piper.model "$HOME/.apx/voices/es_AR-daniela-high.onnx"
82
-
83
- # 4. Test
84
- apx voice say "hola, soy APX" --provider piper # Spanish text exercises the es_AR voice
74
+ apx voice say "hola, soy APX" --provider piper
85
75
  ```
86
76
 
87
- ### Gemini cloud (quick if you already have a key)
77
+ ## Quick setup: Gemini cloud
88
78
 
89
79
  ```bash
90
80
  apx config set voice.tts.provider gemini
@@ -93,45 +83,35 @@ apx config set engines.gemini.api_key '<GEMINI_KEY>' # reuse for LLM router
93
83
  apx voice say "Hello from APX" --provider gemini
94
84
  ```
95
85
 
96
- ## The unified voice channel
86
+ ## Unified voice channel
97
87
 
98
- `POST /voice/turn` is one round-trip: send audio (or text), get back `{ user_text, reply_text, reply_audio_path }`. STT in, agent loop, TTS out. Surface for the overlay and any future "voice room" client.
88
+ `POST /voice/turn` is one round-trip: send audio (or text), get back `{ user_text, reply_text, reply_audio_path }`. STT in, agent loop, TTS out. For overlay and future "voice room" clients.
99
89
 
100
90
  ```bash
101
- # Drive from curl with already-transcribed text (skip STT)
102
91
  curl -X POST http://127.0.0.1:7430/voice/turn \
103
92
  -H "Authorization: Bearer $(cat ~/.apx/daemon.token)" \
104
93
  -H "Content-Type: application/json" \
105
94
  -d '{"text":"Hello APX","channel":"voice"}'
106
95
  ```
107
96
 
108
- Telegram voice messages and the overlay mascot still have their own STT pipelines today — they don't go through `/voice/turn` (yet). The endpoint exists for callers that want one-shot bidirectional voice.
97
+ Telegram voice messages and overlay mascot still have their own STT pipelines — they don't go through `/voice/turn` yet.
109
98
 
110
99
  ## Anti-examples
111
100
 
112
- ```bash
113
- # DON'T trust `apx voice providers` saying "mock available" as a green light.
114
- # Mock is silence; useful for tests, useless for talking to humans.
115
- # If only mock is "available", configure a real provider.
116
-
117
- # DON'T set voice.tts.provider to a provider with no key.
118
- # It will fall through `auto` to the next, but that's not what you asked for.
119
-
120
- # DON'T expect Gemini TTS to give you an MP3.
121
- # Today it returns raw L16 PCM. APX wraps it in a 44-byte WAV header so afplay
122
- # accepts it. Files are .wav, mime "audio/wav". If you need MP3, convert with ffmpeg.
123
- ```
101
+ - DON'T trust `apx voice providers` saying "mock available" as green light — mock is silence. Configure a real provider.
102
+ - DON'T set `voice.tts.provider` to a provider with no key. It falls through `auto` to the next, but that's not what you asked.
103
+ - DON'T expect Gemini TTS to return MP3 — it returns raw L16 PCM; APX wraps in WAV. Files are `.wav`, mime `audio/wav`. Convert with ffmpeg if you need MP3.
124
104
 
125
105
  ## Troubleshooting silent output
126
106
 
127
107
  1. `apx voice providers` — what's actually available?
128
- 2. `apx voice say "test" --provider <engine> --no-play` — does the file exist?
129
- 3. `file <path>` — is it a valid container? Gemini's output should be `RIFF WAVE Microsoft PCM`.
130
- 4. `afplay <path>` directly — does the OS player open it?
131
- 5. If 3 fails for Gemini, you may be on an older APX before the PCM-wrap fix (commit `ba5c416` or later).
108
+ 2. `apx voice say "test" --provider <engine> --no-play` — file exists?
109
+ 3. `file <path>` — valid container? Gemini output should be `RIFF WAVE Microsoft PCM`.
110
+ 4. `afplay <path>` — does the OS player open it?
111
+ 5. If 3 fails for Gemini, you may be on APX before the PCM-wrap fix (commit `ba5c416`+).
132
112
 
133
113
  ## Don't
134
114
 
135
- - Don't paste base64 audio into chat. Use file paths or upload via `send_voice` / `send_audio`.
136
- - Don't switch providers mid-routine without testing — voice quality varies a lot between Piper voices and cloud engines.
137
- - Don't expect TTS streaming yet — `apx voice say` returns a complete file. A `/tts/stream` endpoint with chunked audio is open work.
115
+ - Paste base64 audio into chat. Use file paths or `send_voice` / `send_audio`.
116
+ - Switch providers mid-routine without testing — quality varies a lot across Piper voices and cloud engines.
117
+ - Expect TTS streaming yet — `apx voice say` returns a complete file. `/tts/stream` is open work.
@@ -27,7 +27,7 @@ export function conversationPath(storagePath, agentSlug, idOrFilename) {
27
27
  return path.join(storagePath, "agents", agentSlug, "conversations", filename);
28
28
  }
29
29
 
30
- export function startConversation({ storagePath, agentSlug, engine, system }) {
30
+ export function startConversation({ storagePath, agentSlug, engine, system, channel }) {
31
31
  const dir = path.join(storagePath, "agents", agentSlug, "conversations");
32
32
  fs.mkdirSync(dir, { recursive: true });
33
33
  const id = generateConversationId(storagePath, agentSlug);
@@ -38,6 +38,7 @@ export function startConversation({ storagePath, agentSlug, engine, system }) {
38
38
  `id: ${id}\n` +
39
39
  `agent: ${agentSlug}\n` +
40
40
  `engine: ${engine}\n` +
41
+ (channel ? `channel: ${channel}\n` : "") +
41
42
  `started: ${started}\n` +
42
43
  `last_turn: \n` +
43
44
  `status: open\n` +
@@ -98,7 +99,31 @@ export function listConversations(storagePath, agentSlug) {
98
99
  .filter((f) => f.endsWith(".md"))
99
100
  .sort()
100
101
  .reverse()
101
- .map((f) => ({ filename: f, id: f.replace(/\.md$/, "") }));
102
+ .map((f) => summarizeConversation(path.join(dir, f), agentSlug, f))
103
+ .filter(Boolean);
104
+ }
105
+
106
+ // Lightweight summary used by the chat list sidebar — reads frontmatter and
107
+ // counts turns without loading the whole conversation into memory beyond what
108
+ // `fs.readFileSync` already does. The fields match `ConversationListEntry` on
109
+ // the frontend so the sidebar can group + filter without a second roundtrip.
110
+ function summarizeConversation(filePath, agentSlug, filename) {
111
+ let text;
112
+ try { text = fs.readFileSync(filePath, "utf8"); } catch { return null; }
113
+ const { fm, turns } = parseConversation(text);
114
+ const messages = turns.filter((t) => t.role !== "system" && t.role !== "compact").length;
115
+ const firstUser = turns.find((t) => t.role === "user");
116
+ const title = (firstUser?.content || "").split("\n")[0].slice(0, 80).trim() || undefined;
117
+ return {
118
+ id: filename.replace(/\.md$/, ""),
119
+ filename,
120
+ agent_slug: agentSlug,
121
+ started_at: fm.started || fm.last_turn || "",
122
+ ended_at: fm.status === "closed" ? (fm.last_turn || undefined) : undefined,
123
+ channel: fm.channel || undefined,
124
+ messages,
125
+ title,
126
+ };
102
127
  }
103
128
 
104
129
  export function setStatus(filePath, status) {
@@ -116,6 +116,7 @@ export function register(app, { projects, project, config }) {
116
116
  model: modelOverride,
117
117
  temperature,
118
118
  maxTokens,
119
+ channel,
119
120
  } = req.body || {};
120
121
  if (!prompt) return res.status(400).json({ error: "prompt required" });
121
122
  const agents = readAgents(p.path);
@@ -171,6 +172,7 @@ export function register(app, { projects, project, config }) {
171
172
  agentSlug: agent.slug,
172
173
  engine: modelId,
173
174
  system,
175
+ channel,
174
176
  });
175
177
  convPath = conv.path;
176
178
  convId = conv.id;
@@ -1,12 +1,42 @@
1
- // Lightweight `/skills` listing for UI surfaces (web composer picker,
2
- // future palettes). Same data backing `list_skills` tool, but here without
3
- // auth-binding to a project — anyone with a valid daemon token can ask
4
- // "which skills are around right now?".
1
+ // `/skills` listing + Skill Inspector control surface for UI clients.
5
2
  //
6
- // Returns the catalog already condensed (slug + first-sentence description)
7
- // so the picker doesn't have to repeat the cleanup work.
3
+ // GET /skills catalog (slug + condensed description)
4
+ // GET /skills/inspector inspector config + index status
5
+ // PUT /skills/inspector toggle / tune inspector config
6
+ // POST /skills/index (re)build the inspector vector index
7
+ // POST /skills/inspect dry-run the inspector for a prompt
8
+ //
9
+ // The listing is the same data backing `list_skills` (no auth-binding to a
10
+ // project). The inspector routes mirror /embeddings/* so the web admin can
11
+ // configure the skill RAG exactly like it configures the memory RAG.
8
12
  import { listSkills } from "#core/agent/skills/loader.js";
9
13
  import { condenseSkillDescription } from "#core/agent/skills/catalog.js";
14
+ import {
15
+ inspectPromptForSkills,
16
+ INSPECTOR_DEFAULTS,
17
+ } from "#core/agent/skills/inspector.js";
18
+ import {
19
+ ensureIndex,
20
+ planIndex,
21
+ readIndex,
22
+ } from "#core/agent/skills/index-store.js";
23
+ import { readConfig, writeConfig } from "#core/config/index.js";
24
+
25
+ const KNOWN_KEYS = Object.keys(INSPECTOR_DEFAULTS);
26
+
27
+ function mergedInspectorConfig(cfg) {
28
+ return { ...INSPECTOR_DEFAULTS, ...(cfg?.skills?.inspector || {}) };
29
+ }
30
+
31
+ function indexStatus() {
32
+ const idx = readIndex();
33
+ return {
34
+ count: Object.keys(idx.items || {}).length,
35
+ embedder: idx.embedder || null,
36
+ dim: idx.dim || null,
37
+ updated_at: idx.updated_at || null,
38
+ };
39
+ }
10
40
 
11
41
  export function register(app /*, ctx */) {
12
42
  app.get("/skills", (req, res) => {
@@ -27,4 +57,108 @@ export function register(app /*, ctx */) {
27
57
  res.status(500).json({ error: e.message });
28
58
  }
29
59
  });
60
+
61
+ // ---- Inspector config + status -----------------------------------------
62
+
63
+ app.get("/skills/inspector", (_req, res) => {
64
+ try {
65
+ const cfg = readConfig();
66
+ res.json({
67
+ config: mergedInspectorConfig(cfg),
68
+ defaults: INSPECTOR_DEFAULTS,
69
+ keys: KNOWN_KEYS,
70
+ index: indexStatus(),
71
+ });
72
+ } catch (e) {
73
+ res.status(500).json({ error: e.message });
74
+ }
75
+ });
76
+
77
+ app.put("/skills/inspector", (req, res) => {
78
+ try {
79
+ const patch = req.body || {};
80
+ const cfg = readConfig();
81
+ cfg.skills = cfg.skills || {};
82
+ const current = mergedInspectorConfig(cfg);
83
+ const next = { ...current };
84
+
85
+ for (const [k, v] of Object.entries(patch)) {
86
+ if (!KNOWN_KEYS.includes(k)) continue;
87
+ const def = INSPECTOR_DEFAULTS[k];
88
+ if (typeof def === "boolean") next[k] = !!v;
89
+ else if (typeof def === "number") {
90
+ const n = Number(v);
91
+ if (Number.isFinite(n)) next[k] = n;
92
+ } else {
93
+ next[k] = v;
94
+ }
95
+ }
96
+
97
+ cfg.skills.inspector = next;
98
+ writeConfig(cfg);
99
+ res.json({ ok: true, config: next, index: indexStatus() });
100
+ } catch (e) {
101
+ res.status(500).json({ error: e.message });
102
+ }
103
+ });
104
+
105
+ // ---- Index build --------------------------------------------------------
106
+
107
+ app.post("/skills/index", async (req, res) => {
108
+ try {
109
+ const { project_path, force } = req.body || {};
110
+ const cfg = readConfig();
111
+ const plan = planIndex({ projectPath: project_path });
112
+ const out = await ensureIndex({
113
+ projectPath: project_path,
114
+ embedOpts: { globalConfig: cfg },
115
+ force: !!force,
116
+ });
117
+ res.json({
118
+ ok: true,
119
+ embedder: out.embedder,
120
+ dim: out.dim,
121
+ planned: {
122
+ missing: plan.missing.length,
123
+ stale: plan.stale.length,
124
+ gone: plan.gone.length,
125
+ total: plan.total,
126
+ },
127
+ changed: {
128
+ added: out.changed.added.length,
129
+ refreshed: out.changed.refreshed.length,
130
+ removed: out.changed.removed.length,
131
+ kept: out.changed.kept.length,
132
+ },
133
+ index: indexStatus(),
134
+ });
135
+ } catch (e) {
136
+ res.status(500).json({ error: e.message });
137
+ }
138
+ });
139
+
140
+ // ---- Dry-run ------------------------------------------------------------
141
+
142
+ app.post("/skills/inspect", async (req, res) => {
143
+ try {
144
+ const { prompt, project_path } = req.body || {};
145
+ if (!prompt || typeof prompt !== "string") {
146
+ return res.status(400).json({ error: "prompt required" });
147
+ }
148
+ const cfg = readConfig();
149
+ // Force enabled for the dry-run so the operator sees what it WOULD do
150
+ // even when the feature is currently off.
151
+ const probed = structuredClone(cfg);
152
+ probed.skills = probed.skills || {};
153
+ probed.skills.inspector = { ...mergedInspectorConfig(cfg), enabled: true };
154
+ const out = await inspectPromptForSkills({
155
+ prompt,
156
+ projectPath: project_path,
157
+ globalConfig: probed,
158
+ });
159
+ res.json({ trace: out.trace, contextNote: out.contextNote });
160
+ } catch (e) {
161
+ res.status(500).json({ error: e.message });
162
+ }
163
+ });
30
164
  }
@@ -15,10 +15,30 @@ import { appendGlobalMessage } from "#core/stores/messages.js";
15
15
  import { createWebConfirmAdapter } from "#core/confirmation/adapters/web.js";
16
16
  import { tryResolveSkillCommand } from "#core/agent/skills/trigger.js";
17
17
  import { suggestSkillForPrompt } from "#core/agent/skills/rag.js";
18
+ import { inspectPromptForSkills, isInspectorEnabled, summarizeTrace } from "#core/agent/skills/inspector.js";
18
19
  import { CHANNELS } from "#core/constants/channels.js";
19
20
 
20
21
  const log = loggerFor("super-agent");
21
22
 
23
+ // Emit a single, readable line so `apx log -f` shows exactly what the skill
24
+ // inspector decided this turn (which skills it loaded/hinted, the embedder, and
25
+ // the top similarity). Best-effort: logging must never break a reply.
26
+ function logInspectorDecision(trace, { trace_id, channel } = {}) {
27
+ if (!trace) return;
28
+ try {
29
+ const top = trace.scored?.[0];
30
+ const topStr = top ? ` top=${top.slug}@${top.sim}` : "";
31
+ log.info(`skill inspector: ${summarizeTrace(trace)} [${trace.embedder || "?"}]${topStr}`, {
32
+ trace_id,
33
+ channel,
34
+ loaded: trace.loaded || [],
35
+ hinted: trace.hinted || [],
36
+ });
37
+ } catch {
38
+ /* logging is best-effort */
39
+ }
40
+ }
41
+
22
42
  // Persist human web turns to the cross-channel message store so they feed the
23
43
  // RAG index, search_messages, and the "active threads" awareness block. Only
24
44
  // the human surfaces (web big chat + sidebar) — not generic "api"/automation
@@ -79,10 +99,25 @@ export function register(app, { projects, registries, plugins, project, config }
79
99
  // slug is unknown.
80
100
  const slashed = tryResolveSkillCommand(rawPrompt, { projectPath: p.path });
81
101
  const prompt = slashed.handled ? slashed.prompt : rawPrompt;
102
+ const inspectorOn = isInspectorEnabled(config);
103
+ let inspectorTrace = null;
82
104
  if (slashed.handled) {
83
105
  ctx.contextNote = [ctx.contextNote, slashed.contextNote].filter(Boolean).join("\n\n");
106
+ } else if (inspectorOn) {
107
+ // Inspector middleware: per-turn semantic RAG. Replaces both the passive
108
+ // suggestSkillForPrompt nudge AND the static slug-dump in the system
109
+ // prompt — see runSuperAgent({ skipSkillsHint }).
110
+ const out = await inspectPromptForSkills({
111
+ prompt,
112
+ projectPath: p.path,
113
+ globalConfig: config,
114
+ });
115
+ inspectorTrace = out.trace;
116
+ if (out.contextNote) {
117
+ ctx.contextNote = [ctx.contextNote, out.contextNote].filter(Boolean).join("\n\n");
118
+ }
84
119
  } else {
85
- // Semantic skill nudge only when there was no explicit /slug.
120
+ // Legacy path — passive nudge, still works when inspector is off.
86
121
  const hint = await suggestSkillForPrompt(prompt, { projectPath: p.path });
87
122
  if (hint) ctx.contextNote = [ctx.contextNote, hint].filter(Boolean).join("\n\n");
88
123
  }
@@ -101,6 +136,14 @@ export function register(app, { projects, registries, plugins, project, config }
101
136
  channel: ctx.channel,
102
137
  });
103
138
 
139
+ // Surface the inspector decision to clients before model_start so the web
140
+ // debug panel / TUI can render "loaded: X" the moment the turn begins.
141
+ if (inspectorTrace) {
142
+ try { onEvent({ type: "skill_inspector", inspector: inspectorTrace }); }
143
+ catch { /* trace is best-effort */ }
144
+ logInspectorDecision(inspectorTrace, { trace_id: req.apxTraceId, channel: ctx.channel });
145
+ }
146
+
104
147
  // Web/TUI channels receive a "confirmation_required" SSE event and respond
105
148
  // via POST /super-agent/confirm/:correlationId (see api/confirm.js).
106
149
  const requestConfirmation = createWebConfirmAdapter({ onEvent });
@@ -122,6 +165,7 @@ export function register(app, { projects, registries, plugins, project, config }
122
165
  ...(completionContract ? { completionContract: true } : {}),
123
166
  onEvent,
124
167
  requestConfirmation,
168
+ skipSkillsHint: inspectorOn,
125
169
  });
126
170
  projects.rebuild(p.id);
127
171
  logWebTurn(ctx.channel, { prompt, replyText: saResult.text });
@@ -194,6 +238,16 @@ export function register(app, { projects, registries, plugins, project, config }
194
238
  req.body || {};
195
239
  if (!prompt) return res.status(400).json({ error: "prompt required" });
196
240
  const ctx = resolveSuperAgentContext(req, p);
241
+ const inspectorOn = isInspectorEnabled(config);
242
+ if (inspectorOn) {
243
+ try {
244
+ const out = await inspectPromptForSkills({ prompt, projectPath: p.path, globalConfig: config });
245
+ if (out.contextNote) {
246
+ ctx.contextNote = [ctx.contextNote, out.contextNote].filter(Boolean).join("\n\n");
247
+ }
248
+ logInspectorDecision(out.trace, { trace_id: req.apxTraceId, channel: ctx.channel });
249
+ } catch { /* inspector failure must not block the turn */ }
250
+ }
197
251
  try {
198
252
  const saResult = await runSuperAgent({
199
253
  globalConfig: config,
@@ -213,6 +267,7 @@ export function register(app, { projects, registries, plugins, project, config }
213
267
  trace_id: req.apxTraceId,
214
268
  channel: ctx.channel,
215
269
  }),
270
+ skipSkillsHint: inspectorOn,
216
271
  });
217
272
  projects.rebuild(p.id);
218
273
  logWebTurn(ctx.channel, { prompt, replyText: saResult.text });
@@ -218,6 +218,23 @@ async function main() {
218
218
  // store, and start the incremental RAG indexer. Best-effort — never blocks
219
219
  // boot and never throws into the daemon.
220
220
  initMemory({ config: cfg, log }).catch((e) => log(`memory: init failed: ${e?.message || e}`));
221
+ // Skill Inspector: if enabled, refresh its vector index in the background so
222
+ // any SKILL.md added/edited while the daemon was down is picked up without a
223
+ // manual `apx skills index`. Best-effort; never blocks boot.
224
+ (async () => {
225
+ try {
226
+ const { isInspectorEnabled } = await import("#core/agent/skills/inspector.js");
227
+ if (!isInspectorEnabled(cfg)) return;
228
+ const { backgroundRefreshIfStale } = await import("#core/agent/skills/index-store.js");
229
+ const r = backgroundRefreshIfStale({
230
+ embedOpts: { globalConfig: cfg },
231
+ onDone: (out) => log(`skill inspector: index refreshed (${out.embedder}, +${out.changed.added.length} -${out.changed.removed.length} ~${out.changed.refreshed.length})`),
232
+ });
233
+ if (r.started) log(`skill inspector: reindexing ${r.missing} new / ${r.stale} stale / ${r.gone} gone skills…`);
234
+ } catch (e) {
235
+ log(`skill inspector: index refresh skipped (${e?.message || e})`);
236
+ }
237
+ })();
221
238
  // Fire wake-up message after a short delay so plugins (Telegram) are ready
222
239
  setTimeout(() => triggerWakeup(cfg, log), 3000);
223
240
  // Preload whisper-server in the background so first desktop transcription is fast.
@@ -0,0 +1,53 @@
1
+ // APX CLI branding — a consistent "you're running APX vX" mark on every command.
2
+ //
3
+ // Two shapes:
4
+ // apxBanner(version, subtitle) big ASCII wordmark for branding-heavy moments
5
+ // (onboarding, top-level entry). Loud on purpose.
6
+ // apxHeader(version, subtitle) one-line "▸ APX CLI · vX · <subtitle>" for the
7
+ // everyday commands. Quiet, never in the way.
8
+ //
9
+ // Both write to STDERR so they never pollute piped stdout (`apx exec … | jq`,
10
+ // `apx config show > file`). Like mascot.js, they always print (so the mark is
11
+ // truly on every run), and self-suppress only when APX_QUIET / APX_NO_BANNER is
12
+ // set — the escape hatch for scripts and CI.
13
+ //
14
+ // Color: reuses raw ANSI like mascot.js. Honors NO_COLOR.
15
+
16
+ const NO_COLOR = !!process.env.NO_COLOR;
17
+ const c = (code) => (s) => (NO_COLOR ? s : `\x1b[${code}m${s}\x1b[0m`);
18
+ const B = c("1");
19
+ const DI = c("2");
20
+ const GR = c("32");
21
+ const CY = c("36");
22
+ const WH = c("97");
23
+
24
+ function suppressed() {
25
+ return !!(process.env.APX_NO_BANNER || process.env.APX_QUIET);
26
+ }
27
+
28
+ // Compact, single-line header. The default for everyday subcommands.
29
+ // ▸ APX CLI · v1.34.0 · skills inspector
30
+ export function apxHeader(version, subtitle = "") {
31
+ if (suppressed()) return;
32
+ const tag = `${GR("▸")} ${B(WH("APX"))} ${DI("CLI")}`;
33
+ const ver = DI(`v${version}`);
34
+ const sub = subtitle ? ` ${DI("·")} ${CY(subtitle)}` : "";
35
+ process.stderr.write(`\n${tag} ${DI("·")} ${ver}${sub}\n\n`);
36
+ }
37
+
38
+ // Big ASCII wordmark for branding-heavy commands.
39
+ export function apxBanner(version, subtitle = "") {
40
+ if (suppressed()) return;
41
+ const g = (s) => GR(s);
42
+ const lines = [
43
+ "",
44
+ ` ${g("█████╗ ██████╗ ██╗ ██╗")}`,
45
+ ` ${g("██╔══██╗██╔══██╗╚██╗██╔╝")}`,
46
+ ` ${g("███████║██████╔╝ ╚███╔╝ ")} ${B(WH("Agent Project Context"))}`,
47
+ ` ${g("██╔══██║██╔═══╝ ██╔██╗ ")} ${DI(`v${version}`)}`,
48
+ ` ${g("██║ ██║██║ ██╔╝ ██╗")}${subtitle ? ` ${CY(subtitle)}` : ""}`,
49
+ ` ${g("╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝")}`,
50
+ "",
51
+ ];
52
+ process.stderr.write(lines.join("\n") + "\n");
53
+ }