@agentprojectcontext/apx 1.36.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.
- package/README.md +70 -3
- package/package.json +1 -1
- package/src/host/daemon/api/agents.js +6 -0
- package/src/host/daemon/api/conversations.js +9 -2
- package/src/host/daemon/api/web.js +20 -1
- package/src/host/daemon/desktop-ws.js +31 -0
- package/src/host/daemon/index.js +12 -2
- package/src/interfaces/cli/commands/agent.js +20 -0
- package/src/interfaces/cli/commands/chat.js +15 -6
- package/src/interfaces/cli/commands/identity.js +20 -1
- package/src/interfaces/cli/commands/update.js +2 -0
- package/src/interfaces/cli/index.js +14 -0
- package/src/interfaces/web/dist/assets/index-B6sYFQFa.css +1 -0
- package/src/interfaces/web/dist/assets/{index-DJKA763h.js → index-DsADpObh.js} +27 -22
- package/src/interfaces/web/dist/assets/{index-DJKA763h.js.map → index-DsADpObh.js.map} +1 -1
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/App.tsx +23 -3
- package/src/interfaces/web/src/i18n/en.ts +2 -1
- package/src/interfaces/web/src/i18n/es.ts +2 -1
- package/src/interfaces/web/dist/assets/index-Cm0KyPoZ.css +0 -1
package/README.md
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="assets/
|
|
2
|
+
<img src="assets/banner.svg" alt="APX — Agent Project eXecutable" width="820">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
<p align="center">
|
|
6
|
+
<b>APX</b> — <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> ·
|
|
20
|
+
<a href="#quick-start">Quick start</a> ·
|
|
21
|
+
<a href="#examples">Examples</a> ·
|
|
22
|
+
<a href="#web-admin">Web admin</a> ·
|
|
23
|
+
<a href="#use-cases">Use cases</a> ·
|
|
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
|
-
#
|
|
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
|
@@ -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
|
-
|
|
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));
|
package/src/host/daemon/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
55
|
-
|
|
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(
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
if (
|
|
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
|
|
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];
|