@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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/package.json +52 -0
  4. package/skills/apx/SKILL.md +77 -0
  5. package/src/cli/commands/a2a.js +66 -0
  6. package/src/cli/commands/agent.js +181 -0
  7. package/src/cli/commands/chat.js +84 -0
  8. package/src/cli/commands/command.js +42 -0
  9. package/src/cli/commands/config.js +56 -0
  10. package/src/cli/commands/daemon.js +148 -0
  11. package/src/cli/commands/exec.js +56 -0
  12. package/src/cli/commands/identity.js +146 -0
  13. package/src/cli/commands/init.js +23 -0
  14. package/src/cli/commands/mcp.js +147 -0
  15. package/src/cli/commands/memory.js +69 -0
  16. package/src/cli/commands/messages.js +61 -0
  17. package/src/cli/commands/plugins.js +23 -0
  18. package/src/cli/commands/project.js +124 -0
  19. package/src/cli/commands/routine.js +99 -0
  20. package/src/cli/commands/runtime.js +64 -0
  21. package/src/cli/commands/session.js +387 -0
  22. package/src/cli/commands/skills.js +153 -0
  23. package/src/cli/commands/telegram.js +48 -0
  24. package/src/cli/http.js +102 -0
  25. package/src/cli/index.js +481 -0
  26. package/src/cli/postinstall.js +25 -0
  27. package/src/core/apc-context-skill.md +150 -0
  28. package/src/core/apx-skill.md +78 -0
  29. package/src/core/config.js +129 -0
  30. package/src/core/identity.js +23 -0
  31. package/src/core/messages-store.js +421 -0
  32. package/src/core/parser.js +217 -0
  33. package/src/core/routines-store.js +144 -0
  34. package/src/core/scaffold.js +417 -0
  35. package/src/core/session-store.js +36 -0
  36. package/src/daemon/apc-runtime-context.js +123 -0
  37. package/src/daemon/api.js +946 -0
  38. package/src/daemon/compact.js +140 -0
  39. package/src/daemon/conversations.js +108 -0
  40. package/src/daemon/db.js +81 -0
  41. package/src/daemon/engines/anthropic.js +58 -0
  42. package/src/daemon/engines/gemini.js +55 -0
  43. package/src/daemon/engines/index.js +65 -0
  44. package/src/daemon/engines/mock.js +18 -0
  45. package/src/daemon/engines/ollama.js +66 -0
  46. package/src/daemon/engines/openai.js +58 -0
  47. package/src/daemon/env-detect.js +69 -0
  48. package/src/daemon/index.js +156 -0
  49. package/src/daemon/mcp-runner.js +218 -0
  50. package/src/daemon/mcp-sources.js +114 -0
  51. package/src/daemon/plugins/index.js +91 -0
  52. package/src/daemon/plugins/telegram.js +549 -0
  53. package/src/daemon/project-config.js +98 -0
  54. package/src/daemon/routines.js +211 -0
  55. package/src/daemon/runtimes/_spawn.js +44 -0
  56. package/src/daemon/runtimes/aider.js +32 -0
  57. package/src/daemon/runtimes/claude-code.js +60 -0
  58. package/src/daemon/runtimes/codex.js +30 -0
  59. package/src/daemon/runtimes/index.js +39 -0
  60. package/src/daemon/runtimes/opencode.js +28 -0
  61. package/src/daemon/smoke.js +54 -0
  62. package/src/daemon/super-agent-tools.js +539 -0
  63. package/src/daemon/super-agent.js +188 -0
  64. package/src/daemon/thinking.js +45 -0
  65. package/src/daemon/tool-call-parser.js +116 -0
  66. package/src/daemon/wakeup.js +92 -0
  67. 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
+ }