@agentprojectcontext/apx 1.35.0 → 1.37.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 (35) hide show
  1. package/README.md +70 -3
  2. package/package.json +1 -1
  3. package/src/core/stores/conversations.js +27 -2
  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/exec.js +2 -0
  7. package/src/host/daemon/api/web.js +20 -1
  8. package/src/host/daemon/desktop-ws.js +31 -0
  9. package/src/host/daemon/index.js +12 -2
  10. package/src/interfaces/cli/commands/agent.js +20 -0
  11. package/src/interfaces/cli/commands/chat.js +15 -6
  12. package/src/interfaces/cli/commands/identity.js +20 -1
  13. package/src/interfaces/cli/commands/update.js +2 -0
  14. package/src/interfaces/cli/index.js +14 -0
  15. package/src/interfaces/web/dist/assets/index-B6sYFQFa.css +1 -0
  16. package/src/interfaces/web/dist/assets/index-DsADpObh.js +633 -0
  17. package/src/interfaces/web/dist/assets/index-DsADpObh.js.map +1 -0
  18. package/src/interfaces/web/dist/index.html +2 -2
  19. package/src/interfaces/web/src/App.tsx +23 -4
  20. package/src/interfaces/web/src/components/chat/ChatList.tsx +412 -0
  21. package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
  22. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +1 -1
  23. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +1 -1
  24. package/src/interfaces/web/src/hooks/useChat.ts +35 -2
  25. package/src/interfaces/web/src/i18n/en.ts +13 -2
  26. package/src/interfaces/web/src/i18n/es.ts +13 -2
  27. package/src/interfaces/web/src/lib/api/agents.ts +1 -1
  28. package/src/interfaces/web/src/screens/ProjectScreen.tsx +3 -5
  29. package/src/interfaces/web/src/screens/SettingsScreen.tsx +6 -4
  30. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +1 -1
  31. package/src/interfaces/web/src/screens/project/ChatTab.tsx +120 -87
  32. package/src/interfaces/web/dist/assets/index-C0fm31dY.js +0 -618
  33. package/src/interfaces/web/dist/assets/index-C0fm31dY.js.map +0 -1
  34. package/src/interfaces/web/dist/assets/index-UcAqlBO6.css +0 -1
  35. package/src/interfaces/web/src/screens/project/ThreadsTab.tsx +0 -100
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
@@ -21,9 +44,13 @@ APX is opinionated about storage: the filesystem is the source of truth. Project
21
44
  ## Quick start
22
45
 
23
46
  ```bash
47
+ # 1 · Install
24
48
  npm install -g apx
25
49
 
26
- # In any directory with an AGENTS.md
50
+ # 2 · Set up interactive wizard (provider → model → channels → daemon)
51
+ apx setup
52
+
53
+ # In any directory with an AGENTS.md, register the project
27
54
  apx init
28
55
 
29
56
  # Spawn an agent with a full external runtime
@@ -36,6 +63,27 @@ apx exec sofia "What is my role in this project?"
36
63
  apx messages tail
37
64
  ```
38
65
 
66
+ ## Examples
67
+
68
+ Real commands — copy one, point it at an agent like `sofia`, and APX routes it to the right
69
+ runtime. The session and memory land in `.apc/`.
70
+
71
+ | What | Command |
72
+ |------|---------|
73
+ | Register a project | `apx init` |
74
+ | Spawn an agent (full runtime) | `apx run sofia --runtime claude-code "Review the open PRs and summarize them"` |
75
+ | Ask a quick question (one-shot) | `apx exec sofia "What is my role in this project?"` |
76
+ | Read an agent's memory | `apx memory sofia` |
77
+ | Switch runtime, same context | `apx run sofia --runtime codex "Add tests for the parser"` |
78
+ | Watch what's happening | `apx messages tail` |
79
+
80
+ ## Use cases
81
+
82
+ - **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/`.
83
+ - **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.
84
+ - **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.
85
+ - **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.
86
+
39
87
  ## Installation
40
88
 
41
89
  ```bash
@@ -44,6 +92,25 @@ npm install -g apx
44
92
 
45
93
  Requires Node.js 20+. The daemon starts automatically on first `apx` call.
46
94
 
95
+ ## Web admin
96
+
97
+ APX ships a local **web admin** — the same runtime, in your browser. The daemon serves a
98
+ single-page app so you can browse and manage everything the CLI does without leaving the UI:
99
+
100
+ - **Projects & agents** — see registered projects, open agents, edit roles, models, and skills
101
+ - **Sessions & messages** — read past sessions and tail live activity across every channel
102
+ - **MCPs, engines & channels** — review MCP servers, configure engines, and manage Telegram/desktop
103
+
104
+ It runs entirely on your machine. Start the daemon (any `apx` call does this) and open:
105
+
106
+ ```bash
107
+ apx # ensures the daemon is up
108
+ open http://localhost:7430 # macOS — or just visit it in any browser
109
+ ```
110
+
111
+ The web admin is served from `src/interfaces/web/dist` at the daemon port (`7430` by default,
112
+ override with `APX_PORT`). Nothing is sent anywhere — it talks to the local daemon only.
113
+
47
114
  ## Project layout
48
115
 
49
116
  Project context — committed to the repository:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentprojectcontext/apx",
3
- "version": "1.35.0",
3
+ "version": "1.37.0",
4
4
  "description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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) {
@@ -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
  });
@@ -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;
@@ -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
 
@@ -16,6 +16,7 @@ import {
16
16
  cmdAgentAdd,
17
17
  cmdAgentList,
18
18
  cmdAgentGet,
19
+ cmdAgentRemove,
19
20
  cmdAgentImport,
20
21
  cmdAgentVaultList,
21
22
  cmdAgentVaultAdd,
@@ -307,6 +308,7 @@ const HELP_TOPICS = new Map(Object.entries({
307
308
  ["add <slug>", "Create a project-local agent."],
308
309
  ["list | ls", "List project agents."],
309
310
  ["get | show <slug>", "Print one agent definition."],
311
+ ["remove | rm <slug>", "Delete a project agent (file + runtime memory)."],
310
312
  ["import <slug>", "Import an agent template from ~/.apx/agents."],
311
313
  ["vault list | ls", "List vault templates (bundled defaults + your overrides)."],
312
314
  ["vault add <slug>", "Create a new vault template in the user layer (~/.apx/agents/)."],
@@ -341,6 +343,12 @@ const HELP_TOPICS = new Map(Object.entries({
341
343
  usage: ["apx agent get <slug>", "apx agent show <slug>"],
342
344
  examples: ["apx agent get reviewer"],
343
345
  }),
346
+ "agent remove": topic({
347
+ title: "apx agent remove",
348
+ summary: "Delete a project agent (its .apc file + runtime memory).",
349
+ usage: ["apx agent remove <slug>", "apx agent rm <slug>"],
350
+ examples: ["apx agent remove reviewer"],
351
+ }),
344
352
  "agent import": topic({
345
353
  title: "apx agent import",
346
354
  summary: "Import a reusable agent template from the APX vault into the current project.",
@@ -1968,6 +1976,7 @@ function buildHelp(version) {
1968
1976
  hCmd("apx agent add <slug>", 36, "--role R --model M --skills a,b --language es-AR --description D"),
1969
1977
  hCmd("apx agent list", 36, ""),
1970
1978
  hCmd("apx agent get <slug>", 36, ""),
1979
+ hCmd("apx agent remove <slug>", 36, "delete a project agent (file + runtime memory)"),
1971
1980
  hCmd("apx agent import", 36, ""),
1972
1981
  hCmd("apx agent vault list", 36, ""),
1973
1982
  hCmd("apx agent vault add", 36, ""),
@@ -2252,6 +2261,10 @@ if (helpRequest?.topic) {
2252
2261
  }
2253
2262
 
2254
2263
  if (argv[0] === "--version" || argv[0] === "-v") {
2264
+ // Big wordmark to stderr (branding), bare version to stdout so
2265
+ // `apx --version` stays parseable in scripts. apxBanner self-suppresses
2266
+ // under APX_QUIET / APX_NO_BANNER.
2267
+ apxBanner(VERSION, "version");
2255
2268
  console.log(VERSION);
2256
2269
  process.exit(0);
2257
2270
  }
@@ -2302,6 +2315,7 @@ async function dispatch(cmd, rest) {
2302
2315
  if (sub === "add") await cmdAgentAdd(a);
2303
2316
  else if (sub === "list" || sub === "ls") cmdAgentList();
2304
2317
  else if (sub === "get" || sub === "show") cmdAgentGet(a);
2318
+ else if (sub === "remove" || sub === "rm" || sub === "delete") await cmdAgentRemove(a);
2305
2319
  else if (sub === "import") await cmdAgentImport(a);
2306
2320
  else if (sub === "vault") {
2307
2321
  const vsub = a._[0];