@agentprojectcontext/apx 1.0.3
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/LICENSE +21 -0
- package/README.md +142 -0
- package/package.json +52 -0
- package/skills/apx/SKILL.md +77 -0
- package/src/cli/commands/a2a.js +66 -0
- package/src/cli/commands/agent.js +181 -0
- package/src/cli/commands/chat.js +84 -0
- package/src/cli/commands/command.js +42 -0
- package/src/cli/commands/config.js +56 -0
- package/src/cli/commands/daemon.js +148 -0
- package/src/cli/commands/exec.js +56 -0
- package/src/cli/commands/identity.js +146 -0
- package/src/cli/commands/init.js +23 -0
- package/src/cli/commands/mcp.js +147 -0
- package/src/cli/commands/memory.js +69 -0
- package/src/cli/commands/messages.js +61 -0
- package/src/cli/commands/plugins.js +23 -0
- package/src/cli/commands/project.js +124 -0
- package/src/cli/commands/routine.js +99 -0
- package/src/cli/commands/runtime.js +64 -0
- package/src/cli/commands/session.js +387 -0
- package/src/cli/commands/skills.js +153 -0
- package/src/cli/commands/telegram.js +48 -0
- package/src/cli/http.js +102 -0
- package/src/cli/index.js +481 -0
- package/src/cli/postinstall.js +25 -0
- package/src/core/apc-context-skill.md +150 -0
- package/src/core/apx-skill.md +78 -0
- package/src/core/config.js +129 -0
- package/src/core/identity.js +23 -0
- package/src/core/messages-store.js +421 -0
- package/src/core/parser.js +217 -0
- package/src/core/routines-store.js +144 -0
- package/src/core/scaffold.js +417 -0
- package/src/core/session-store.js +36 -0
- package/src/daemon/apc-runtime-context.js +123 -0
- package/src/daemon/api.js +946 -0
- package/src/daemon/compact.js +140 -0
- package/src/daemon/conversations.js +108 -0
- package/src/daemon/db.js +81 -0
- package/src/daemon/engines/anthropic.js +58 -0
- package/src/daemon/engines/gemini.js +55 -0
- package/src/daemon/engines/index.js +65 -0
- package/src/daemon/engines/mock.js +18 -0
- package/src/daemon/engines/ollama.js +66 -0
- package/src/daemon/engines/openai.js +58 -0
- package/src/daemon/env-detect.js +69 -0
- package/src/daemon/index.js +156 -0
- package/src/daemon/mcp-runner.js +218 -0
- package/src/daemon/mcp-sources.js +114 -0
- package/src/daemon/plugins/index.js +91 -0
- package/src/daemon/plugins/telegram.js +549 -0
- package/src/daemon/project-config.js +98 -0
- package/src/daemon/routines.js +211 -0
- package/src/daemon/runtimes/_spawn.js +44 -0
- package/src/daemon/runtimes/aider.js +32 -0
- package/src/daemon/runtimes/claude-code.js +60 -0
- package/src/daemon/runtimes/codex.js +30 -0
- package/src/daemon/runtimes/index.js +39 -0
- package/src/daemon/runtimes/opencode.js +28 -0
- package/src/daemon/smoke.js +54 -0
- package/src/daemon/super-agent-tools.js +539 -0
- package/src/daemon/super-agent.js +188 -0
- package/src/daemon/thinking.js +45 -0
- package/src/daemon/tool-call-parser.js +116 -0
- package/src/daemon/wakeup.js +92 -0
- package/src/mcp/index.js +220 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 APF Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/hero.png" alt="APX — Local Runtime for AI Agents" width="600">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
> The reference implementation of the [APC protocol](https://github.com/agentprojectcontext/agentprojectcontext).
|
|
6
|
+
> APX is to APC what a language SDK is to a protocol spec.
|
|
7
|
+
|
|
8
|
+
## What APX is
|
|
9
|
+
|
|
10
|
+
APX is a daemon + CLI that brings the APC convention to life:
|
|
11
|
+
|
|
12
|
+
- **Daemon** — a local HTTP server that manages projects, agents, sessions, and message logs
|
|
13
|
+
- **CLI** (`apx`) — commands for running agents, reading memory, tailing messages, managing sessions
|
|
14
|
+
- **Runtimes** — bridges to Claude Code, Codex, OpenCode, Aider
|
|
15
|
+
- **Engines** — direct LLM calls via Anthropic, OpenAI, Gemini, Ollama, or a mock
|
|
16
|
+
- **Plugins** — Telegram bot integration out of the box
|
|
17
|
+
- **MCP support** — each agent can expose or consume MCP servers
|
|
18
|
+
|
|
19
|
+
APX is opinionated about storage: the filesystem is the source of truth. No database required to read agent state. Project definitions live in the repo; runtime state (memory, sessions) lives in `~/.apx/` and is never committed.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g apx
|
|
25
|
+
|
|
26
|
+
# In any directory with an AGENTS.md
|
|
27
|
+
apx init
|
|
28
|
+
|
|
29
|
+
# Spawn an agent with a full Claude Code runtime
|
|
30
|
+
apx run sofia --runtime claude-code "Review the open PRs and summarize them"
|
|
31
|
+
|
|
32
|
+
# Or a quick one-shot LLM exec
|
|
33
|
+
apx exec sofia "What is my role in this project?"
|
|
34
|
+
|
|
35
|
+
# Watch what's happening
|
|
36
|
+
apx messages tail
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install -g apx
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Node.js 20+. The daemon starts automatically on first `apx` call.
|
|
46
|
+
|
|
47
|
+
## Project layout
|
|
48
|
+
|
|
49
|
+
Project context — committed to the repository:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
project-root/
|
|
53
|
+
├── AGENTS.md ← agent definitions
|
|
54
|
+
└── .apc/
|
|
55
|
+
├── project.json ← project metadata + stable "id"
|
|
56
|
+
├── agents/
|
|
57
|
+
│ └── <slug>.md ← agent definition (role, model, skills…)
|
|
58
|
+
├── mcps.json ← MCP servers available to this project
|
|
59
|
+
├── skills/ ← reusable skill prompts
|
|
60
|
+
└── commands/ ← custom slash commands
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Runtime state — local machine only, never committed:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
~/.apx/projects/<project-id>/
|
|
67
|
+
├── project.db ← regenerable SQLite cache
|
|
68
|
+
└── agents/
|
|
69
|
+
├── <slug>/
|
|
70
|
+
│ ├── memory.md ← durable memory, updated by the agent
|
|
71
|
+
│ ├── sessions/ ← one .md per runtime invocation
|
|
72
|
+
│ └── conversations/ ← LLM conversation threads
|
|
73
|
+
└── default/ ← fallback when no agent role is active
|
|
74
|
+
├── memory.md
|
|
75
|
+
└── sessions/
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Core commands
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
apx init [path] # initialize a project
|
|
82
|
+
apx agent list # list agents
|
|
83
|
+
apx agent add <slug> --role R --model M # add an agent
|
|
84
|
+
apx memory <slug> # read agent memory
|
|
85
|
+
apx memory <slug> --append "<note>" # append to memory
|
|
86
|
+
|
|
87
|
+
apx run <slug> --runtime claude-code "<prompt>" # full runtime session
|
|
88
|
+
apx exec <slug> "<prompt>" # quick LLM call
|
|
89
|
+
|
|
90
|
+
apx session list <slug> # list past sessions
|
|
91
|
+
apx messages tail # last 50 messages, all channels
|
|
92
|
+
apx messages tail --channel runtime # only agent invocations
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Message channels
|
|
96
|
+
|
|
97
|
+
All activity is logged to `.apc/messages/YYYY-MM-DD.jsonl`:
|
|
98
|
+
|
|
99
|
+
| Channel | What it captures |
|
|
100
|
+
|---------|-----------------|
|
|
101
|
+
| `runtime` | `apx run` invocations (prompt in, response out) |
|
|
102
|
+
| `a2a` | Agent-to-agent calls made from within a session |
|
|
103
|
+
| `telegram` | Telegram bot messages (stored globally in `~/.apx/messages/telegram/`) |
|
|
104
|
+
| `exec` | Quick `apx exec` calls |
|
|
105
|
+
|
|
106
|
+
## Runtimes
|
|
107
|
+
|
|
108
|
+
| Runtime | Description |
|
|
109
|
+
|---------|-------------|
|
|
110
|
+
| `claude-code` | Spawns Claude Code CLI with the agent's system prompt injected |
|
|
111
|
+
| `codex` | OpenAI Codex CLI |
|
|
112
|
+
| `opencode` | OpenCode CLI |
|
|
113
|
+
| `aider` | Aider CLI |
|
|
114
|
+
|
|
115
|
+
## Engines (for `apx exec`)
|
|
116
|
+
|
|
117
|
+
Configured in `~/.apx/config.json`:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"engines": {
|
|
122
|
+
"anthropic": { "api_key": "sk-ant-..." },
|
|
123
|
+
"openai": { "api_key": "sk-..." },
|
|
124
|
+
"ollama": { "base_url": "http://localhost:11434" },
|
|
125
|
+
"gemini": { "api_key": "..." }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Architecture
|
|
131
|
+
|
|
132
|
+
<p align="center">
|
|
133
|
+
<img src="assets/diagram.png" alt="APX architecture diagram" width="720">
|
|
134
|
+
</p>
|
|
135
|
+
|
|
136
|
+
## APC protocol
|
|
137
|
+
|
|
138
|
+
APX implements the [APC specification](https://github.com/agentprojectcontext/agentprojectcontext). The spec defines the on-disk layout; APX provides the tooling to use it.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentprojectcontext/apx",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"bin": {
|
|
11
|
+
"apx": "./src/cli/index.js",
|
|
12
|
+
"apx-daemon": "./src/daemon/index.js",
|
|
13
|
+
"apx-mcp": "./src/mcp/index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src/",
|
|
17
|
+
"skills/",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"start": "node src/daemon/index.js",
|
|
25
|
+
"smoke": "node src/daemon/smoke.js",
|
|
26
|
+
"test": "node --test --test-reporter=spec tests/*.test.js",
|
|
27
|
+
"upgrade": "npm install && npm install -g .",
|
|
28
|
+
"postinstall": "node src/cli/postinstall.js"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
32
|
+
"express": "^4.21.0",
|
|
33
|
+
"node-fetch": "^3.3.2"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"better-sqlite3": "^11.3.0"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"apc",
|
|
40
|
+
"apx",
|
|
41
|
+
"agents",
|
|
42
|
+
"mcp",
|
|
43
|
+
"daemon",
|
|
44
|
+
"claude",
|
|
45
|
+
"ai"
|
|
46
|
+
],
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/agentprojectcontext/apx.git"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/agentprojectcontext/apx"
|
|
52
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apx
|
|
3
|
+
description: "APX CLI skill. Activate ONLY when the user asks about running agents, coordinating between agents, or explicitly uses apx commands. Provides: apx run, apx exec, apx memory, apx mcp, apx session, apx messages tail. Do NOT activate just because .apc/ exists — project context is handled by the apc-context skill. Activate on: 'apx run', 'apx exec', 'run an agent', 'coordinate agents', 'multi-agent', 'apx memory', 'apx mcp', 'daemon'."
|
|
4
|
+
homepage: https://github.com/agentprojectcontext/apx
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# APX — Agent Project Framework
|
|
8
|
+
|
|
9
|
+
This project uses **APX**. The daemon runs on `127.0.0.1:7430` and auto-starts on first `apx` call.
|
|
10
|
+
Your current session, project, and agent are already injected above this block — refer to them.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Discover the project
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
apx agent list # agents in AGENTS.md + their roles/models
|
|
18
|
+
apx mcp list # MCP servers available to this project
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Coordinate with other agents
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Full external session (best for complex, multi-step tasks)
|
|
25
|
+
apx run <slug> --runtime claude-code "<prompt>"
|
|
26
|
+
apx run <slug> --runtime codex "<prompt>"
|
|
27
|
+
|
|
28
|
+
# Quick one-shot LLM call (requires engine API key in ~/.apx/config.json)
|
|
29
|
+
apx exec <slug> "<prompt>"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The output of `apx run` / `apx exec` is the agent's full stdout.
|
|
33
|
+
If the agent printed `APC_RESULT: <value>`, that value is also captured as structured output.
|
|
34
|
+
|
|
35
|
+
## Memory — durable, persists between sessions
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
apx memory <slug> # read agent's memory.md
|
|
39
|
+
apx memory <slug> --append "<fact>" # append a durable note (non-destructive)
|
|
40
|
+
apx memory <slug> --replace < file.md # replace entire memory from stdin
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Write to memory when you discover something the agent should know on every future run.
|
|
44
|
+
|
|
45
|
+
## Observe activity
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
apx messages tail # last 50 messages, all channels
|
|
49
|
+
apx messages tail --channel runtime # only agent invocations (in/out)
|
|
50
|
+
apx messages tail --channel telegram # Telegram conversation history
|
|
51
|
+
apx messages tail --agent <slug> -n 20
|
|
52
|
+
apx session list <slug> # sessions for a specific agent
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## MCP tools
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
apx mcp list # registered MCP servers
|
|
59
|
+
apx mcp tools <server> # list tools a server exposes
|
|
60
|
+
apx mcp run <server> <tool> '<json>' # call a tool directly
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Anti-collision guard
|
|
64
|
+
|
|
65
|
+
Before starting a long task, prevent duplicate runs:
|
|
66
|
+
```bash
|
|
67
|
+
apx session check # exits 1 if a session is already active for this agent
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## APC_RESULT — how to signal your return value
|
|
71
|
+
|
|
72
|
+
Print this on the last meaningful line of your output:
|
|
73
|
+
```
|
|
74
|
+
APC_RESULT: <one-line summary or value>
|
|
75
|
+
```
|
|
76
|
+
The invoker (`apx run`, super-agent, Telegram bot) captures it as structured output.
|
|
77
|
+
Keep it factual and short — it becomes the session result stored in `.apc/agents/<slug>/sessions/`.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { http } from "../http.js";
|
|
2
|
+
import { resolveProjectId } from "./project.js";
|
|
3
|
+
|
|
4
|
+
export async function cmdSend(args) {
|
|
5
|
+
const from = args._[0];
|
|
6
|
+
const to = args._[1];
|
|
7
|
+
if (!from || !to) {
|
|
8
|
+
throw new Error('apx send: usage: apx send <from> <to> "<body>" [--deliver]');
|
|
9
|
+
}
|
|
10
|
+
let body = args._.slice(2).join(" ").trim();
|
|
11
|
+
if (!body || body === "-") {
|
|
12
|
+
const fs = await import("node:fs");
|
|
13
|
+
if (!process.stdin.isTTY) {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
const buf = Buffer.alloc(65536);
|
|
16
|
+
try {
|
|
17
|
+
while (true) {
|
|
18
|
+
const n = fs.readSync(0, buf, 0, buf.length);
|
|
19
|
+
if (!n) break;
|
|
20
|
+
chunks.push(buf.slice(0, n).toString("utf8"));
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
body = chunks.join("").trim();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!body) throw new Error("apx send: body is empty");
|
|
27
|
+
|
|
28
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
29
|
+
const result = await http.post(`/projects/${pid}/send`, {
|
|
30
|
+
from,
|
|
31
|
+
to,
|
|
32
|
+
body,
|
|
33
|
+
deliver: !!args.flags.deliver,
|
|
34
|
+
});
|
|
35
|
+
console.log(`✉ ${from} → ${to} @ ${result.ts}`);
|
|
36
|
+
console.log(` ${body}`);
|
|
37
|
+
if (result.reply) {
|
|
38
|
+
if (result.reply.error) {
|
|
39
|
+
console.log(`\n⚠ delivery failed: ${result.reply.error}`);
|
|
40
|
+
} else {
|
|
41
|
+
console.log(`\n← ${to} replies:`);
|
|
42
|
+
console.log(result.reply.text);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function cmdConnections(args) {
|
|
48
|
+
const slug = args._[0];
|
|
49
|
+
if (!slug) throw new Error("apx connections: missing <agent-slug>");
|
|
50
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
51
|
+
const peers = await http.get(`/projects/${pid}/agents/${slug}/connections`);
|
|
52
|
+
if (peers.length === 0) {
|
|
53
|
+
console.log(`(no connections logged for ${slug} yet)`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.log("PEER".padEnd(16) + " CH".padEnd(11) + " DIR N LAST");
|
|
57
|
+
for (const p of peers) {
|
|
58
|
+
console.log(
|
|
59
|
+
(p.peer || "?").padEnd(16) + " " +
|
|
60
|
+
(p.channel || "").padEnd(10) + " " +
|
|
61
|
+
(p.direction || "").padEnd(4) + " " +
|
|
62
|
+
String(p.n).padEnd(4) + " " +
|
|
63
|
+
(p.last_ts || "")
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { findApfRoot, readAgents, readVaultAgents, VAULT_DIR, SLUG_RE } from "../../core/parser.js";
|
|
4
|
+
import { writeAgentFile, writeVaultAgentFile, addImportedAgent, ensureAgentDir, regenerateAgentsMd } from "../../core/scaffold.js";
|
|
5
|
+
import { http } from "../http.js";
|
|
6
|
+
|
|
7
|
+
// ── ANSI ──────────────────────────────────────────────────────────────────────
|
|
8
|
+
const c = { reset:"\x1b[0m", bold:"\x1b[1m", dim:"\x1b[2m", cyan:"\x1b[36m", green:"\x1b[32m", yellow:"\x1b[33m", gray:"\x1b[90m" };
|
|
9
|
+
const dim = (s) => `${c.dim}${s}${c.reset}`;
|
|
10
|
+
const bold = (s) => `${c.bold}${s}${c.reset}`;
|
|
11
|
+
const cyan = (s) => `${c.cyan}${s}${c.reset}`;
|
|
12
|
+
const gray = (s) => `${c.gray}${s}${c.reset}`;
|
|
13
|
+
const tag = (s) => `${c.yellow}${s}${c.reset}`;
|
|
14
|
+
|
|
15
|
+
function requireRoot() {
|
|
16
|
+
const root = findApfRoot();
|
|
17
|
+
if (!root) throw new Error("not inside an APC project (run `apx init` first)");
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function nudgeDaemon(root) {
|
|
22
|
+
try {
|
|
23
|
+
if (!(await http.ping())) return;
|
|
24
|
+
const projects = await http.get("/projects", { autoStart: false });
|
|
25
|
+
const me = projects.find((p) => p.path === root);
|
|
26
|
+
if (me) await http.post(`/projects/${me.id}/rebuild`, undefined, { autoStart: false });
|
|
27
|
+
} catch { /* daemon hiccup */ }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function cmdAgentAdd(args) {
|
|
31
|
+
const slug = args._[0];
|
|
32
|
+
if (!slug) throw new Error("apx agent add: missing <slug>");
|
|
33
|
+
if (!SLUG_RE.test(slug)) throw new Error(`invalid slug "${slug}"`);
|
|
34
|
+
|
|
35
|
+
const root = requireRoot();
|
|
36
|
+
const existing = readAgents(root);
|
|
37
|
+
if (existing.some((a) => a.slug === slug)) {
|
|
38
|
+
throw new Error(`agent "${slug}" already exists`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const fields = {};
|
|
42
|
+
const f = args.flags;
|
|
43
|
+
if (f.role && f.role !== true) fields.Role = f.role;
|
|
44
|
+
if (f.model && f.model !== true) fields.Model = f.model;
|
|
45
|
+
if (f.language && f.language !== true) fields.Language = f.language;
|
|
46
|
+
if (f.description && f.description !== true) fields.Description = f.description;
|
|
47
|
+
if (f.skills && f.skills !== true) fields.Skills = String(f.skills).split(",").map((s) => s.trim()).filter(Boolean);
|
|
48
|
+
if (f.tools && f.tools !== true) fields.Tools = String(f.tools).split(",").map((s) => s.trim()).filter(Boolean);
|
|
49
|
+
|
|
50
|
+
writeAgentFile(root, slug, fields);
|
|
51
|
+
ensureAgentDir(root, slug);
|
|
52
|
+
regenerateAgentsMd(root);
|
|
53
|
+
await nudgeDaemon(root);
|
|
54
|
+
|
|
55
|
+
console.log(`Added agent ${slug}`);
|
|
56
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
57
|
+
console.log(` ${k}: ${Array.isArray(v) ? v.join(", ") : v}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function cmdAgentList() {
|
|
62
|
+
const root = requireRoot();
|
|
63
|
+
const agents = readAgents(root);
|
|
64
|
+
if (agents.length === 0) {
|
|
65
|
+
console.log(dim("(no agents — try `apx agent add <slug>` or `apx agent import <slug>`)"));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
console.log();
|
|
69
|
+
for (const a of agents) {
|
|
70
|
+
const src = a.source === "vault" ? tag(" ↑ vault") : a.source === "legacy" ? gray(" ↑ legacy") : "";
|
|
71
|
+
const role = a.fields.Role ? dim(a.fields.Role) : gray("—");
|
|
72
|
+
const model = a.fields.Model ? dim(a.fields.Model) : gray("—");
|
|
73
|
+
console.log(` ${bold(a.slug)}${src} ${role} ${cyan(model)}`);
|
|
74
|
+
}
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function cmdAgentGet(args) {
|
|
79
|
+
const slug = args._[0];
|
|
80
|
+
if (!slug) throw new Error("apx agent get: missing <slug>");
|
|
81
|
+
const root = requireRoot();
|
|
82
|
+
const a = readAgents(root).find((x) => x.slug === slug);
|
|
83
|
+
if (!a) {
|
|
84
|
+
// Check vault and suggest import
|
|
85
|
+
const vault = readVaultAgents();
|
|
86
|
+
const inVault = vault.find((v) => v.slug === slug);
|
|
87
|
+
if (inVault) {
|
|
88
|
+
throw new Error(`agent "${slug}" not imported in this project. Run: apx agent import ${slug}`);
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`agent "${slug}" not found`);
|
|
91
|
+
}
|
|
92
|
+
const src = a.source === "vault" ? tag(" ↑ vault") : a.source === "legacy" ? gray(" ↑ legacy") : "";
|
|
93
|
+
console.log(`\n ${bold(a.slug)}${src}`);
|
|
94
|
+
for (const [k, v] of Object.entries(a.fields)) {
|
|
95
|
+
console.log(` ${gray(k.padEnd(12))} ${Array.isArray(v) ? v.join(", ") : v}`);
|
|
96
|
+
}
|
|
97
|
+
if (a.body) console.log(`\n${dim(a.body)}`);
|
|
98
|
+
console.log();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Vault commands ────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export function cmdAgentVaultList() {
|
|
104
|
+
const vault = readVaultAgents();
|
|
105
|
+
if (vault.length === 0) {
|
|
106
|
+
console.log(dim(`(vault empty — add templates with \`apx agent vault add <slug>\`)`));
|
|
107
|
+
console.log(gray(` vault: ${VAULT_DIR}`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
console.log(`\n ${gray("vault:")} ${gray(VAULT_DIR)}\n`);
|
|
111
|
+
for (const a of vault) {
|
|
112
|
+
const role = a.fields.Role ? dim(a.fields.Role) : gray("—");
|
|
113
|
+
const model = a.fields.Model ? dim(a.fields.Model) : gray("—");
|
|
114
|
+
console.log(` ${bold(a.slug)} ${role} ${cyan(model)}`);
|
|
115
|
+
}
|
|
116
|
+
console.log();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function cmdAgentVaultAdd(args) {
|
|
120
|
+
const slug = args._[0];
|
|
121
|
+
if (!slug || !SLUG_RE.test(slug)) throw new Error("apx agent vault add: missing or invalid <slug>");
|
|
122
|
+
|
|
123
|
+
// If we're inside a project, offer to copy the local agent to vault
|
|
124
|
+
const root = findApfRoot();
|
|
125
|
+
if (root) {
|
|
126
|
+
const local = readAgents(root).find((a) => a.slug === slug && a.source === "local");
|
|
127
|
+
if (local) {
|
|
128
|
+
writeVaultAgentFile(slug, local.fields, local.body);
|
|
129
|
+
console.log(`\n ${bold(slug)} added to vault from local definition\n`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Otherwise create a blank vault entry from flags
|
|
135
|
+
const fields = {};
|
|
136
|
+
const f = args.flags;
|
|
137
|
+
if (f.role && f.role !== true) fields.Role = f.role;
|
|
138
|
+
if (f.model && f.model !== true) fields.Model = f.model;
|
|
139
|
+
if (f.language && f.language !== true) fields.Language = f.language;
|
|
140
|
+
if (f.description && f.description !== true) fields.Description = f.description;
|
|
141
|
+
if (f.skills && f.skills !== true) fields.Skills = String(f.skills).split(",").map((s) => s.trim()).filter(Boolean);
|
|
142
|
+
|
|
143
|
+
writeVaultAgentFile(slug, fields);
|
|
144
|
+
console.log(`\n ${bold(slug)} added to vault ${gray(VAULT_DIR + "/" + slug + ".md")}\n`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function cmdAgentImport(args) {
|
|
148
|
+
const slug = args._[0];
|
|
149
|
+
if (!slug) throw new Error("apx agent import: missing <slug>");
|
|
150
|
+
const root = requireRoot();
|
|
151
|
+
|
|
152
|
+
const vaultPath = path.join(VAULT_DIR, `${slug}.md`);
|
|
153
|
+
if (!fs.existsSync(vaultPath)) {
|
|
154
|
+
const vault = readVaultAgents();
|
|
155
|
+
const available = vault.map((a) => a.slug).join(", ") || "(none)";
|
|
156
|
+
throw new Error(`"${slug}" not found in vault. Available: ${available}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const alreadyLocal = fs.existsSync(path.join(root, ".apc", "agents", `${slug}.md`));
|
|
160
|
+
if (alreadyLocal && !args.flags.force) {
|
|
161
|
+
console.log(dim(` "${slug}" already has a local definition. Use --force to overwrite.`));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (args.flags.copy) {
|
|
166
|
+
// Copy .md into project so user can edit locally
|
|
167
|
+
fs.copyFileSync(vaultPath, path.join(root, ".apc", "agents", `${slug}.md`));
|
|
168
|
+
console.log(`\n ${bold(slug)} copied from vault to project (now local)\n`);
|
|
169
|
+
} else {
|
|
170
|
+
// Just register as imported — reads from vault at runtime
|
|
171
|
+
addImportedAgent(root, slug);
|
|
172
|
+
console.log(`\n ${bold(slug)} imported from vault ${tag("↑ vault")}\n`);
|
|
173
|
+
console.log(gray(` definition: ${vaultPath}`));
|
|
174
|
+
console.log(gray(` memory: ${path.join(root, ".apc", "agents", slug, "memory.md")} (project-local)`));
|
|
175
|
+
console.log();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
ensureAgentDir(root, slug);
|
|
179
|
+
regenerateAgentsMd(root);
|
|
180
|
+
await nudgeDaemon(root);
|
|
181
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import { http } from "../http.js";
|
|
3
|
+
import { resolveProjectId } from "./project.js";
|
|
4
|
+
|
|
5
|
+
export async function cmdChat(args) {
|
|
6
|
+
const slug = args._[0];
|
|
7
|
+
if (!slug) throw new Error("apx chat: usage: apx chat <agent> [--conversation <id>] [--model <id>]");
|
|
8
|
+
|
|
9
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
10
|
+
let convId = args.flags.conversation === true ? null : args.flags.conversation || null;
|
|
11
|
+
const overrideModel = args.flags.model === true ? null : args.flags.model || null;
|
|
12
|
+
|
|
13
|
+
const rl = readline.createInterface({
|
|
14
|
+
input: process.stdin,
|
|
15
|
+
output: process.stdout,
|
|
16
|
+
prompt: `${slug}> `,
|
|
17
|
+
terminal: process.stdin.isTTY,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
console.log(`apx chat with ${slug}${convId ? ` (cont. ${convId})` : ""} — type Ctrl-D or 'exit' to quit`);
|
|
21
|
+
rl.prompt();
|
|
22
|
+
|
|
23
|
+
rl.on("line", async (line) => {
|
|
24
|
+
const text = line.trim();
|
|
25
|
+
if (text === "exit" || text === "quit") {
|
|
26
|
+
rl.close();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!text) {
|
|
30
|
+
rl.prompt();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const body = { prompt: text };
|
|
35
|
+
if (convId) body.conversation_id = convId;
|
|
36
|
+
if (overrideModel) body.model = overrideModel;
|
|
37
|
+
|
|
38
|
+
const result = await http.post(`/projects/${pid}/agents/${slug}/chat`, body);
|
|
39
|
+
convId = result.conversation_id;
|
|
40
|
+
process.stdout.write("\n" + result.text + "\n\n");
|
|
41
|
+
} catch (e) {
|
|
42
|
+
process.stderr.write(`apx: ${e.message}\n`);
|
|
43
|
+
}
|
|
44
|
+
rl.prompt();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
rl.on("close", () => {
|
|
48
|
+
process.stdout.write("\n");
|
|
49
|
+
process.exit(0);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function cmdConversationsList(args) {
|
|
54
|
+
const slug = args._[0];
|
|
55
|
+
if (!slug) throw new Error("apx conversations list: missing <agent-slug>");
|
|
56
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
57
|
+
const rows = await http.get(`/projects/${pid}/agents/${slug}/conversations`);
|
|
58
|
+
if (rows.length === 0) {
|
|
59
|
+
console.log("(no conversations)");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.log("ID".padEnd(16) + " ENGINE".padEnd(35) + " TURNS STATUS");
|
|
63
|
+
for (const r of rows) {
|
|
64
|
+
const id = r.filename.replace(/\.md$/, "");
|
|
65
|
+
console.log(
|
|
66
|
+
id.padEnd(16) +
|
|
67
|
+
" " + (r.engine || "?").padEnd(34) +
|
|
68
|
+
" " + String(r.turn_count || 0).padEnd(6) +
|
|
69
|
+
" " + (r.status || "open")
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
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>");
|
|
78
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
79
|
+
const conv = await http.get(`/projects/${pid}/agents/${slug}/conversations/${id}`);
|
|
80
|
+
process.stdout.write(`# Conversation ${id} (${slug})\n`);
|
|
81
|
+
for (const t of conv.turns) {
|
|
82
|
+
process.stdout.write(`\n## ${t.role} — ${t.ts}\n${t.content}\n`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// apx command — list and show workflow commands from .apc/commands/
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { findApfRoot } from "../../core/parser.js";
|
|
5
|
+
|
|
6
|
+
function commandsDir(root) {
|
|
7
|
+
return path.join(root, ".apc", "commands");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function listCommandFiles(root) {
|
|
11
|
+
const dir = commandsDir(root);
|
|
12
|
+
if (!fs.existsSync(dir)) return [];
|
|
13
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function cmdCommandList() {
|
|
17
|
+
const root = findApfRoot();
|
|
18
|
+
if (!root) throw new Error("not inside an APC project");
|
|
19
|
+
const files = listCommandFiles(root);
|
|
20
|
+
if (files.length === 0) {
|
|
21
|
+
console.log("(no commands — add .md files to .apc/commands/)");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
for (const f of files) {
|
|
25
|
+
const slug = f.replace(/\.md$/, "");
|
|
26
|
+
const text = fs.readFileSync(path.join(commandsDir(root), f), "utf8");
|
|
27
|
+
const firstLine = text.split("\n").find((l) => l.trim() && !l.startsWith("#"))
|
|
28
|
+
|| text.split("\n").find((l) => l.startsWith("# "))?.replace(/^#\s*/, "")
|
|
29
|
+
|| "";
|
|
30
|
+
console.log(` ${slug.padEnd(24)} ${firstLine.slice(0, 60)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function cmdCommandShow(args) {
|
|
35
|
+
const name = args._[0];
|
|
36
|
+
if (!name) throw new Error("apx command show: missing <name>");
|
|
37
|
+
const root = findApfRoot();
|
|
38
|
+
if (!root) throw new Error("not inside an APC project");
|
|
39
|
+
const file = path.join(commandsDir(root), `${name}.md`);
|
|
40
|
+
if (!fs.existsSync(file)) throw new Error(`command "${name}" not found in .apc/commands/`);
|
|
41
|
+
process.stdout.write(fs.readFileSync(file, "utf8"));
|
|
42
|
+
}
|