@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.
- package/README.md +81 -3
- package/package.json +1 -1
- package/src/core/mascot.js +80 -80
- 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-CQc_5t8F.js +629 -0
- package/src/interfaces/web/dist/assets/index-CQc_5t8F.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-hwxuTPcK.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/App.tsx +20 -9
- package/src/interfaces/web/src/components/ModelCombobox.tsx +1 -1
- package/src/interfaces/web/src/components/Roby.tsx +96 -0
- package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +11 -11
- package/src/interfaces/web/src/components/TelegramSendDialog.tsx +5 -5
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +2 -2
- package/src/interfaces/web/src/components/chat/ModelPicker.tsx +5 -5
- package/src/interfaces/web/src/components/chat/ToolCall.tsx +23 -19
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +10 -10
- package/src/interfaces/web/src/components/code/CodeContextTab.tsx +7 -7
- package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +3 -2
- package/src/interfaces/web/src/components/common/TabNav.tsx +3 -2
- package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +3 -2
- package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -2
- package/src/interfaces/web/src/components/config/global-config-sections.ts +9 -9
- package/src/interfaces/web/src/components/config/project-config-sections.ts +61 -54
- package/src/interfaces/web/src/components/deck/DaemonCard.tsx +6 -5
- package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +5 -4
- package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +3 -3
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +22 -9
- package/src/interfaces/web/src/components/settings/AdvancedPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +14 -14
- package/src/interfaces/web/src/components/settings/DevicesPanel.tsx +3 -3
- package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +7 -7
- package/src/interfaces/web/src/components/settings/IdentityPanel.tsx +2 -2
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +37 -37
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +44 -35
- package/src/interfaces/web/src/components/settings/SuperAgentPanel.tsx +5 -5
- package/src/interfaces/web/src/components/settings/TelegramChannelsPanel.tsx +3 -3
- package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/TelegramGlobalPanel.tsx +3 -3
- package/src/interfaces/web/src/components/settings/TelegramRolesPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +6 -6
- package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +36 -36
- package/src/interfaces/web/src/components/voice/VoiceProviderList.tsx +15 -14
- package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +22 -22
- package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +18 -17
- package/src/interfaces/web/src/components/voice/VoiceTestCard.tsx +19 -18
- package/src/interfaces/web/src/hooks/useChat.ts +6 -5
- package/src/interfaces/web/src/i18n/en.ts +519 -2
- package/src/interfaces/web/src/i18n/es.ts +519 -2
- package/src/interfaces/web/src/i18n/index.ts +1 -1
- package/src/interfaces/web/src/lib/api/voice.ts +5 -5
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +14 -1
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +8 -8
- package/src/interfaces/web/src/screens/base/ComingSoon.tsx +3 -2
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +12 -12
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +15 -15
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +37 -37
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +8 -8
- package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +16 -10
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +25 -24
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +2 -2
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +3 -3
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +6 -9
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +66 -52
- package/src/interfaces/web/src/screens/project/TelegramTab.tsx +1 -1
- package/src/interfaces/web/dist/assets/index-Cm0KyPoZ.css +0 -1
- package/src/interfaces/web/dist/assets/index-DJKA763h.js +0 -628
- 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/
|
|
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
|
|
@@ -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
|
-
#
|
|
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
package/src/core/mascot.js
CHANGED
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
// APX mascot — a
|
|
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
|
-
//
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
118
|
-
mascot.sad
|
|
119
|
-
mascot.happy
|
|
120
|
-
mascot.wave
|
|
121
|
-
mascot.excited
|
|
122
|
-
mascot.sleeping
|
|
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
|
-
|
|
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
|
|