@agentprojectcontext/apx 1.36.0 → 1.38.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 (79) hide show
  1. package/README.md +81 -3
  2. package/package.json +1 -1
  3. package/src/core/mascot.js +80 -80
  4. package/src/host/daemon/api/agents.js +6 -0
  5. package/src/host/daemon/api/conversations.js +9 -2
  6. package/src/host/daemon/api/web.js +20 -1
  7. package/src/host/daemon/desktop-ws.js +31 -0
  8. package/src/host/daemon/index.js +12 -2
  9. package/src/interfaces/cli/commands/agent.js +20 -0
  10. package/src/interfaces/cli/commands/chat.js +15 -6
  11. package/src/interfaces/cli/commands/identity.js +20 -1
  12. package/src/interfaces/cli/commands/update.js +2 -0
  13. package/src/interfaces/cli/index.js +14 -0
  14. package/src/interfaces/web/dist/assets/index-CQc_5t8F.js +629 -0
  15. package/src/interfaces/web/dist/assets/index-CQc_5t8F.js.map +1 -0
  16. package/src/interfaces/web/dist/assets/index-hwxuTPcK.css +1 -0
  17. package/src/interfaces/web/dist/index.html +2 -2
  18. package/src/interfaces/web/src/App.tsx +20 -9
  19. package/src/interfaces/web/src/components/ModelCombobox.tsx +1 -1
  20. package/src/interfaces/web/src/components/Roby.tsx +96 -0
  21. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +11 -11
  22. package/src/interfaces/web/src/components/TelegramSendDialog.tsx +5 -5
  23. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +2 -2
  24. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +5 -5
  25. package/src/interfaces/web/src/components/chat/ToolCall.tsx +23 -19
  26. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +10 -10
  27. package/src/interfaces/web/src/components/code/CodeContextTab.tsx +7 -7
  28. package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +3 -2
  29. package/src/interfaces/web/src/components/common/TabNav.tsx +3 -2
  30. package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +3 -2
  31. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -2
  32. package/src/interfaces/web/src/components/config/global-config-sections.ts +9 -9
  33. package/src/interfaces/web/src/components/config/project-config-sections.ts +61 -54
  34. package/src/interfaces/web/src/components/deck/DaemonCard.tsx +6 -5
  35. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +5 -4
  36. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +3 -3
  37. package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +22 -9
  38. package/src/interfaces/web/src/components/settings/AdvancedPanel.tsx +1 -1
  39. package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
  40. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +14 -14
  41. package/src/interfaces/web/src/components/settings/DevicesPanel.tsx +3 -3
  42. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +7 -7
  43. package/src/interfaces/web/src/components/settings/IdentityPanel.tsx +2 -2
  44. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +37 -37
  45. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +44 -35
  46. package/src/interfaces/web/src/components/settings/SuperAgentPanel.tsx +5 -5
  47. package/src/interfaces/web/src/components/settings/TelegramChannelsPanel.tsx +3 -3
  48. package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +1 -1
  49. package/src/interfaces/web/src/components/settings/TelegramGlobalPanel.tsx +3 -3
  50. package/src/interfaces/web/src/components/settings/TelegramRolesPanel.tsx +1 -1
  51. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +6 -6
  52. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +36 -36
  53. package/src/interfaces/web/src/components/voice/VoiceProviderList.tsx +15 -14
  54. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +22 -22
  55. package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +18 -17
  56. package/src/interfaces/web/src/components/voice/VoiceTestCard.tsx +19 -18
  57. package/src/interfaces/web/src/hooks/useChat.ts +6 -5
  58. package/src/interfaces/web/src/i18n/en.ts +519 -2
  59. package/src/interfaces/web/src/i18n/es.ts +519 -2
  60. package/src/interfaces/web/src/i18n/index.ts +1 -1
  61. package/src/interfaces/web/src/lib/api/voice.ts +5 -5
  62. package/src/interfaces/web/src/screens/ProjectScreen.tsx +14 -1
  63. package/src/interfaces/web/src/screens/SettingsScreen.tsx +1 -1
  64. package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +8 -8
  65. package/src/interfaces/web/src/screens/base/ComingSoon.tsx +3 -2
  66. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +12 -12
  67. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +15 -15
  68. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +37 -37
  69. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +8 -8
  70. package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +16 -10
  71. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +25 -24
  72. package/src/interfaces/web/src/screens/project/ChatTab.tsx +2 -2
  73. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +3 -3
  74. package/src/interfaces/web/src/screens/project/McpsTab.tsx +6 -9
  75. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +66 -52
  76. package/src/interfaces/web/src/screens/project/TelegramTab.tsx +1 -1
  77. package/src/interfaces/web/dist/assets/index-Cm0KyPoZ.css +0 -1
  78. package/src/interfaces/web/dist/assets/index-DJKA763h.js +0 -628
  79. package/src/interfaces/web/dist/assets/index-DJKA763h.js.map +0 -1
package/README.md CHANGED
@@ -1,8 +1,30 @@
1
1
  <p align="center">
2
- <img src="assets/hero.png" alt="APX — Local Runtime for AI Agents" width="600">
2
+ <img src="assets/banner.svg" alt="APX — Agent Project eXecutable" width="820">
3
3
  </p>
4
4
 
5
- > The reference implementation of the [APC protocol](https://github.com/agentprojectcontext/agentprojectcontext).
5
+ <p align="center">
6
+ <b>APX</b> &mdash; <b>A</b>gent <b>P</b>roject e<b>X</b>ecutable.<br>
7
+ A local runtime, CLI and web admin for AI agents, built on the
8
+ <a href="https://github.com/agentprojectcontext/agentprojectcontext">APC protocol</a>.
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://agentprojectcontext.github.io/apx/"><img src="https://img.shields.io/badge/Website-agentprojectcontext.github.io-3fb950?style=flat-square&logo=googlechrome&logoColor=white" alt="Website"></a>
13
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-3fb950?style=flat-square" alt="License: MIT"></a>
14
+ <img src="https://img.shields.io/badge/Node.js-20%2B-3fb950?style=flat-square&logo=nodedotjs&logoColor=white" alt="Node.js 20+">
15
+ <a href="https://github.com/agentprojectcontext/agentprojectcontext"><img src="https://img.shields.io/badge/Protocol-APC-3fb950?style=flat-square" alt="APC protocol"></a>
16
+ </p>
17
+
18
+ <p align="center">
19
+ <b><a href="https://agentprojectcontext.github.io/apx/">🌐 Visit the website</a></b> &nbsp;&middot;&nbsp;
20
+ <a href="#quick-start">Quick start</a> &middot;
21
+ <a href="#examples">Examples</a> &middot;
22
+ <a href="#web-admin">Web admin</a> &middot;
23
+ <a href="#use-cases">Use cases</a> &middot;
24
+ <a href="https://github.com/agentprojectcontext/agentprojectcontext">APC spec</a>
25
+ </p>
26
+
27
+ > APX is the reference implementation of the [APC protocol](https://github.com/agentprojectcontext/agentprojectcontext).
6
28
  > APX is to APC what a language SDK is to a protocol spec.
7
29
 
8
30
  ## What APX is
@@ -11,6 +33,7 @@ APX is a daemon + CLI that brings the APC convention to life:
11
33
 
12
34
  - **Daemon** — a local HTTP server that manages projects, agents, sessions, and message logs
13
35
  - **CLI** (`apx`) — commands for running agents, reading memory, tailing messages, managing sessions
36
+ - **Web admin** — a local web UI served by the daemon to browse projects, agents, sessions, and MCPs from the browser
14
37
  - **Runtimes** — bridges to Claude Code, Codex, OpenCode, Aider
15
38
  - **Engines** — direct LLM calls via Anthropic, OpenAI, Gemini, Ollama, or a mock
16
39
  - **Plugins** — Telegram bot integration out of the box
@@ -18,12 +41,27 @@ APX is a daemon + CLI that brings the APC convention to life:
18
41
 
19
42
  APX is opinionated about storage: the filesystem is the source of truth. Project definitions and curated memory live in the repo. Runtime state such as sessions, conversations, messages, and caches lives in `~/.apx/` and is never committed.
20
43
 
44
+ <div align="center">
45
+ <pre>
46
+ ▄███████▄
47
+ █ ██ ██ █
48
+ █ ◕ ◕ █
49
+ █ ‿ █
50
+ ▀███████▀
51
+ </pre>
52
+ <sub>Meet <b>Roby</b> — your project's super-agent. He greets you across the <code>apx</code> CLI and the web admin.</sub>
53
+ </div>
54
+
21
55
  ## Quick start
22
56
 
23
57
  ```bash
58
+ # 1 · Install
24
59
  npm install -g apx
25
60
 
26
- # In any directory with an AGENTS.md
61
+ # 2 · Set up interactive wizard (provider → model → channels → daemon)
62
+ apx setup
63
+
64
+ # In any directory with an AGENTS.md, register the project
27
65
  apx init
28
66
 
29
67
  # Spawn an agent with a full external runtime
@@ -36,6 +74,27 @@ apx exec sofia "What is my role in this project?"
36
74
  apx messages tail
37
75
  ```
38
76
 
77
+ ## Examples
78
+
79
+ Real commands — copy one, point it at an agent like `sofia`, and APX routes it to the right
80
+ runtime. The session and memory land in `.apc/`.
81
+
82
+ | What | Command |
83
+ |------|---------|
84
+ | Register a project | `apx init` |
85
+ | Spawn an agent (full runtime) | `apx run sofia --runtime claude-code "Review the open PRs and summarize them"` |
86
+ | Ask a quick question (one-shot) | `apx exec sofia "What is my role in this project?"` |
87
+ | Read an agent's memory | `apx memory sofia` |
88
+ | Switch runtime, same context | `apx run sofia --runtime codex "Add tests for the parser"` |
89
+ | Watch what's happening | `apx messages tail` |
90
+
91
+ ## Use cases
92
+
93
+ - **Review PRs across any runtime** — point an agent at your repo; APX routes to Claude Code and falls back to Codex or OpenCode if one isn't installed. The session and its summary land in `.apc/`.
94
+ - **Operate your agents from Telegram** — talk to project agents from your phone. Identity roles gate who can do what, and every message is logged per channel for a full audit trail.
95
+ - **Memory that lives in your repo** — curated, per-agent memory is plain markdown, committed and reviewable alongside your code. No vendor database, no hidden state, no lock-in.
96
+ - **Run the same prompt across engines** — send one prompt through Anthropic, OpenAI, Gemini or a local Ollama model with `apx exec`, configured per project or globally.
97
+
39
98
  ## Installation
40
99
 
41
100
  ```bash
@@ -44,6 +103,25 @@ npm install -g apx
44
103
 
45
104
  Requires Node.js 20+. The daemon starts automatically on first `apx` call.
46
105
 
106
+ ## Web admin
107
+
108
+ APX ships a local **web admin** — the same runtime, in your browser. The daemon serves a
109
+ single-page app so you can browse and manage everything the CLI does without leaving the UI:
110
+
111
+ - **Projects & agents** — see registered projects, open agents, edit roles, models, and skills
112
+ - **Sessions & messages** — read past sessions and tail live activity across every channel
113
+ - **MCPs, engines & channels** — review MCP servers, configure engines, and manage Telegram/desktop
114
+
115
+ It runs entirely on your machine. Start the daemon (any `apx` call does this) and open:
116
+
117
+ ```bash
118
+ apx # ensures the daemon is up
119
+ open http://localhost:7430 # macOS — or just visit it in any browser
120
+ ```
121
+
122
+ The web admin is served from `src/interfaces/web/dist` at the daemon port (`7430` by default,
123
+ override with `APX_PORT`). Nothing is sent anywhere — it talks to the local daemon only.
124
+
47
125
  ## Project layout
48
126
 
49
127
  Project context — committed to the repository:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.36.0",
3
+ "version": "1.38.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,122 +1,122 @@
1
- // APX mascot — a panda that appears in different moods across the CLI.
1
+ // APX mascot — "Roby", a little terminal critter that shows up across the CLI.
2
2
  // Usage: import { mascot } from '#core/mascot.js'; mascot('happy');
3
+ //
4
+ // Same character as the web Splash/404 ("Roby"): a chunky ▄███████▄ head with
5
+ // two screen-eyes and a tiny mouth. Rendered as clean emerald-green line art on
6
+ // the terminal's own background — no heavy black-on-white blocks, no stray legs.
7
+ // Kept deliberately simple and a touch hand-made, not a photoreal sprite.
3
8
 
4
9
  const R = "\x1b[0m";
5
10
  const B = "\x1b[1m";
6
- const W = "\x1b[97m"; // bright white
7
- const BK = "\x1b[40m"; // bg black
8
- const CY = "\x1b[36m";
9
- const YE = "\x1b[33m";
10
- const GR = "\x1b[32m";
11
- const RE = "\x1b[31m";
12
11
  const DI = "\x1b[2m";
13
- const BL = "\x1b[34m";
14
12
 
15
- // Each mood: [panda lines, caption]
13
+ // Honor NO_COLOR; otherwise tint in emerald to match the web Roby (text-emerald).
14
+ const NO_COLOR = !!process.env.NO_COLOR;
15
+ const truecolor = /truecolor|24bit/i.test(process.env.COLORTERM || "");
16
+
17
+ function rgb(r, g, b) {
18
+ if (NO_COLOR) return "";
19
+ if (truecolor) return `\x1b[38;2;${r};${g};${b}m`;
20
+ // 6x6x6 cube fallback for 256-color terminals.
21
+ const q = (v) => Math.round((v / 255) * 5);
22
+ return `\x1b[38;5;${16 + 36 * q(r) + 6 * q(g) + q(b)}m`;
23
+ }
24
+
25
+ // Two emerald tones: a bright frame and a dimmer "screen" green for the eyes,
26
+ // so the face reads without resorting to a second background colour.
27
+ const G = rgb(52, 211, 153); // emerald-400 — frame, mouth, pupils
28
+ const GD = rgb(20, 120, 92); // dim emerald — recessed eye screens
29
+ const reset = NO_COLOR ? "" : R;
30
+ const bold = NO_COLOR ? "" : B;
31
+ const dim = NO_COLOR ? "" : DI;
32
+
33
+ // Caption colours per mood (still emerald-family, just a nudge of hue).
34
+ const C_OK = rgb(52, 211, 153);
35
+ const C_INFO = rgb(94, 198, 255);
36
+ const C_WARN = rgb(240, 200, 90);
37
+ const C_BAD = rgb(240, 110, 110);
38
+
39
+ // Build Roby's head. `eyes` is [left, right] pupils; `mouth` is one glyph;
40
+ // `top` is an optional floating accent that hovers over the head.
41
+ function buildRoby({ eyes, mouth, top = "" }) {
42
+ const [el, er] = eyes;
43
+ const lines = [];
44
+ lines.push(top ? ` ${dim}${top}${reset}` : "");
45
+ lines.push(` ${G}▄███████▄${reset}`);
46
+ // Eye screens: dim ██ sockets set into the bright frame.
47
+ lines.push(` ${G}█ ${GD}██${G} ${GD}██${G} █${reset}`);
48
+ // Pupils + mouth, each a single green run (clean, no inline colour breaks).
49
+ lines.push(` ${G}█ ${el} ${er} █${reset}`);
50
+ lines.push(` ${G}█ ${mouth} █${reset}`);
51
+ lines.push(` ${G}▀███████▀${reset}`);
52
+ return lines.filter((l, i) => !(i === 0 && l === ""));
53
+ }
54
+
16
55
  const MOODS = {
17
56
  // ─── happy: default greeting / daemon started ────────────────────────────
18
57
  happy: {
19
- color: GR,
20
- lines: [
21
- ` ${BK}${W} ▄███████▄ ${R}`,
22
- ` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
23
- ` ${BK}${W} █ ◕ ◕ █ ${R}`,
24
- ` ${BK}${W} █ ╰ω╯ █ ${R}`,
25
- ` ${BK}${W} ▀███████▀ ${R}`,
26
- ` ${DI} ╱ ╲ ╱ ╲ ${R}`,
27
- ],
28
- caption: `${GR}${B}ready to go!${R}`,
58
+ caption: "ready to go!",
59
+ color: C_OK,
60
+ lines: buildRoby({ eyes: ["◕", "◕"], mouth: "‿" }),
29
61
  },
30
62
 
31
63
  // ─── wave: first run / setup ─────────────────────────────────────────────
32
64
  wave: {
33
- color: CY,
34
- lines: [
35
- ` ${BK}${W} ▄███████▄ ${R} ${DI}/)${R}`,
36
- ` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R} ${DI}//${R}`,
37
- ` ${BK}${W} █ ◕ ◕ █ ${R} ${DI}//${R}`,
38
- ` ${BK}${W} █ ╰▽╯ █ ${R}${DI}/${R}`,
39
- ` ${BK}${W} ▀███████▀ ${R}`,
40
- ` ${DI} ╱ ╲ ╱ ╲ ${R}`,
41
- ],
42
- caption: `${CY}${B}APX — Agent Project Context${R}`,
65
+ caption: "APX — Agent Project Context",
66
+ color: C_OK,
67
+ lines: buildRoby({ eyes: ["◕", "◕"], mouth: "▽", top: "·" }),
43
68
  },
44
69
 
45
- // ─── confused: unknown command / not found ────────────────────────────────
70
+ // ─── confused: unknown command / not found (the web 404 "lost" Roby) ──────
46
71
  confused: {
47
- color: YE,
48
- lines: [
49
- ` ${BK}${W} ▄███████▄ ${R} ${YE}?${R}`,
50
- ` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
51
- ` ${BK}${W} █ ◔ ◔ █ ${R}`,
52
- ` ${BK}${W} █ ╰~╯ █ ${R}`,
53
- ` ${BK}${W} ▀███████▀ ${R}`,
54
- ` ${DI} ╱ ╲ ╱ ╲ ${R}`,
55
- ],
56
- caption: `${YE}${B}hmm, I don't know that one${R}`,
72
+ caption: "hmm, I don't know that one",
73
+ color: C_WARN,
74
+ lines: buildRoby({ eyes: ["◑", "◐"], mouth: "o", top: "?" }),
57
75
  },
58
76
 
59
77
  // ─── sad: error ───────────────────────────────────────────────────────────
60
78
  sad: {
61
- color: RE,
62
- lines: [
63
- ` ${BK}${W} ▄███████▄ ${R}`,
64
- ` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
65
- ` ${BK}${W} █ ╥ ╥ █ ${R}`,
66
- ` ${BK}${W} █ ╰︵╯ █ ${R}`,
67
- ` ${BK}${W} ▀███████▀ ${R}`,
68
- ` ${DI} ╱ ╲ ╱ ╲ ${R}`,
69
- ],
70
- caption: `${RE}${B}something went wrong${R}`,
79
+ caption: "something went wrong",
80
+ color: C_BAD,
81
+ lines: buildRoby({ eyes: ["╥", "╥"], mouth: "︵" }),
71
82
  },
72
83
 
73
84
  // ─── excited: update available ────────────────────────────────────────────
74
85
  excited: {
75
- color: BL,
76
- lines: [
77
- ` ${BK}${W} ▄███████▄ ${R} ${BL}↑${R}`,
78
- ` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
79
- ` ${BK}${W} █ ★ ★ █ ${R}`,
80
- ` ${BK}${W} █ ╰◡╯ █ ${R}`,
81
- ` ${BK}${W} ▀███████▀ ${R}`,
82
- ` ${DI} ╱ ╲ ╱ ╲ ${R}`,
83
- ],
84
- caption: `${BL}${B}new version available!${R}`,
86
+ caption: "new version available!",
87
+ color: C_INFO,
88
+ lines: buildRoby({ eyes: ["★", "★"], mouth: "▽", top: "✦" }),
85
89
  },
86
90
 
87
91
  // ─── sleeping: daemon not running ────────────────────────────────────────
88
92
  sleeping: {
89
- color: DI,
90
- lines: [
91
- ` ${BK}${W} ▄███████▄ ${R} ${DI}z z z${R}`,
92
- ` ${BK}${W} █ ${R}${B}██${R}${W} ${B}██${R}${BK}${W} █ ${R}`,
93
- ` ${BK}${W} █ − − █ ${R}`,
94
- ` ${BK}${W} █ ╰_╯ █ ${R}`,
95
- ` ${BK}${W} ▀███████▀ ${R}`,
96
- ` ${DI} ╱ ╲ ╱ ╲ ${R}`,
97
- ],
98
- caption: `${DI}${B}daemon is not running${R}`,
93
+ caption: "daemon is not running",
94
+ color: rgb(150, 150, 150),
95
+ lines: buildRoby({ eyes: ["−", "−"], mouth: "‿", top: "z z" }),
99
96
  },
100
97
  };
101
98
 
102
- // Print the mascot to stderr (doesn't interfere with piped output).
99
+ // Print the mascot to stderr (doesn't interfere with piped stdout).
103
100
  // mood: 'happy' | 'wave' | 'confused' | 'sad' | 'excited' | 'sleeping'
104
101
  export function mascot(mood = "happy", message = "") {
105
102
  const def = MOODS[mood] || MOODS.happy;
106
103
  const out = [
107
104
  "",
108
105
  ...def.lines,
109
- ` ${def.caption}`,
110
- message ? ` ${def.color}${message}${R}` : "",
111
106
  "",
112
- ].join("\n");
107
+ ` ${def.color}${bold}${def.caption}${reset}`,
108
+ message ? ` ${dim}${message}${reset}` : "",
109
+ "",
110
+ ]
111
+ .filter((l, i, a) => !(l === "" && a[i - 1] === ""))
112
+ .join("\n");
113
113
  process.stderr.write(out + "\n");
114
114
  }
115
115
 
116
116
  // One-liner for inline use: mascot.confused("apx: unknown command: foo")
117
- mascot.confused = (msg) => mascot("confused", msg);
118
- mascot.sad = (msg) => mascot("sad", msg);
119
- mascot.happy = (msg) => mascot("happy", msg);
120
- mascot.wave = (msg) => mascot("wave", msg);
121
- mascot.excited = (msg) => mascot("excited", msg);
122
- mascot.sleeping = (msg) => mascot("sleeping", msg);
117
+ mascot.confused = (msg) => mascot("confused", msg);
118
+ mascot.sad = (msg) => mascot("sad", msg);
119
+ mascot.happy = (msg) => mascot("happy", msg);
120
+ mascot.wave = (msg) => mascot("wave", msg);
121
+ mascot.excited = (msg) => mascot("excited", msg);
122
+ mascot.sleeping = (msg) => mascot("sleeping", msg);
@@ -245,12 +245,18 @@ export function register(app, { projects, project }) {
245
245
  app.get("/projects/:pid/agents/:slug/memory", (req, res) => {
246
246
  const p = project(req, res);
247
247
  if (!p) return;
248
+ // Validate the agent exists — otherwise an unknown slug returned 200 with an
249
+ // empty body, masking typos (QA BUG-API-1).
250
+ if (!readAgents(p.path).some((a) => a.slug === req.params.slug))
251
+ return res.status(404).json({ error: "agent not found" });
248
252
  res.json({ body: readAgentMemory(p, req.params.slug) });
249
253
  });
250
254
 
251
255
  app.put("/projects/:pid/agents/:slug/memory", (req, res) => {
252
256
  const p = project(req, res);
253
257
  if (!p) return;
258
+ if (!readAgents(p.path).some((a) => a.slug === req.params.slug))
259
+ return res.status(404).json({ error: "agent not found" });
254
260
  const { body } = req.body || {};
255
261
  if (typeof body !== "string")
256
262
  return res.status(400).json({ error: "body must be string" });
@@ -11,11 +11,18 @@ import { replyAsAgent } from "#core/agent/a2a/reply.js";
11
11
  import { nowIso } from "./shared.js";
12
12
 
13
13
  export function register(app, { project, config }) {
14
+ // The super-agent (default name "apx") is a pseudo-agent: it owns
15
+ // conversations per project but is NOT listed in AGENTS.md. Resolve its slug
16
+ // so `apx conversations list` (which defaults to the super-agent) works
17
+ // instead of 404-ing on the AGENTS.md check.
18
+ const superAgentSlug = () => config?.super_agent?.name || "apx";
19
+ const agentResolvable = (p, slug) =>
20
+ slug === superAgentSlug() || readAgents(p.path).some((a) => a.slug === slug);
21
+
14
22
  app.get("/projects/:pid/agents/:slug/conversations", (req, res) => {
15
23
  const p = project(req, res);
16
24
  if (!p) return;
17
- const agents = readAgents(p.path);
18
- if (!agents.find((a) => a.slug === req.params.slug))
25
+ if (!agentResolvable(p, req.params.slug))
19
26
  return res.status(404).json({ error: "agent not found" });
20
27
  res.json(listConversations(p.storagePath, req.params.slug));
21
28
  });
@@ -27,13 +27,29 @@ const API_PREFIXES = [
27
27
  "/super-agent", "/identity",
28
28
  ];
29
29
 
30
- function isApiPath(p) {
30
+ export function isApiPath(p) {
31
31
  for (const prefix of API_PREFIXES) {
32
32
  if (p === prefix || p.startsWith(prefix + "/")) return true;
33
33
  }
34
34
  return false;
35
35
  }
36
36
 
37
+ // Client-side routes the SPA actually knows how to render. Must mirror the
38
+ // <Routes> registry in src/interfaces/web/src/App.tsx. Anything else still
39
+ // gets the SPA shell (so React Router shows the styled 404 page) but with an
40
+ // HTTP 404 status, so curl/crawlers/health-checks see the right code instead
41
+ // of a misleading 200.
42
+ const SPA_ROUTES = [
43
+ /^\/$/,
44
+ /^\/settings(\/.*)?$/,
45
+ /^\/m\/(voice|desktop|deck|code)(\/.*)?$/,
46
+ /^\/p\/[^/]+(\/.*)?$/,
47
+ ];
48
+
49
+ export function isKnownSpaRoute(p) {
50
+ return SPA_ROUTES.some((re) => re.test(p));
51
+ }
52
+
37
53
  export function register(app, { express, token }) {
38
54
  // /admin/web-token: localhost-only endpoint that returns the daemon token
39
55
  // so the same-origin admin panel can authenticate every subsequent call.
@@ -118,6 +134,9 @@ export function register(app, { express, token }) {
118
134
  if (req.method !== "GET") return next();
119
135
  if (isApiPath(req.path)) return next();
120
136
  if (path.extname(req.path)) return next(); // let static handle 404 of /foo.png
137
+ // Always serve the shell so the SPA can render, but set 404 for routes the
138
+ // app doesn't recognize so the HTTP status matches what the user sees.
139
+ res.status(isKnownSpaRoute(req.path) ? 200 : 404);
121
140
  res.sendFile(path.join(WEB_DIST, "index.html"));
122
141
  });
123
142
  }
@@ -10,6 +10,37 @@ export function setDesktopMessageHandler(fn) {
10
10
  _messageHandler = fn;
11
11
  }
12
12
 
13
+ // --- WS upgrade auth helpers (shared by the daemon upgrade handler + tests) ---
14
+ //
15
+ // The desktop WS channel must authenticate the same way the HTTP /desktop/*
16
+ // routes do: a bearer token (master or paired client) carried on the upgrade
17
+ // request. The legitimate desktop window sends `Authorization: Bearer <token>`
18
+ // (src/interfaces/desktop/main.js); browser clients can pass `?token=`. Without
19
+ // this the daemon (which binds 0.0.0.0 by default) would let any LAN client open
20
+ // the channel and drive the super-agent. See QA BUG-WS-AUTH.
21
+
22
+ /** Path-gate: is this upgrade for the desktop (or legacy overlay) WS channel? */
23
+ export function isDesktopUpgradePath(url) {
24
+ let pathname = url || "";
25
+ try { pathname = new URL(url, "http://localhost").pathname; } catch { /* keep raw */ }
26
+ return pathname === "/desktop/ws" || pathname === "/overlay/ws";
27
+ }
28
+
29
+ /** Extract the bearer token from the upgrade request (header first, ?token= fallback). */
30
+ export function extractWsToken(req) {
31
+ const auth = (req && req.headers && req.headers["authorization"]) || "";
32
+ if (auth.startsWith("Bearer ")) return auth.slice(7);
33
+ try {
34
+ return new URL((req && req.url) || "", "http://localhost").searchParams.get("token") || "";
35
+ } catch { return ""; }
36
+ }
37
+
38
+ /** True iff the upgrade request carries a token the store recognizes. */
39
+ export function isDesktopUpgradeAuthorized(req, tokenStore) {
40
+ if (!tokenStore || typeof tokenStore.has !== "function") return false;
41
+ return tokenStore.has(extractWsToken(req));
42
+ }
43
+
13
44
  export function registerDesktopClient(ws) {
14
45
  _clients.add(ws);
15
46
  ws.on("close", () => _clients.delete(ws));
@@ -22,7 +22,7 @@ import { RoutineScheduler } from "./routines-scheduler.js";
22
22
  import { buildApi } from "./api.js";
23
23
  import { createTokenStore } from "./token-store.js";
24
24
  import { triggerWakeup } from "./wakeup.js";
25
- import { registerDesktopClient } from "./desktop-ws.js";
25
+ import { registerDesktopClient, isDesktopUpgradePath, isDesktopUpgradeAuthorized } from "./desktop-ws.js";
26
26
  import { log as logToUnified } from "#core/logging.js";
27
27
  import { initMemory, stopMemory } from "#core/memory/index.js";
28
28
 
@@ -247,7 +247,17 @@ async function main() {
247
247
  // Attach WebSocket upgrade for the desktop channel on /desktop/ws
248
248
  // (legacy /overlay/ws still accepted for one release).
249
249
  server.on("upgrade", async (req, socket, head) => {
250
- if (req.url !== "/desktop/ws" && req.url !== "/overlay/ws") { socket.destroy(); return; }
250
+ if (!isDesktopUpgradePath(req.url)) { socket.destroy(); return; }
251
+ // Auth: the WS upgrade must carry a valid token (master or paired client),
252
+ // matching the HTTP /desktop/* routes. Without this, any client that can
253
+ // reach the daemon (host binds 0.0.0.0 → the LAN) could open the desktop
254
+ // channel and drive the super-agent (permission_mode "total"). The
255
+ // legitimate desktop window already sends the bearer token. See QA BUG-WS-AUTH.
256
+ if (!isDesktopUpgradeAuthorized(req, tokenStore)) {
257
+ socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
258
+ socket.destroy();
259
+ return;
260
+ }
251
261
  // Lazy-import ws to avoid hard dep on startup
252
262
  let WebSocketServer;
253
263
  try { ({ WebSocketServer } = await import("ws")); } catch {
@@ -5,6 +5,7 @@ import { apcAgentFile } from "#core/apc/paths.js";
5
5
  import { writeAgentFile, writeVaultAgentFile, removeVaultAgent, restoreVaultAgent, addImportedAgent, ensureAgentDir } from "#core/apc/scaffold.js";
6
6
  import { ensureAgentRuntimeDir, agentMemoryPath } from "#core/agent/memory.js";
7
7
  import { http } from "../http.js";
8
+ import { resolveProjectId } from "./project.js";
8
9
 
9
10
  // ── ANSI ──────────────────────────────────────────────────────────────────────
10
11
  const c = { reset:"\x1b[0m", bold:"\x1b[1m", dim:"\x1b[2m", cyan:"\x1b[36m", green:"\x1b[32m", yellow:"\x1b[33m", gray:"\x1b[90m" };
@@ -100,6 +101,25 @@ export function cmdAgentGet(args) {
100
101
  console.log();
101
102
  }
102
103
 
104
+ export async function cmdAgentRemove(args) {
105
+ const slug = args._[0];
106
+ if (!slug) throw new Error("apx agent remove: missing <slug> — usage: apx agent remove <slug>");
107
+ // Resolve locally first so we can give a clear message + suggestions instead
108
+ // of a bare 404 when the slug is wrong.
109
+ const root = findApfRoot();
110
+ if (root) {
111
+ const local = readAgents(root).find((a) => a.slug === slug);
112
+ if (!local) {
113
+ const inVault = readVaultAgents().find((v) => v.slug === slug);
114
+ if (inVault) throw new Error(`agent "${slug}" is in the vault but not in this project — nothing to remove here (use \`apx agent vault rm ${slug}\` to delete the template)`);
115
+ throw new Error(`agent "${slug}" not found in this project — run \`apx agent list\` to see the agents you can remove`);
116
+ }
117
+ }
118
+ const pid = await resolveProjectId(args?.flags?.project);
119
+ await http.delete(`/projects/${pid}/agents/${slug}`);
120
+ console.log(`${tag("removed")} ${bold(slug)} ${gray("(agent file + runtime memory deleted)")}`);
121
+ }
122
+
103
123
  // ── Vault commands ────────────────────────────────────────────────────────────
104
124
 
105
125
  export function cmdAgentVaultList(args = { flags: {} }) {
@@ -1,6 +1,13 @@
1
1
  import readline from "node:readline";
2
2
  import { http } from "../http.js";
3
3
  import { resolveProjectId } from "./project.js";
4
+ import { readConfig } from "#core/config/index.js";
5
+
6
+ // The super-agent (default name "apx") is the default conversational agent, so
7
+ // `apx conversations list` (no slug) targets it — no need to spell it out.
8
+ function superAgentSlug() {
9
+ try { return readConfig()?.super_agent?.name || "apx"; } catch { return "apx"; }
10
+ }
4
11
 
5
12
  export async function cmdChat(args) {
6
13
  const slug = args._[0];
@@ -51,12 +58,12 @@ export async function cmdChat(args) {
51
58
  }
52
59
 
53
60
  export async function cmdConversationsList(args) {
54
- const slug = args._[0];
55
- if (!slug) throw new Error("apx conversations list: missing <agent-slug>");
61
+ // No slug the super-agent (default conversational agent).
62
+ const slug = args._[0] || superAgentSlug();
56
63
  const pid = await resolveProjectId(args?.flags?.project);
57
64
  const rows = await http.get(`/projects/${pid}/agents/${slug}/conversations`);
58
65
  if (rows.length === 0) {
59
- console.log("(no conversations)");
66
+ console.log(`(no conversations for ${slug})`);
60
67
  return;
61
68
  }
62
69
  console.log("ID".padEnd(16) + " ENGINE".padEnd(35) + " TURNS STATUS");
@@ -72,9 +79,11 @@ export async function cmdConversationsList(args) {
72
79
  }
73
80
 
74
81
  export async function cmdConversationsGet(args) {
75
- const slug = args._[0];
76
- const id = args._[1];
77
- if (!slug || !id) throw new Error("apx conversations get: usage: apx conversations get <agent> <id>");
82
+ // Two forms: `get <agent> <id>` or `get <id>` (→ super-agent).
83
+ let slug, id;
84
+ if (args._.length >= 2) { slug = args._[0]; id = args._[1]; }
85
+ else { slug = superAgentSlug(); id = args._[0]; }
86
+ if (!id) throw new Error("apx conversations get: usage: apx conversations get [<agent>] <id>");
78
87
  const pid = await resolveProjectId(args?.flags?.project);
79
88
  const conv = await http.get(`/projects/${pid}/agents/${slug}/conversations/${id}`);
80
89
  process.stdout.write(`# Conversation ${id} (${slug})\n`);
@@ -46,8 +46,27 @@ export async function cmdIdentity(args) {
46
46
  if (args.flags.personality && args.flags.personality !== true) fields.personality = args.flags.personality;
47
47
  if (args.flags.language && args.flags.language !== true) fields.language = args.flags.language;
48
48
 
49
+ // Positional form documented in help: `apx identity set <key> <value>`.
50
+ const KEY_TO_FIELD = {
51
+ name: "agent_name", owner: "owner_name", context: "owner_context",
52
+ personality: "personality", language: "language",
53
+ agent_name: "agent_name", owner_name: "owner_name", owner_context: "owner_context",
54
+ };
55
+ const pKey = args._[1];
56
+ const pVal = args._.slice(2).join(" ");
57
+ if (pKey && pVal) {
58
+ const field = KEY_TO_FIELD[pKey];
59
+ if (!field) {
60
+ console.error(`unknown identity key "${pKey}". Known: name, owner, context, personality, language`);
61
+ process.exitCode = 2;
62
+ return;
63
+ }
64
+ fields[field] = pVal;
65
+ }
66
+
49
67
  if (Object.keys(fields).length === 0) {
50
- console.log("Usage: apx identity set --name <name> --owner <name> --context <text> --personality <text> --language <lang>");
68
+ console.log("Usage: apx identity set <key> <value> (keys: name, owner, context, personality, language)");
69
+ console.log(" or: apx identity set --name <name> --owner <name> --context <text> --personality <text> --language <lang>");
51
70
  return;
52
71
  }
53
72
 
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
2
2
  import readline from "node:readline";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { getLatestVersion } from "#core/update-check.js";
5
+ import { apxBanner } from "../branding.js";
5
6
 
6
7
  const PACKAGE_NAME = "@agentprojectcontext/apx";
7
8
 
@@ -58,6 +59,7 @@ function daemonRunning() {
58
59
  export async function cmdUpdate(args, currentVersion) {
59
60
  const force = args.flags.force || args.flags.yes || args.flags.y;
60
61
 
62
+ apxBanner(currentVersion, "update");
61
63
  console.log("Checking for updates...");
62
64
  const latest = await getLatestVersion();
63
65