@agentprojectcontext/apx 1.33.0 → 1.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/package.json +1 -1
  2. package/skills/apc-context/SKILL.md +2 -5
  3. package/skills/apx/SKILL.md +49 -61
  4. package/src/core/agent/a2a/reply.js +48 -0
  5. package/src/core/agent/build-agent-system.js +4 -3
  6. package/src/core/agent/channels/voice-context.js +98 -0
  7. package/src/core/agent/memory.js +2 -1
  8. package/src/core/agent/prompt-builder.js +2 -1
  9. package/src/core/agent/prompts/modes/code-build.md +1 -0
  10. package/src/core/agent/prompts/modes/code-plan.md +1 -0
  11. package/src/core/agent/prompts/modes/index.js +28 -0
  12. package/src/core/agent/skills/loader.js +22 -18
  13. package/src/core/agent/stream/turn-accumulator.js +73 -0
  14. package/src/core/agent/suggestions.js +37 -0
  15. package/src/core/agent/tools/handlers/add-project.js +5 -2
  16. package/src/core/agent/tools/handlers/call-runtime.js +3 -2
  17. package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
  18. package/src/core/agent/tools/helpers.js +2 -2
  19. package/src/core/agent/tools/names.js +138 -0
  20. package/src/core/agent/tools/registry-bridge.js +6 -14
  21. package/src/core/agent/tools/registry.js +68 -65
  22. package/src/core/apc/context-copy.js +27 -0
  23. package/src/core/apc/notes.js +19 -0
  24. package/src/core/apc/parser.js +13 -6
  25. package/src/core/apc/paths.js +87 -0
  26. package/src/core/apc/scaffold.js +82 -74
  27. package/src/core/apc/skill-sync.js +13 -1
  28. package/src/core/channels/telegram/dispatch.js +595 -0
  29. package/src/core/channels/telegram/helpers.js +130 -0
  30. package/src/core/config/index.js +3 -2
  31. package/src/core/config/redact.js +95 -0
  32. package/src/core/constants/channels.js +2 -0
  33. package/src/core/constants/code-modes.js +10 -0
  34. package/src/core/constants/index.js +1 -0
  35. package/src/core/deck/manifest.js +186 -0
  36. package/src/core/engines/catalog.js +83 -0
  37. package/src/core/engines/gemini.js +28 -11
  38. package/src/core/engines/index.js +11 -1
  39. package/src/core/{tools → http-tools}/browser.js +0 -1
  40. package/src/core/{tools → http-tools}/fetch.js +0 -1
  41. package/src/core/{tools → http-tools}/glob.js +0 -1
  42. package/src/core/{tools → http-tools}/grep.js +0 -1
  43. package/src/core/{tools → http-tools}/registry.js +0 -1
  44. package/src/core/{tools → http-tools}/search.js +0 -1
  45. package/src/core/i18n/en.js +9 -0
  46. package/src/core/i18n/es.js +12 -0
  47. package/src/core/i18n/index.js +54 -0
  48. package/src/core/i18n/pt.js +9 -0
  49. package/src/core/identity/telegram.js +2 -1
  50. package/src/core/mcp/runner.js +272 -14
  51. package/src/core/mcp/sources.js +3 -2
  52. package/src/core/routines/index.js +16 -0
  53. package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
  54. package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
  55. package/src/core/runtime-skills/apx/SKILL.md +95 -0
  56. package/src/core/runtime-skills/apx-mcp/SKILL.md +116 -0
  57. package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
  58. package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
  59. package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
  60. package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
  61. package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
  62. package/src/core/stores/code-sessions.js +50 -2
  63. package/src/core/stores/routine-memory.js +1 -1
  64. package/src/core/stores/sessions-search.js +121 -0
  65. package/src/core/stores/sessions.js +38 -0
  66. package/src/core/vars/index.js +14 -0
  67. package/src/core/vars/interpolate.js +86 -0
  68. package/src/core/vars/sources.js +151 -0
  69. package/src/core/voice/audio-decode.js +38 -0
  70. package/src/core/voice/transcription.js +225 -0
  71. package/src/host/daemon/api/admin-config.js +5 -82
  72. package/src/host/daemon/api/agents.js +5 -5
  73. package/src/host/daemon/api/code.js +17 -169
  74. package/src/host/daemon/api/config.js +3 -4
  75. package/src/host/daemon/api/conversations.js +8 -29
  76. package/src/host/daemon/api/deck.js +37 -404
  77. package/src/host/daemon/api/engines.js +1 -50
  78. package/src/host/daemon/api/exec.js +1 -1
  79. package/src/host/daemon/api/mcps.js +32 -0
  80. package/src/host/daemon/api/routines.js +1 -1
  81. package/src/host/daemon/api/runtimes.js +4 -3
  82. package/src/host/daemon/api/sessions-search.js +24 -140
  83. package/src/host/daemon/api/sessions.js +12 -30
  84. package/src/host/daemon/api/shared.js +2 -1
  85. package/src/host/daemon/api/telegram.js +1 -11
  86. package/src/host/daemon/api/tools.js +6 -6
  87. package/src/host/daemon/api/transcribe.js +2 -2
  88. package/src/host/daemon/api/vars.js +137 -0
  89. package/src/host/daemon/api/voice.js +13 -290
  90. package/src/host/daemon/api.js +2 -0
  91. package/src/host/daemon/db.js +6 -6
  92. package/src/host/daemon/deck-exec.js +148 -0
  93. package/src/host/daemon/index.js +3 -3
  94. package/src/host/daemon/plugins/telegram/index.js +24 -687
  95. package/src/host/daemon/routines-scheduler.js +64 -0
  96. package/src/host/daemon/smoke.js +3 -2
  97. package/src/host/daemon/whisper-server.js +225 -0
  98. package/src/interfaces/cli/commands/agent.js +3 -2
  99. package/src/interfaces/cli/commands/command.js +2 -3
  100. package/src/interfaces/cli/commands/messages.js +6 -2
  101. package/src/interfaces/cli/commands/pair.js +5 -4
  102. package/src/interfaces/cli/commands/search.js +1 -1
  103. package/src/interfaces/cli/commands/sessions.js +3 -2
  104. package/src/interfaces/cli/commands/skills.js +36 -55
  105. package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +1 -0
  106. package/src/interfaces/web/dist/assets/index-M4FspaCH.js +613 -0
  107. package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +1 -0
  108. package/src/interfaces/web/dist/index.html +2 -2
  109. package/src/interfaces/web/package-lock.json +182 -182
  110. package/src/interfaces/web/src/components/ModelCombobox.tsx +44 -8
  111. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
  112. package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
  113. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
  114. package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
  115. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
  116. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
  117. package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
  118. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
  119. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
  120. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
  121. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
  122. package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
  123. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
  124. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
  125. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
  126. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
  127. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +5 -4
  128. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
  129. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
  130. package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
  131. package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
  132. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
  133. package/src/interfaces/web/src/constants/index.ts +1 -1
  134. package/src/interfaces/web/src/i18n/en.ts +174 -7
  135. package/src/interfaces/web/src/i18n/es.ts +179 -15
  136. package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
  137. package/src/interfaces/web/src/lib/api/vars.ts +38 -0
  138. package/src/interfaces/web/src/lib/api.ts +1 -0
  139. package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
  140. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  141. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
  142. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
  143. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
  144. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
  145. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
  146. package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
  147. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
  148. package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
  149. package/src/interfaces/web/src/types/daemon.ts +5 -0
  150. package/src/host/daemon/transcription.js +0 -538
  151. package/src/host/daemon/whisper-transcribe.py +0 -73
  152. package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +0 -1
  153. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +0 -602
  154. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js.map +0 -1
  155. /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
  156. /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
  157. /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
  158. /package/src/core/{tools → http-tools}/index.js +0 -0
  159. /package/{skills → src/core/runtime-skills}/apx-agency-agents/SKILL.md +0 -0
  160. /package/{skills → src/core/runtime-skills}/apx-agent/SKILL.md +0 -0
  161. /package/{skills → src/core/runtime-skills}/apx-mcp-builder/SKILL.md +0 -0
  162. /package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +0 -0
  163. /package/{skills → src/core/runtime-skills}/apx-routine/SKILL.md +0 -0
  164. /package/{skills → src/core/runtime-skills}/apx-runtime/SKILL.md +0 -0
  165. /package/{skills → src/core/runtime-skills}/apx-sessions/SKILL.md +0 -0
  166. /package/{skills → src/core/runtime-skills}/apx-skill-builder/SKILL.md +0 -0
  167. /package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +0 -0
  168. /package/{skills → src/core/runtime-skills}/apx-telegram/SKILL.md +0 -0
  169. /package/{skills → src/core/runtime-skills}/apx-voice/SKILL.md +0 -0
  170. /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
  171. /package/src/{host/daemon → core/stores}/conversations.js +0 -0
  172. /package/src/{host/daemon → core/util}/thinking.js +0 -0
@@ -2,169 +2,53 @@
2
2
  // GET /sessions/search?q=…&project=…&limit=20
3
3
  // POST /sessions/:id/compact resolves which project/agent owns the file
4
4
  // then delegates to compactConversation.
5
- import fs from "node:fs";
6
5
  import path from "node:path";
7
6
  import { readAgents } from "#core/apc/parser.js";
8
- import { compactConversation } from "../compact.js";
7
+ import { compactConversation } from "#core/stores/conversations-compactor.js";
8
+ import { searchSessions, findSessionFile } from "#core/stores/sessions-search.js";
9
+
10
+ function resolveProjects(projects, projectRef) {
11
+ const all = projects.list();
12
+ if (projectRef != null) {
13
+ const ref = String(projectRef);
14
+ const found = all.find((p) => String(p.id) === ref || p.path === path.resolve(ref));
15
+ return found ? [projects.get(found.id)] : [];
16
+ }
17
+ return all.map((p) => projects.get(p.id)).filter(Boolean);
18
+ }
9
19
 
10
20
  export function register(app, { projects, config }) {
11
21
  app.get("/sessions/search", (req, res) => {
12
22
  const { q, project: projectRef, limit = "20" } = req.query;
13
23
  if (!q) return res.status(400).json({ error: "q required" });
14
24
  const lim = Math.min(parseInt(limit, 10) || 20, 200);
15
- const needle = q.toLowerCase();
16
-
17
- const allProjects = projects.list();
18
- const targetProjects = (() => {
19
- if (projectRef != null) {
20
- const ref = String(projectRef);
21
- const found = allProjects.find(
22
- (p) => String(p.id) === ref || p.path === path.resolve(ref)
23
- );
24
- return found ? [projects.get(found.id)] : [];
25
- }
26
- return allProjects.map((p) => projects.get(p.id)).filter(Boolean);
27
- })();
28
-
29
- const matches = [];
30
-
31
- for (const p of targetProjects) {
32
- if (!p) continue;
33
-
34
- // 1) Legacy session files in the repo (.apc/agents/<slug>/sessions/)
35
- const sessionAgentsDir = path.join(p.path, ".apc", "agents");
36
- if (fs.existsSync(sessionAgentsDir)) {
37
- for (const slug of fs.readdirSync(sessionAgentsDir)) {
38
- const sessionsDir = path.join(sessionAgentsDir, slug, "sessions");
39
- if (!fs.existsSync(sessionsDir)) continue;
40
- for (const f of fs
41
- .readdirSync(sessionsDir)
42
- .filter((x) => x.endsWith(".md"))) {
43
- const filePath = path.join(sessionsDir, f);
44
- try {
45
- const text = fs.readFileSync(filePath, "utf8");
46
- if (text.toLowerCase().includes(needle)) {
47
- const lines = text.split("\n");
48
- const matchLine = lines.findIndex((l) =>
49
- l.toLowerCase().includes(needle)
50
- );
51
- const excerpt = lines
52
- .slice(Math.max(0, matchLine - 1), matchLine + 3)
53
- .join("\n");
54
- matches.push({
55
- type: "session",
56
- project: p.id,
57
- agent: slug,
58
- filename: f,
59
- path: filePath,
60
- excerpt: excerpt.slice(0, 300),
61
- });
62
- if (matches.length >= lim) break;
63
- }
64
- } catch {}
65
- }
66
- if (matches.length >= lim) break;
67
- }
68
- }
69
-
70
- if (matches.length >= lim) break;
71
-
72
- // 2) Conversation files in daemon storage (~/.apx/…/conversations/)
73
- const convAgentsDir = path.join(p.storagePath, "agents");
74
- if (fs.existsSync(convAgentsDir)) {
75
- for (const slug of fs.readdirSync(convAgentsDir)) {
76
- const convDir = path.join(convAgentsDir, slug, "conversations");
77
- if (!fs.existsSync(convDir)) continue;
78
- for (const f of fs
79
- .readdirSync(convDir)
80
- .filter((x) => x.endsWith(".md"))) {
81
- const filePath = path.join(convDir, f);
82
- try {
83
- const text = fs.readFileSync(filePath, "utf8");
84
- if (text.toLowerCase().includes(needle)) {
85
- const lines = text.split("\n");
86
- const matchLine = lines.findIndex((l) =>
87
- l.toLowerCase().includes(needle)
88
- );
89
- const excerpt = lines
90
- .slice(Math.max(0, matchLine - 1), matchLine + 3)
91
- .join("\n");
92
- matches.push({
93
- type: "conversation",
94
- project: p.id,
95
- agent: slug,
96
- filename: f,
97
- path: filePath,
98
- excerpt: excerpt.slice(0, 300),
99
- });
100
- if (matches.length >= lim) break;
101
- }
102
- } catch {}
103
- }
104
- if (matches.length >= lim) break;
105
- }
106
- }
107
-
108
- if (matches.length >= lim) break;
109
- }
110
-
111
- res.json({ q, count: matches.length, results: matches });
25
+ const targets = resolveProjects(projects, projectRef);
26
+ const results = searchSessions(targets, q, lim);
27
+ res.json({ q, count: results.length, results });
112
28
  });
113
29
 
114
30
  app.post("/sessions/:id/compact", async (req, res) => {
115
31
  const { id } = req.params;
116
32
  const { model: modelOverride, project: projectRef } = req.body || {};
117
-
118
- const candidates =
119
- projectRef != null
120
- ? (() => {
121
- const ref = String(projectRef);
122
- const found = projects
123
- .list()
124
- .find(
125
- (p) => String(p.id) === ref || p.path === path.resolve(ref)
126
- );
127
- return found ? [projects.get(found.id)] : [];
128
- })()
129
- : projects.list().map((p) => projects.get(p.id)).filter(Boolean);
130
-
131
- let found = null;
132
- const filename = id.endsWith(".md") ? id : `${id}.md`;
133
-
134
- for (const p of candidates) {
135
- if (!p) continue;
136
- const agentsDir = path.join(p.storagePath, "agents");
137
- if (fs.existsSync(agentsDir)) {
138
- for (const slug of fs.readdirSync(agentsDir)) {
139
- const f = path.join(agentsDir, slug, "conversations", filename);
140
- if (fs.existsSync(f)) {
141
- found = { p, slug };
142
- break;
143
- }
144
- }
145
- }
146
- if (found) break;
147
- }
33
+ const candidates = resolveProjects(projects, projectRef);
34
+ const found = findSessionFile(candidates, id);
148
35
 
149
36
  if (!found) {
150
- return res
151
- .status(404)
152
- .json({ error: `session/conversation "${id}" not found` });
37
+ return res.status(404).json({ error: `session/conversation "${id}" not found` });
153
38
  }
154
39
 
155
- const { p, slug } = found;
40
+ const { project: p, agentSlug, filename } = found;
156
41
  const agents = readAgents(p.path);
157
- const agent = agents.find((a) => a.slug === slug);
42
+ const agent = agents.find((a) => a.slug === agentSlug);
158
43
  const modelId = modelOverride || agent?.fields?.Model;
159
- if (!modelId)
160
- return res
161
- .status(400)
162
- .json({ error: "agent has no model; pass model in body" });
44
+ if (!modelId) {
45
+ return res.status(400).json({ error: "agent has no model; pass model in body" });
46
+ }
163
47
 
164
48
  try {
165
49
  const result = await compactConversation({
166
50
  storagePath: p.storagePath,
167
- agentSlug: slug,
51
+ agentSlug,
168
52
  filename,
169
53
  modelId,
170
54
  config: p.config || config,
@@ -4,9 +4,15 @@
4
4
  // GET /projects/:pid/sessions/:sid by filename (cross-agent lookup)
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
- import { parseSessionFrontmatter, readAgents } from "#core/apc/parser.js";
7
+ import { readAgents } from "#core/apc/parser.js";
8
+ import {
9
+ parseSessionFrontmatter,
10
+ } from "#core/apc/parser.js";
11
+ import {
12
+ agentSessionsDir,
13
+ createAgentSessionFile,
14
+ } from "#core/stores/sessions.js";
8
15
  import { collectAllSessions } from "#interfaces/cli/commands/sessions.js";
9
- import { nowIso } from "./shared.js";
10
16
 
11
17
  export function register(app, { projects, project }) {
12
18
  // Cross-engine sessions (apx · claude · codex), newest first.
@@ -29,12 +35,7 @@ export function register(app, { projects, project }) {
29
35
  const agents = readAgents(p.path);
30
36
  if (!agents.find((a) => a.slug === req.params.slug))
31
37
  return res.status(404).json({ error: "agent not found" });
32
- const sessionsDir = path.join(
33
- p.storagePath,
34
- "agents",
35
- req.params.slug,
36
- "sessions"
37
- );
38
+ const sessionsDir = agentSessionsDir(p.storagePath, req.params.slug);
38
39
  if (!fs.existsSync(sessionsDir)) return res.json([]);
39
40
  const sessions = fs
40
41
  .readdirSync(sessionsDir)
@@ -62,32 +63,13 @@ export function register(app, { projects, project }) {
62
63
  if (!p) return;
63
64
  const { title, body = "" } = req.body || {};
64
65
  if (!title) return res.status(400).json({ error: "title required" });
65
- const sessionsDir = path.join(
66
+ const { filename, path: filePath } = createAgentSessionFile(
66
67
  p.storagePath,
67
- "agents",
68
68
  req.params.slug,
69
- "sessions"
69
+ { title, body }
70
70
  );
71
- fs.mkdirSync(sessionsDir, { recursive: true });
72
- const titleSlug =
73
- title
74
- .toLowerCase()
75
- .replace(/[^a-z0-9]+/g, "-")
76
- .replace(/^-|-$/g, "") || "session";
77
- const today = new Date().toISOString().slice(0, 10);
78
- let candidate = path.join(sessionsDir, `${today}-${titleSlug}.md`);
79
- let n = 2;
80
- while (fs.existsSync(candidate)) {
81
- candidate = path.join(sessionsDir, `${today}-${titleSlug}-${n}.md`);
82
- n++;
83
- }
84
- const started = nowIso();
85
- const content = `---\ntitle: ${title}\nstarted: ${started}\n---\n\n# ${title}\n\n${body}\n`;
86
- fs.writeFileSync(candidate, content);
87
71
  projects.rebuild(p.id);
88
- res
89
- .status(201)
90
- .json({ filename: path.basename(candidate), path: candidate });
72
+ res.status(201).json({ filename, path: filePath });
91
73
  });
92
74
 
93
75
  // GET session by filename (sid may include or omit the .md extension)
@@ -8,6 +8,7 @@ import { randomUUID } from "node:crypto";
8
8
  import { appendErrorTrace, previewText } from "#core/logging.js";
9
9
  import { readAgents } from "#core/apc/parser.js";
10
10
  import { agentMemoryPath } from "#core/agent/memory.js";
11
+ import { apcMemoryFile } from "#core/apc/paths.js";
11
12
  import { CHANNELS } from "#core/constants/channels.js";
12
13
 
13
14
  export const nowIso = () =>
@@ -117,7 +118,7 @@ export function makeTopProjectResolver(projects) {
117
118
  export function resolveMemoryPath(p) {
118
119
  const firstAgent = readAgents(p.path)[0];
119
120
  if (firstAgent) return agentMemoryPath(p, firstAgent.slug);
120
- return path.join(p.path, ".apc", "memory.md");
121
+ return apcMemoryFile(p.path);
121
122
  }
122
123
 
123
124
  // Channel context passed to the super-agent loop. `api` is the default when
@@ -39,17 +39,7 @@ import {
39
39
  removeRole,
40
40
  } from "#core/config/index.js";
41
41
 
42
- function redactChannel(channel) {
43
- if (!channel?.bot_token) return channel;
44
- return {
45
- ...channel,
46
- bot_token: `*** set *** (...${String(channel.bot_token).slice(-5)})`,
47
- };
48
- }
49
-
50
- function isSecretMarker(value) {
51
- return typeof value === "string" && value.startsWith("*** set ***");
52
- }
42
+ import { redactChannel, isSecretMarker } from "#core/config/redact.js";
53
43
 
54
44
  export function register(app, { telegram }) {
55
45
  app.get("/telegram/status", (_req, res) => {
@@ -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 "#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";
7
+ import { buildBrowserRouter } from "#core/http-tools/browser.js";
8
+ import { buildFetchRouter } from "#core/http-tools/fetch.js";
9
+ import { buildSearchRouter } from "#core/http-tools/search.js";
10
+ import { buildRegistryRouter } from "#core/http-tools/registry.js";
11
+ import { buildGlobRouter } from "#core/http-tools/glob.js";
12
+ import { buildGrepRouter } from "#core/http-tools/grep.js";
13
13
 
14
14
  export function register(app, { express, projects, registries }) {
15
15
  app.use("/tools/fetch", buildFetchRouter(express));
@@ -11,7 +11,7 @@ export function register(app) {
11
11
  // the first real utterance doesn't pay the cold-load cost.
12
12
  app.get("/transcribe/warmup", async (_req, res) => {
13
13
  try {
14
- const { warmupWhisper } = await import("../transcription.js");
14
+ const { warmupWhisper } = await import("../whisper-server.js");
15
15
  res.json(await warmupWhisper());
16
16
  } catch (e) {
17
17
  res.status(500).json({ ok: false, error: e.message });
@@ -29,7 +29,7 @@ export function register(app) {
29
29
  const language = req.headers["x-language"] || "auto";
30
30
  const provider = req.headers["x-provider"];
31
31
  try {
32
- const { transcribeBuffer } = await import("../transcription.js");
32
+ const { transcribeBuffer } = await import("#core/voice/transcription.js");
33
33
  const result = await transcribeBuffer(buf, format, {
34
34
  language: language === "auto" ? undefined : language,
35
35
  beam_size: 3,
@@ -0,0 +1,137 @@
1
+ // Variable management per project. Reads/writes the two APX-owned scopes
2
+ // (project = <storagePath>/vars.json, global = ~/.apx/vars.json) and
3
+ // surfaces a merged effective view with sources annotated.
4
+ //
5
+ // GET /projects/:pid/vars -> { project, global, effective, sources }
6
+ // values masked unless ?reveal=1
7
+ // GET /projects/:pid/vars/:name -> { name, scope, value, masked }
8
+ // ?reveal=1 unmasks
9
+ // POST /projects/:pid/vars -> { ok, name, scope }
10
+ // body { name, value, scope }
11
+ // scope defaults to "project" (or
12
+ // "global" if pid=0).
13
+ // DELETE /projects/:pid/vars/:name?scope=… 204
14
+ //
15
+ // pid=0 (base project) is the conventional bucket for editing global vars
16
+ // from the web UI; project scope is rejected there because there is no
17
+ // storagePath that belongs to a real project.
18
+ import {
19
+ loadAllVars,
20
+ readGlobalVars,
21
+ readProjectVars,
22
+ setVar,
23
+ deleteVar,
24
+ maskValue,
25
+ } from "#core/vars/index.js";
26
+
27
+ function normalizeScope(raw, { isBase }) {
28
+ if (!raw) return isBase ? "global" : "project";
29
+ const s = String(raw).toLowerCase();
30
+ if (s === "project" || s === "global") return s;
31
+ return null;
32
+ }
33
+
34
+ function maskAll(obj) {
35
+ const out = {};
36
+ for (const [k, v] of Object.entries(obj)) out[k] = maskValue(v);
37
+ return out;
38
+ }
39
+
40
+ export function register(app, { project }) {
41
+ app.get("/projects/:pid/vars", (req, res) => {
42
+ const p = project(req, res);
43
+ if (!p) return;
44
+ const reveal = req.query?.reveal === "1" || req.query?.reveal === "true";
45
+ const { project: proj, global, effective, sources } = loadAllVars({
46
+ storagePath: p.storagePath,
47
+ });
48
+ res.json({
49
+ scope_hint: String(p.id) === "0" ? "global" : "project",
50
+ project: reveal ? proj : maskAll(proj),
51
+ global: reveal ? global : maskAll(global),
52
+ effective: reveal ? effective : maskAll(effective),
53
+ sources,
54
+ });
55
+ });
56
+
57
+ app.get("/projects/:pid/vars/:name", (req, res) => {
58
+ const p = project(req, res);
59
+ if (!p) return;
60
+ const name = req.params.name;
61
+ const proj = p.storagePath ? readProjectVars(p.storagePath) : {};
62
+ const global = readGlobalVars();
63
+ let scope = null;
64
+ let value = null;
65
+ if (Object.prototype.hasOwnProperty.call(proj, name)) {
66
+ scope = "project";
67
+ value = proj[name];
68
+ } else if (Object.prototype.hasOwnProperty.call(global, name)) {
69
+ scope = "global";
70
+ value = global[name];
71
+ } else {
72
+ return res.status(404).json({ error: `variable "${name}" not found` });
73
+ }
74
+ const reveal = req.query?.reveal === "1" || req.query?.reveal === "true";
75
+ res.json({
76
+ name,
77
+ scope,
78
+ value: reveal ? value : maskValue(value),
79
+ masked: !reveal,
80
+ });
81
+ });
82
+
83
+ app.post("/projects/:pid/vars", (req, res) => {
84
+ const p = project(req, res);
85
+ if (!p) return;
86
+ const { name, value } = req.body || {};
87
+ if (!name || typeof name !== "string") {
88
+ return res.status(400).json({ error: "name required" });
89
+ }
90
+ if (value === undefined || value === null) {
91
+ return res.status(400).json({ error: "value required" });
92
+ }
93
+ const isBase = String(p.id) === "0";
94
+ const scope = normalizeScope(req.body?.scope, { isBase });
95
+ if (scope === null) {
96
+ return res
97
+ .status(400)
98
+ .json({ error: `unknown scope "${req.body?.scope}" (use project|global)` });
99
+ }
100
+ if (scope === "project" && (!p.storagePath || isBase)) {
101
+ return res.status(400).json({
102
+ error: "project scope is not available for the base workspace — use scope=global",
103
+ });
104
+ }
105
+ try {
106
+ setVar({ storagePath: p.storagePath, scope, name, value });
107
+ } catch (e) {
108
+ return res.status(400).json({ error: e.message });
109
+ }
110
+ res.status(201).json({ ok: true, name, scope });
111
+ });
112
+
113
+ app.delete("/projects/:pid/vars/:name", (req, res) => {
114
+ const p = project(req, res);
115
+ if (!p) return;
116
+ const isBase = String(p.id) === "0";
117
+ const scope = normalizeScope(req.query?.scope, { isBase });
118
+ if (scope === null) {
119
+ return res
120
+ .status(400)
121
+ .json({ error: `unknown scope "${req.query?.scope}" (use project|global)` });
122
+ }
123
+ if (scope === "project" && (!p.storagePath || isBase)) {
124
+ return res.status(400).json({
125
+ error: "project scope is not available for the base workspace",
126
+ });
127
+ }
128
+ let removed;
129
+ try {
130
+ removed = deleteVar({ storagePath: p.storagePath, scope, name: req.params.name });
131
+ } catch (e) {
132
+ return res.status(400).json({ error: e.message });
133
+ }
134
+ if (!removed) return res.status(404).end();
135
+ res.status(204).end();
136
+ });
137
+ }