@agentprojectcontext/apx 1.0.3 → 1.1.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 +4 -4
- package/package.json +5 -2
- package/skills/apx/SKILL.md +7 -4
- package/src/cli/commands/project.js +2 -5
- package/src/cli/commands/session.js +13 -5
- package/src/cli/commands/update.js +75 -0
- package/src/cli/index.js +23 -5
- package/src/core/apc-context-skill.md +70 -118
- package/src/core/apx-skill.md +9 -2
- package/src/core/config.js +15 -0
- package/src/core/messages-store.js +7 -7
- package/src/core/scaffold.js +24 -3
- package/src/core/session-store.js +2 -2
- package/src/core/update-check.js +99 -0
- package/src/daemon/apc-runtime-context.js +8 -8
- package/src/daemon/api.js +14 -13
- package/src/daemon/compact.js +4 -4
- package/src/daemon/conversations.js +13 -13
- package/src/daemon/db.js +45 -2
- package/src/daemon/index.js +3 -0
- package/src/daemon/smoke.js +2 -3
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ APX is a daemon + CLI that brings the APC convention to life:
|
|
|
16
16
|
- **Plugins** — Telegram bot integration out of the box
|
|
17
17
|
- **MCP support** — each agent can expose or consume MCP servers
|
|
18
18
|
|
|
19
|
-
APX is opinionated about storage: the filesystem is the source of truth.
|
|
19
|
+
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
20
|
|
|
21
21
|
## Quick start
|
|
22
22
|
|
|
@@ -65,13 +65,12 @@ Runtime state — local machine only, never committed:
|
|
|
65
65
|
```text
|
|
66
66
|
~/.apx/projects/<project-id>/
|
|
67
67
|
├── project.db ← regenerable SQLite cache
|
|
68
|
+
├── messages/ ← local message history
|
|
68
69
|
└── agents/
|
|
69
70
|
├── <slug>/
|
|
70
|
-
│ ├── memory.md ← durable memory, updated by the agent
|
|
71
71
|
│ ├── sessions/ ← one .md per runtime invocation
|
|
72
72
|
│ └── conversations/ ← LLM conversation threads
|
|
73
73
|
└── default/ ← fallback when no agent role is active
|
|
74
|
-
├── memory.md
|
|
75
74
|
└── sessions/
|
|
76
75
|
```
|
|
77
76
|
|
|
@@ -94,7 +93,8 @@ apx messages tail --channel runtime # only agent invocations
|
|
|
94
93
|
|
|
95
94
|
## Message channels
|
|
96
95
|
|
|
97
|
-
|
|
96
|
+
Activity belongs to APX runtime state, not `.apc/`. Message storage is local to APX, under
|
|
97
|
+
`~/.apx/`:
|
|
98
98
|
|
|
99
99
|
| Channel | What it captures |
|
|
100
100
|
|---------|-----------------|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentprojectcontext/apx",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
"node-fetch": "^3.3.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"
|
|
36
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
37
|
+
"@semantic-release/git": "^10.0.1",
|
|
38
|
+
"better-sqlite3": "^11.3.0",
|
|
39
|
+
"conventional-changelog-conventionalcommits": "^9.3.1"
|
|
37
40
|
},
|
|
38
41
|
"keywords": [
|
|
39
42
|
"apc",
|
package/skills/apx/SKILL.md
CHANGED
|
@@ -4,11 +4,13 @@ description: "APX CLI skill. Activate ONLY when the user asks about running agen
|
|
|
4
4
|
homepage: https://github.com/agentprojectcontext/apx
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# APX — Agent Project
|
|
7
|
+
# APX — Agent Project Context Runtime
|
|
8
8
|
|
|
9
9
|
This project uses **APX**. The daemon runs on `127.0.0.1:7430` and auto-starts on first `apx` call.
|
|
10
10
|
Your current session, project, and agent are already injected above this block — refer to them.
|
|
11
11
|
|
|
12
|
+
APX runtime state belongs outside `.apc/`, under `~/.apx/projects/<project-id>/`.
|
|
13
|
+
|
|
12
14
|
---
|
|
13
15
|
|
|
14
16
|
## Discover the project
|
|
@@ -32,7 +34,7 @@ apx exec <slug> "<prompt>"
|
|
|
32
34
|
The output of `apx run` / `apx exec` is the agent's full stdout.
|
|
33
35
|
If the agent printed `APC_RESULT: <value>`, that value is also captured as structured output.
|
|
34
36
|
|
|
35
|
-
## Memory — durable,
|
|
37
|
+
## Memory — durable, safe facts
|
|
36
38
|
|
|
37
39
|
```bash
|
|
38
40
|
apx memory <slug> # read agent's memory.md
|
|
@@ -40,7 +42,7 @@ apx memory <slug> --append "<fact>" # append a durable note (non-destructive
|
|
|
40
42
|
apx memory <slug> --replace < file.md # replace entire memory from stdin
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
Write to memory when you discover
|
|
45
|
+
Write to memory only when you discover safe project context the agent should know on future runs.
|
|
44
46
|
|
|
45
47
|
## Observe activity
|
|
46
48
|
|
|
@@ -74,4 +76,5 @@ Print this on the last meaningful line of your output:
|
|
|
74
76
|
APC_RESULT: <one-line summary or value>
|
|
75
77
|
```
|
|
76
78
|
The invoker (`apx run`, super-agent, Telegram bot) captures it as structured output.
|
|
77
|
-
Keep it factual and short
|
|
79
|
+
Keep it factual and short. It becomes the session result stored in APX local runtime state, not
|
|
80
|
+
inside `.apc/`.
|
|
@@ -109,11 +109,8 @@ export async function resolveProjectId(target) {
|
|
|
109
109
|
// No override: walk up from cwd
|
|
110
110
|
const root = findApfRoot();
|
|
111
111
|
if (!root) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
? `registered projects: ${registered.map((p) => `"${p.name}" (id ${p.id})`).join(", ")}`
|
|
115
|
-
: "no projects registered yet — run `apx project add <path>` first";
|
|
116
|
-
throw new Error(`not inside an APC project. Use --project <name|id|path>. ${hint}`);
|
|
112
|
+
// Fall back to the default project (id=0) — always available, no .apc/ required.
|
|
113
|
+
return 0;
|
|
117
114
|
}
|
|
118
115
|
const projects = await http.get("/projects");
|
|
119
116
|
const found = projects.find((p) => p.path === root);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { findApfRoot, readAgents } from "../../core/parser.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getOrCreateApxId } from "../../core/scaffold.js";
|
|
5
5
|
import { generateSessionId } from "../../core/session-store.js";
|
|
6
|
+
import { ensureProjectStorage } from "../../core/config.js";
|
|
6
7
|
import { http } from "../http.js";
|
|
7
8
|
import { resolveProjectId } from "./project.js";
|
|
8
9
|
|
|
@@ -14,6 +15,12 @@ function requireRoot() {
|
|
|
14
15
|
return root;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
function requireStorageRoot(root) {
|
|
19
|
+
const apxId = getOrCreateApxId(root);
|
|
20
|
+
if (!apxId) throw new Error("could not resolve APX project storage id");
|
|
21
|
+
return ensureProjectStorage(apxId);
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
18
25
|
|
|
19
26
|
function readStdinSync() {
|
|
@@ -61,7 +68,7 @@ function setFrontmatterField(text, field, value) {
|
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
function listAllSessions(root) {
|
|
64
|
-
const agentsDir = path.join(root, "
|
|
71
|
+
const agentsDir = path.join(requireStorageRoot(root), "agents");
|
|
65
72
|
if (!fs.existsSync(agentsDir)) return [];
|
|
66
73
|
const out = [];
|
|
67
74
|
for (const slug of fs.readdirSync(agentsDir)) {
|
|
@@ -124,10 +131,11 @@ export function cmdSessionNew(args) {
|
|
|
124
131
|
throw new Error(`agent "${slug}" not found in AGENTS.md`);
|
|
125
132
|
}
|
|
126
133
|
|
|
127
|
-
|
|
128
|
-
const id = generateSessionId(
|
|
134
|
+
const storageRoot = requireStorageRoot(root);
|
|
135
|
+
const id = generateSessionId(storageRoot, slug);
|
|
129
136
|
const filename = `${id}.md`;
|
|
130
|
-
const filepath = path.join(
|
|
137
|
+
const filepath = path.join(storageRoot, "agents", slug, "sessions", filename);
|
|
138
|
+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
131
139
|
|
|
132
140
|
const taskRef = args.flags["task-ref"] === true ? "" : (args.flags["task-ref"] || "");
|
|
133
141
|
let body = "";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import { getLatestVersion } from "../../core/update-check.js";
|
|
4
|
+
|
|
5
|
+
const PACKAGE_NAME = "@agentprojectcontext/apx";
|
|
6
|
+
|
|
7
|
+
export async function cmdUpdate(args) {
|
|
8
|
+
const force = args.flags.force || args.flags.yes || args.flags.y;
|
|
9
|
+
|
|
10
|
+
console.log("Checking for updates...");
|
|
11
|
+
const latest = await getLatestVersion();
|
|
12
|
+
|
|
13
|
+
if (!latest) {
|
|
14
|
+
console.error("Could not reach npm registry. Check your connection.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Read current version from the package that owns this file.
|
|
19
|
+
const { createRequire } = await import("node:module");
|
|
20
|
+
const { fileURLToPath } = await import("node:url");
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const pkg = require("../../package.json");
|
|
23
|
+
const current = pkg.version;
|
|
24
|
+
|
|
25
|
+
function isNewer(cur, lat) {
|
|
26
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
|
|
27
|
+
const [ma, mi, pa] = parse(cur);
|
|
28
|
+
const [mb, mib, pb] = parse(lat);
|
|
29
|
+
if (mb > ma) return true;
|
|
30
|
+
if (mb === ma && mib > mi) return true;
|
|
31
|
+
if (mb === ma && mib === mi && pb > pa) return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!isNewer(current, latest)) {
|
|
36
|
+
console.log(`✅ Already up to date (${current})`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`\n Current: ${current}`);
|
|
41
|
+
console.log(` Latest: ${latest}`);
|
|
42
|
+
|
|
43
|
+
if (!force) {
|
|
44
|
+
const confirmed = await confirm(`\nUpdate to ${latest}? [y/N] `);
|
|
45
|
+
if (!confirmed) {
|
|
46
|
+
console.log("Cancelled.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(`\nRunning: npm install -g ${PACKAGE_NAME}@${latest}\n`);
|
|
52
|
+
const result = spawnSync(
|
|
53
|
+
"npm",
|
|
54
|
+
["install", "-g", `${PACKAGE_NAME}@${latest}`],
|
|
55
|
+
{ stdio: "inherit" }
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (result.status !== 0) {
|
|
59
|
+
console.error(`\n❌ Update failed (exit ${result.status})`);
|
|
60
|
+
process.exit(result.status || 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`\n✅ Updated to ${latest}. Restart any running apx daemon:`);
|
|
64
|
+
console.log(` apx daemon stop && apx daemon start`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function confirm(prompt) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
70
|
+
rl.question(prompt, (answer) => {
|
|
71
|
+
rl.close();
|
|
72
|
+
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// apx — unified CLI for APC (Agent Project
|
|
2
|
+
// apx — unified CLI for APC (Agent Project Context).
|
|
3
3
|
// ESM, Node >= 18.
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
@@ -71,6 +71,8 @@ import { cmdPluginsList, cmdPluginStatus } from "./commands/plugins.js";
|
|
|
71
71
|
import { cmdSkillsAdd, cmdSkillsList, cmdSkillsStatus } from "./commands/skills.js";
|
|
72
72
|
import { cmdIdentity } from "./commands/identity.js";
|
|
73
73
|
import { cmdCommandList, cmdCommandShow } from "./commands/command.js";
|
|
74
|
+
import { cmdUpdate } from "./commands/update.js";
|
|
75
|
+
import { checkForUpdate } from "../core/update-check.js";
|
|
74
76
|
import {
|
|
75
77
|
cmdRoutineList,
|
|
76
78
|
cmdRoutineGet,
|
|
@@ -87,17 +89,18 @@ const VERSION = JSON.parse(
|
|
|
87
89
|
fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8")
|
|
88
90
|
).version;
|
|
89
91
|
|
|
90
|
-
const HELP = `apx — Agent Project
|
|
92
|
+
const HELP = `apx — Agent Project Context
|
|
91
93
|
|
|
92
94
|
Usage:
|
|
93
95
|
apx <command> [<subcommand>] [args] [--flags]
|
|
94
96
|
|
|
95
97
|
Bootstrap:
|
|
96
98
|
apx init [path] [--name "<name>"] initialize an APC project
|
|
97
|
-
apx project add
|
|
99
|
+
apx project add [path] register a project with the daemon
|
|
98
100
|
apx project list
|
|
99
101
|
apx project remove <path|id>
|
|
100
|
-
apx project rebuild [<path|id>] rebuild
|
|
102
|
+
apx project rebuild [<path|id>] rebuild project index from filesystem
|
|
103
|
+
apx add project [path] alias for: apx project add
|
|
101
104
|
|
|
102
105
|
Agents:
|
|
103
106
|
apx agent add <slug> [--role R] [--model M] [--skills a,b] [--language es-AR] [--description D]
|
|
@@ -144,7 +147,7 @@ Telegram:
|
|
|
144
147
|
Messages:
|
|
145
148
|
apx messages tail [--agent <slug>] [--channel <ch>] [-n 50] [--global]
|
|
146
149
|
global channels (telegram, direct, whatsapp) → ~/.apx/messages/<ch>/
|
|
147
|
-
project channels (runtime, a2a, exec) →
|
|
150
|
+
project channels (runtime, a2a, exec) → ~/.apx/projects/<id>/messages/
|
|
148
151
|
apx messages search "<query>"
|
|
149
152
|
|
|
150
153
|
LLM engines (v0.2):
|
|
@@ -200,6 +203,7 @@ Skills (IDE integration):
|
|
|
200
203
|
apx skills status show which IDE targets are installed (project + global)
|
|
201
204
|
|
|
202
205
|
Other:
|
|
206
|
+
apx update check for updates and upgrade (alias: apx upgrade)
|
|
203
207
|
apx --help
|
|
204
208
|
apx --version
|
|
205
209
|
|
|
@@ -466,6 +470,19 @@ async function dispatch(cmd, rest) {
|
|
|
466
470
|
await cmdIdentity(parseArgs(rest));
|
|
467
471
|
break;
|
|
468
472
|
|
|
473
|
+
case "add": {
|
|
474
|
+
// apx add <domain> [...args] — consistent alternative to apx <domain> add
|
|
475
|
+
const sub = rest[0];
|
|
476
|
+
if (sub === "project") await cmdProjectAdd(parseArgs(rest.slice(1)));
|
|
477
|
+
else die(`unknown 'add' subcommand: ${sub || "(none)"} — try: project`);
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
case "update":
|
|
482
|
+
case "upgrade":
|
|
483
|
+
await cmdUpdate(parseArgs(rest));
|
|
484
|
+
return; // skip checkForUpdate after an update
|
|
485
|
+
|
|
469
486
|
default:
|
|
470
487
|
die(`unknown command: ${cmd}\nRun \`apx --help\` for usage.`);
|
|
471
488
|
}
|
|
@@ -475,6 +492,7 @@ const [topCmd, ...topRest] = argv;
|
|
|
475
492
|
(async () => {
|
|
476
493
|
try {
|
|
477
494
|
await dispatch(topCmd, topRest);
|
|
495
|
+
checkForUpdate(VERSION);
|
|
478
496
|
} catch (err) {
|
|
479
497
|
die(err && err.message ? err.message : String(err));
|
|
480
498
|
}
|
|
@@ -1,150 +1,102 @@
|
|
|
1
1
|
|
|
2
2
|
# Agent Project Context
|
|
3
3
|
|
|
4
|
-
This project uses APC.
|
|
4
|
+
This project uses APC. APC stores portable project context in `.apc/` and `AGENTS.md`.
|
|
5
|
+
|
|
6
|
+
APC does not store raw runtime sessions. Sessions, conversations, messages, caches, provider
|
|
7
|
+
threads, and private runtime memory stay in the IDE, CLI, daemon, or user-level store that created
|
|
8
|
+
them.
|
|
5
9
|
|
|
6
10
|
## FIRST: check for pending migration
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
Before doing anything else, check if `.apc/migrate.md` exists:
|
|
9
13
|
|
|
10
14
|
```bash
|
|
11
15
|
cat .apc/migrate.md 2>/dev/null
|
|
12
16
|
```
|
|
13
17
|
|
|
14
|
-
If it exists,
|
|
15
|
-
|
|
16
|
-
> I see this project was just initialized with **Agent Project Context (APC)**.
|
|
17
|
-
>
|
|
18
|
-
> I found context files that haven't been migrated yet:
|
|
19
|
-
> [list files from .apc/migrate.md]
|
|
20
|
-
>
|
|
21
|
-
> I'll read them, understand what's in them, and migrate intelligently — keeping only what APC doesn't already handle.
|
|
22
|
-
>
|
|
23
|
-
> **Want me to start?**
|
|
18
|
+
If it exists, offer to migrate before answering anything else. Read detected files, separate durable
|
|
19
|
+
project context from runtime/private state, and migrate only what belongs in APC.
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
If the user says no or later, delete `.apc/migrate.md` so the offer is not repeated.
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
## Migration rule: think, do not copy
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
Classify content:
|
|
30
26
|
|
|
31
|
-
|
|
|
27
|
+
| Content | Action |
|
|
32
28
|
|---|---|
|
|
33
|
-
| Agent definitions
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
**Step 6 — Summarize** what was created, what was kept, and what was dropped (and why).
|
|
48
|
-
|
|
49
|
-
If the user says no or later: delete `.apc/migrate.md` immediately so this offer is not shown again in future sessions.
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## Structure
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
AGENTS.md ← project context: rules, stack, conventions (commit)
|
|
29
|
+
| Agent definitions: role, model, skills, description | Put in `.apc/agents/<slug>.md` and/or `AGENTS.md` |
|
|
30
|
+
| Shared project rules, stack notes, commands, testing policy | Keep in `AGENTS.md` |
|
|
31
|
+
| Reusable instruction blocks | Move to `.apc/skills/<name>.md` |
|
|
32
|
+
| Durable safe facts useful to all contributors | Add to `.apc/agents/<slug>/memory.md` only after curation |
|
|
33
|
+
| MCP expectations without secrets | Add to `.apc/mcps.json` |
|
|
34
|
+
| Raw sessions, transcripts, conversations, messages, tool logs | Do not move into `.apc/`; leave with source runtime |
|
|
35
|
+
| Secrets, tokens, credentials, private headers | Do not store in repository |
|
|
36
|
+
| IDE UI settings or personal aliases | Leave in IDE/user config |
|
|
37
|
+
| Instructions to store sessions under `.apc/` | Drop as obsolete |
|
|
38
|
+
|
|
39
|
+
## APC structure
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
AGENTS.md ← root project contract
|
|
57
43
|
.apc/
|
|
58
|
-
project.json ← metadata
|
|
59
|
-
.gitignore ←
|
|
60
|
-
agents/<slug>.md ← agent definition
|
|
61
|
-
agents/<slug>/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
skills/ ← reusable prompt fragments (commit)
|
|
65
|
-
mcps.json ← MCP declarations without secrets (commit)
|
|
44
|
+
project.json ← project metadata
|
|
45
|
+
.gitignore ← safety guard
|
|
46
|
+
agents/<slug>.md ← agent definition
|
|
47
|
+
agents/<slug>/memory.md ← optional curated project memory
|
|
48
|
+
skills/<name>.md ← reusable project instructions
|
|
49
|
+
mcps.json ← MCP hints without secrets
|
|
66
50
|
```
|
|
67
51
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
## Rules
|
|
82
|
-
|
|
83
|
-
1. Read your definition and memory from `.apc/agents/<your-slug>/`
|
|
84
|
-
2. Write memory to `.apc/agents/<your-slug>/memory.md` — never to IDE-specific folders
|
|
85
|
-
3. Write raw session logs to `.apc/agents/<your-slug>/sessions/` — they are gitignored
|
|
86
|
-
4. `AGENTS.md` is the neutral project context file — edit it directly, it is not auto-generated
|
|
87
|
-
5. To list agents: read `AGENTS.md` or list `.apc/agents/*.md`
|
|
88
|
-
|
|
89
|
-
## Sessions — write one at the end of every task
|
|
90
|
-
|
|
91
|
-
Sessions are the record of what was done, for future agents and for the team.
|
|
52
|
+
Do not store:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
.apc/agents/<slug>/sessions/
|
|
56
|
+
.apc/sessions/
|
|
57
|
+
.apc/conversations/
|
|
58
|
+
.apc/messages/
|
|
59
|
+
.apc/project.db
|
|
60
|
+
.apc/cache/
|
|
61
|
+
.apc/tmp/
|
|
62
|
+
.apc/private/
|
|
63
|
+
.apc/secrets/
|
|
64
|
+
```
|
|
92
65
|
|
|
93
|
-
|
|
94
|
-
- APC standard path: `.apc/agents/<slug>/sessions/<date>-<slug>.md` (gitignored)
|
|
95
|
-
- Project-specific path: check `AGENTS.md` — some projects use `works/sessions/` or similar
|
|
66
|
+
## Operating rules
|
|
96
67
|
|
|
97
|
-
|
|
68
|
+
1. Read `AGENTS.md` and relevant `.apc/` files before assuming project context.
|
|
69
|
+
2. Read agent definitions from `.apc/agents/<slug>.md` when present.
|
|
70
|
+
3. Read curated project memory from `.apc/agents/<slug>/memory.md` when present.
|
|
71
|
+
4. Write only durable, safe, curated facts to APC memory.
|
|
72
|
+
5. Never write raw sessions, transcripts, messages, conversations, or tool logs into `.apc/`.
|
|
73
|
+
6. Keep secrets out of APC and out of git.
|
|
74
|
+
7. Treat `.apc/mcps.json` as MCP configuration hints, not as an MCP implementation.
|
|
98
75
|
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
title: Short title of what was done
|
|
102
|
-
description: One-line summary for the index
|
|
103
|
-
date: YYYY-MM-DD
|
|
104
|
-
status: open | completed | in-progress | abandoned
|
|
105
|
-
---
|
|
76
|
+
## Sessions
|
|
106
77
|
|
|
107
|
-
|
|
108
|
-
What was the task or request.
|
|
78
|
+
Sessions belong to the runtime that created them.
|
|
109
79
|
|
|
110
|
-
|
|
111
|
-
What was completed. Be specific — file paths, decisions, changes.
|
|
80
|
+
Examples:
|
|
112
81
|
|
|
113
|
-
|
|
114
|
-
|
|
82
|
+
```text
|
|
83
|
+
Codex runtime storage
|
|
84
|
+
Claude Code runtime storage
|
|
85
|
+
OpenCode runtime storage
|
|
86
|
+
~/.apx/projects/<project-id>/agents/<slug>/sessions/
|
|
115
87
|
```
|
|
116
88
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
```bash
|
|
120
|
-
apx session new <slug> --title "Short title"
|
|
121
|
-
```
|
|
122
|
-
Then fill in `description`, update `status` to `completed` when done.
|
|
123
|
-
- If APX is not available: create the file manually following the format above.
|
|
124
|
-
|
|
125
|
-
**When to write:** at the end of every task session, before closing. Not optional.
|
|
89
|
+
At task end, provide the user a concise result. If project memory should be updated, write a short
|
|
90
|
+
sanitized fact to `.apc/agents/<slug>/memory.md` only when useful and safe.
|
|
126
91
|
|
|
127
|
-
## APX
|
|
92
|
+
## APX
|
|
128
93
|
|
|
129
|
-
|
|
94
|
+
APX can provide a local daemon, MCP management, Telegram bridge, routines, and runtime dispatch
|
|
95
|
+
across Codex, Claude Code, OpenCode, Aider, or direct LLM engines. Those are APX runtime features,
|
|
96
|
+
not APC portable-core requirements.
|
|
130
97
|
|
|
131
|
-
|
|
132
|
-
- **`"declined"`** — user chose not to install. Never suggest or run `apx` commands.
|
|
133
|
-
- **`null` or missing** — unknown. Do the one-time check below, then write the result and never ask again.
|
|
98
|
+
APX runtime state belongs outside the repository:
|
|
134
99
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
apx --version 2>/dev/null && echo "FOUND" || echo "NOT_FOUND"
|
|
100
|
+
```text
|
|
101
|
+
~/.apx/projects/<project-id>/
|
|
139
102
|
```
|
|
140
|
-
|
|
141
|
-
- If `FOUND`: write `"apx": "installed"` to `.apc/project.json`. Use APX normally.
|
|
142
|
-
- If `NOT_FOUND`: tell the user once:
|
|
143
|
-
|
|
144
|
-
> APX is not installed. It adds agent coordination, MCP server management, and memory commands to this project.
|
|
145
|
-
> Want me to install it? (`npm install -g apx`)
|
|
146
|
-
|
|
147
|
-
- If yes: run `npm install -g apx`, then write `"apx": "installed"`.
|
|
148
|
-
- If no: write `"apx": "declined"`. Never mention APX again in this project.
|
|
149
|
-
|
|
150
|
-
**Never run `apx` commands if the field is `null`, `"declined"`, or unconfirmed.**
|
package/src/core/apx-skill.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
# APX — Agent Project
|
|
1
|
+
# APX — Agent Project Context Runtime
|
|
2
2
|
|
|
3
3
|
The daemon runs on `127.0.0.1:7430` and auto-starts on first `apx` call.
|
|
4
4
|
|
|
5
|
+
APX reads APC project context from `.apc/`, but APX runtime state belongs outside the repository
|
|
6
|
+
under `~/.apx/projects/<project-id>/`.
|
|
7
|
+
|
|
5
8
|
---
|
|
6
9
|
|
|
7
10
|
## Coordinate with other agents
|
|
@@ -48,6 +51,8 @@ apx mcp run filesystem read_file '{"path": "README.md"}'
|
|
|
48
51
|
|
|
49
52
|
## Memory
|
|
50
53
|
|
|
54
|
+
Write memory only for durable, safe project facts. Do not store raw transcripts or secrets.
|
|
55
|
+
|
|
51
56
|
```bash
|
|
52
57
|
apx memory <slug> # read agent's memory.md
|
|
53
58
|
apx memory <slug> --append "<fact>" # append a durable note
|
|
@@ -56,8 +61,10 @@ apx memory <slug> --replace < file.md # replace entire memory from stdin
|
|
|
56
61
|
|
|
57
62
|
## Sessions
|
|
58
63
|
|
|
64
|
+
Sessions are APX runtime state. They do not belong in `.apc/`.
|
|
65
|
+
|
|
59
66
|
```bash
|
|
60
|
-
apx session new <slug> --title "What you did" # create session file
|
|
67
|
+
apx session new <slug> --title "What you did" # create APX local session file
|
|
61
68
|
apx session list <slug> # list sessions
|
|
62
69
|
apx session check # exits 1 if session already active
|
|
63
70
|
```
|
package/src/core/config.js
CHANGED
|
@@ -11,6 +11,21 @@ export const TELEGRAM_STATE_PATH = path.join(APX_HOME, "telegram-state.json");
|
|
|
11
11
|
// Global channel messages (telegram, direct, whatsapp, …) live here,
|
|
12
12
|
// separated from any project. Structure: ~/.apx/messages/<channel>/YYYY-MM-DD.jsonl
|
|
13
13
|
export const GLOBAL_MESSAGES_DIR = path.join(APX_HOME, "messages");
|
|
14
|
+
// Per-project runtime storage (conversations, sessions) — never in the repo.
|
|
15
|
+
// Structure: ~/.apx/projects/<apx_id>/agents/<slug>/conversations/
|
|
16
|
+
export const PROJECT_STORE_ROOT = path.join(APX_HOME, "projects");
|
|
17
|
+
export const DEFAULT_PROJECT_ID = "default";
|
|
18
|
+
export const DEFAULT_PROJECT_STORE = path.join(PROJECT_STORE_ROOT, DEFAULT_PROJECT_ID);
|
|
19
|
+
|
|
20
|
+
export function projectStorageRoot(apxId) {
|
|
21
|
+
return path.join(PROJECT_STORE_ROOT, apxId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ensureProjectStorage(apxId) {
|
|
25
|
+
const root = projectStorageRoot(apxId);
|
|
26
|
+
fs.mkdirSync(root, { recursive: true });
|
|
27
|
+
return root;
|
|
28
|
+
}
|
|
14
29
|
|
|
15
30
|
const DEFAULT_CONFIG = {
|
|
16
31
|
port: 7430,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Messages store: filesystem source-of-truth + SQLite cache mirror.
|
|
2
2
|
//
|
|
3
3
|
// On disk (project-specific — runtime, a2a, exec):
|
|
4
|
-
//
|
|
4
|
+
// ~/.apx/projects/<project-id>/messages/YYYY-MM-DD.jsonl
|
|
5
5
|
//
|
|
6
6
|
// On disk (global cross-project channels — telegram, direct, whatsapp, …):
|
|
7
7
|
// ~/.apx/messages/<channel>/YYYY-MM-DD.jsonl
|
|
@@ -24,12 +24,12 @@ const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
|
24
24
|
|
|
25
25
|
function dayPathJsonl(projectRoot, ts) {
|
|
26
26
|
const day = (ts || nowIso()).slice(0, 10);
|
|
27
|
-
return path.join(projectRoot, "
|
|
27
|
+
return path.join(projectRoot, "messages", `${day}.jsonl`);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function dayPathMd(projectRoot, ts) {
|
|
31
31
|
const day = (ts || nowIso()).slice(0, 10);
|
|
32
|
-
return path.join(projectRoot, "
|
|
32
|
+
return path.join(projectRoot, "messages", `${day}.md`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function appendMessageToFs({ projectRoot, channel, direction, author, body, meta = {}, ts, agent_slug, session_id, external_id }) {
|
|
@@ -262,7 +262,7 @@ function sanitizeAssistantForContext(content) {
|
|
|
262
262
|
// ---------------------------------------------------------------------------
|
|
263
263
|
|
|
264
264
|
export function readProjectMessages(projectRoot, { channel, agent_slug, since, limit = 100 } = {}) {
|
|
265
|
-
const dir = path.join(projectRoot, "
|
|
265
|
+
const dir = path.join(projectRoot, "messages");
|
|
266
266
|
if (!fs.existsSync(dir)) return [];
|
|
267
267
|
const all = [];
|
|
268
268
|
for (const f of fs.readdirSync(dir).sort()) {
|
|
@@ -285,7 +285,7 @@ export function readProjectMessages(projectRoot, { channel, agent_slug, since, l
|
|
|
285
285
|
export function searchProjectMessages(projectRoot, query, limit = 50) {
|
|
286
286
|
if (!query) return [];
|
|
287
287
|
const q = query.toLowerCase();
|
|
288
|
-
const dir = path.join(projectRoot, "
|
|
288
|
+
const dir = path.join(projectRoot, "messages");
|
|
289
289
|
if (!fs.existsSync(dir)) return [];
|
|
290
290
|
const all = [];
|
|
291
291
|
for (const f of fs.readdirSync(dir).sort()) {
|
|
@@ -392,10 +392,10 @@ export function readGlobalMessages({ channel, limit = 100, since } = {}) {
|
|
|
392
392
|
return all.slice(-limit);
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
// Wipe the cache and re-populate from .
|
|
395
|
+
// Wipe the cache and re-populate from APX project messages. Reads BOTH `.jsonl`
|
|
396
396
|
// (current format) and `.md` (legacy). Called by rebuild.
|
|
397
397
|
export function rebuildMessagesFromFs(db, projectRoot) {
|
|
398
|
-
const dir = path.join(projectRoot, "
|
|
398
|
+
const dir = path.join(projectRoot, "messages");
|
|
399
399
|
if (!fs.existsSync(dir)) return { count: 0 };
|
|
400
400
|
db.prepare("DELETE FROM messages").run();
|
|
401
401
|
|
package/src/core/scaffold.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import crypto from "node:crypto";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { readAgents, readAgentsFromDir, VAULT_DIR } from "./parser.js";
|
|
6
7
|
|
|
@@ -188,9 +189,13 @@ const AGENTS_MD_TEMPLATE = `# Agents
|
|
|
188
189
|
-->
|
|
189
190
|
`;
|
|
190
191
|
|
|
191
|
-
const APC_GITIGNORE = `# APC runtime data —
|
|
192
|
+
const APC_GITIGNORE = `# APC runtime data — never in the repository
|
|
193
|
+
# Chat conversations and runtime sessions belong in ~/.apx/projects/<id>/
|
|
192
194
|
agents/*/sessions/
|
|
195
|
+
agents/*/conversations/
|
|
193
196
|
sessions/
|
|
197
|
+
conversations/
|
|
198
|
+
messages/
|
|
194
199
|
chats/
|
|
195
200
|
cache/
|
|
196
201
|
tmp/
|
|
@@ -200,6 +205,7 @@ secrets/
|
|
|
200
205
|
*.secret.json
|
|
201
206
|
*.env
|
|
202
207
|
*.env.*
|
|
208
|
+
project.db
|
|
203
209
|
migrate.md
|
|
204
210
|
`;
|
|
205
211
|
|
|
@@ -241,6 +247,20 @@ function writeMigrateMd(apfDir, found) {
|
|
|
241
247
|
fs.writeFileSync(path.join(apfDir, "migrate.md"), lines.join("\n") + "\n");
|
|
242
248
|
}
|
|
243
249
|
|
|
250
|
+
// Get the stable APX storage ID for a project, generating one if it doesn't exist.
|
|
251
|
+
// Called by the daemon when registering a project.
|
|
252
|
+
export function getOrCreateApxId(root) {
|
|
253
|
+
const p = path.join(root, ".apc", "project.json");
|
|
254
|
+
if (!fs.existsSync(p)) return null;
|
|
255
|
+
let cfg;
|
|
256
|
+
try { cfg = JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
257
|
+
if (cfg.apx_id) return cfg.apx_id;
|
|
258
|
+
const apxId = crypto.randomUUID().replace(/-/g, "").slice(0, 12);
|
|
259
|
+
cfg.apx_id = apxId;
|
|
260
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + "\n");
|
|
261
|
+
return apxId;
|
|
262
|
+
}
|
|
263
|
+
|
|
244
264
|
export function initApf(directory, { name } = {}) {
|
|
245
265
|
const root = path.resolve(directory);
|
|
246
266
|
fs.mkdirSync(root, { recursive: true });
|
|
@@ -252,6 +272,7 @@ export function initApf(directory, { name } = {}) {
|
|
|
252
272
|
|
|
253
273
|
const projectJson = path.join(apfDir, "project.json");
|
|
254
274
|
if (!fs.existsSync(projectJson)) {
|
|
275
|
+
const apxId = crypto.randomUUID().replace(/-/g, "").slice(0, 12);
|
|
255
276
|
fs.writeFileSync(
|
|
256
277
|
projectJson,
|
|
257
278
|
JSON.stringify(
|
|
@@ -261,7 +282,7 @@ export function initApf(directory, { name } = {}) {
|
|
|
261
282
|
apf: SPEC_VERSION,
|
|
262
283
|
created: nowIso(),
|
|
263
284
|
apx: null,
|
|
264
|
-
|
|
285
|
+
apx_id: apxId,
|
|
265
286
|
},
|
|
266
287
|
null,
|
|
267
288
|
2
|
|
@@ -291,7 +312,7 @@ export function initApf(directory, { name } = {}) {
|
|
|
291
312
|
|
|
292
313
|
export function ensureAgentDir(root, slug) {
|
|
293
314
|
const dir = path.join(root, ".apc", "agents", slug);
|
|
294
|
-
fs.mkdirSync(
|
|
315
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
295
316
|
const memory = path.join(dir, "memory.md");
|
|
296
317
|
if (!fs.existsSync(memory)) {
|
|
297
318
|
fs.writeFileSync(
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
|
|
8
|
-
export function generateSessionId(
|
|
8
|
+
export function generateSessionId(storageRoot, agentSlug) {
|
|
9
9
|
const today = new Date().toISOString().slice(0, 10);
|
|
10
|
-
const dir = path.join(
|
|
10
|
+
const dir = path.join(storageRoot, "agents", agentSlug, "sessions");
|
|
11
11
|
let next = 1;
|
|
12
12
|
if (fs.existsSync(dir)) {
|
|
13
13
|
for (const f of fs.readdirSync(dir)) {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Update checker — non-blocking, cached 24h.
|
|
2
|
+
// On each command: reads cache → shows message if newer version exists.
|
|
3
|
+
// In background: refreshes cache from npm registry (fire-and-forget).
|
|
4
|
+
// Never slows down the main command.
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import https from "node:https";
|
|
9
|
+
import { APX_HOME } from "./config.js";
|
|
10
|
+
|
|
11
|
+
const PACKAGE_NAME = "@agentprojectcontext/apx";
|
|
12
|
+
const CACHE_PATH = path.join(APX_HOME, "update-check.json");
|
|
13
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
14
|
+
|
|
15
|
+
function readCache() {
|
|
16
|
+
try {
|
|
17
|
+
const raw = fs.readFileSync(CACHE_PATH, "utf8");
|
|
18
|
+
return JSON.parse(raw);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeCache(data) {
|
|
25
|
+
try {
|
|
26
|
+
fs.mkdirSync(path.dirname(CACHE_PATH), { recursive: true });
|
|
27
|
+
fs.writeFileSync(CACHE_PATH, JSON.stringify(data) + "\n");
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Compare semver strings. Returns true if `latest` > `current`.
|
|
32
|
+
function isNewer(current, latest) {
|
|
33
|
+
if (!current || !latest) return false;
|
|
34
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
|
|
35
|
+
const [ma, mi, pa] = parse(current);
|
|
36
|
+
const [mb, mib, pb] = parse(latest);
|
|
37
|
+
if (mb > ma) return true;
|
|
38
|
+
if (mb === ma && mib > mi) return true;
|
|
39
|
+
if (mb === ma && mib === mi && pb > pa) return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fetch latest version from npm registry (async, no deps).
|
|
44
|
+
function fetchLatest() {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
const url = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
47
|
+
const req = https.get(url, { timeout: 5000 }, (res) => {
|
|
48
|
+
let body = "";
|
|
49
|
+
res.on("data", (c) => (body += c));
|
|
50
|
+
res.on("end", () => {
|
|
51
|
+
try {
|
|
52
|
+
resolve(JSON.parse(body).version || null);
|
|
53
|
+
} catch {
|
|
54
|
+
resolve(null);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
req.on("error", () => resolve(null));
|
|
59
|
+
req.on("timeout", () => { req.destroy(); resolve(null); });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fire-and-forget background refresh. Never awaited by the caller.
|
|
64
|
+
function refreshInBackground(currentVersion) {
|
|
65
|
+
fetchLatest().then((latest) => {
|
|
66
|
+
if (latest) {
|
|
67
|
+
writeCache({ latest, current: currentVersion, checkedAt: Date.now() });
|
|
68
|
+
}
|
|
69
|
+
}).catch(() => {});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Call this at the END of every command (after output is printed).
|
|
73
|
+
// Shows an update notice if a newer version is cached.
|
|
74
|
+
// Also triggers a background refresh if cache is stale.
|
|
75
|
+
export function checkForUpdate(currentVersion) {
|
|
76
|
+
const cache = readCache();
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
|
|
79
|
+
// Trigger background refresh if cache is stale or missing.
|
|
80
|
+
if (!cache || (now - (cache.checkedAt || 0)) > CACHE_TTL_MS) {
|
|
81
|
+
refreshInBackground(currentVersion);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Show notice if cache has a newer version.
|
|
85
|
+
if (cache && isNewer(currentVersion, cache.latest)) {
|
|
86
|
+
const divider = "─".repeat(56);
|
|
87
|
+
process.stderr.write(
|
|
88
|
+
`\n${divider}\n` +
|
|
89
|
+
` apx update available ${currentVersion} → ${cache.latest}\n` +
|
|
90
|
+
` run: apx update\n` +
|
|
91
|
+
`${divider}\n`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Used by `apx update` command to get the latest version (with network call).
|
|
97
|
+
export async function getLatestVersion() {
|
|
98
|
+
return await fetchLatest();
|
|
99
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Helpers that wrap external runtimes (Claude Code, Codex, OpenCode, Aider)
|
|
2
2
|
// with APC awareness:
|
|
3
3
|
//
|
|
4
|
-
// 1. Create an
|
|
4
|
+
// 1. Create an APX runtime session BEFORE the runtime starts.
|
|
5
5
|
// 2. Inject an "APC Runtime Context" block into the system prompt so the
|
|
6
6
|
// runtime knows the session id, the cwd of the project, and the apx
|
|
7
7
|
// commands it can use to update memory / append session notes.
|
|
8
8
|
// 3. After the runtime returns, capture the external transcript path
|
|
9
9
|
// (Claude Code gives one, Codex/OpenCode/Aider don't yet) and write it
|
|
10
|
-
// into the
|
|
10
|
+
// into the APX session frontmatter.
|
|
11
11
|
// 4. Close the session with a synthesised result (truncated stdout).
|
|
12
12
|
//
|
|
13
13
|
// Used by both POST /projects/:pid/agents/:slug/runtime (CLI) and the
|
|
@@ -22,12 +22,12 @@ const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
|
22
22
|
const APC_RUNTIME_HINT = `
|
|
23
23
|
# APC Runtime Context
|
|
24
24
|
|
|
25
|
-
You are running inside an APC (Agent Project
|
|
25
|
+
You are running inside an APC (Agent Project Context) project. APC gives you portable project context. APX gives you local runtime session state.
|
|
26
26
|
|
|
27
27
|
- **Project**: {{name}} ({{path}})
|
|
28
28
|
- **Agent**: {{agent}}
|
|
29
29
|
- **APC session id**: {{session_id}}
|
|
30
|
-
(
|
|
30
|
+
(stored in APX local runtime storage, outside .apc/)
|
|
31
31
|
|
|
32
32
|
## Commands you can use during this run
|
|
33
33
|
|
|
@@ -52,11 +52,11 @@ export function buildApfHint({ projectName, projectPath, agentSlug, sessionId })
|
|
|
52
52
|
.replace(/\{\{session_id\}\}/g, sessionId);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// Create the
|
|
56
|
-
export function createRuntimeSession({ projectRoot, agentSlug, runtime, taskRef = "", title }) {
|
|
57
|
-
const dir = path.join(
|
|
55
|
+
// Create the APX runtime session file on disk. Returns { id, filename, path }.
|
|
56
|
+
export function createRuntimeSession({ projectRoot, storageRoot = projectRoot, agentSlug, runtime, taskRef = "", title }) {
|
|
57
|
+
const dir = path.join(storageRoot, "agents", agentSlug, "sessions");
|
|
58
58
|
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
-
const id = generateSessionId(
|
|
59
|
+
const id = generateSessionId(storageRoot, agentSlug);
|
|
60
60
|
const file = path.join(dir, `${id}.md`);
|
|
61
61
|
const started = nowIso();
|
|
62
62
|
const sessionTitle = title || `Runtime: ${runtime}`;
|
package/src/daemon/api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Express REST API for APX. See docs/
|
|
1
|
+
// Express REST API for APX. See APC docs reference/apx-daemon.
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import express from "express";
|
|
@@ -175,7 +175,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
175
175
|
const agents = readAgents(p.path);
|
|
176
176
|
if (!agents.find((a) => a.slug === req.params.slug))
|
|
177
177
|
return res.status(404).json({ error: "agent not found" });
|
|
178
|
-
const sessionsDir = path.join(p.
|
|
178
|
+
const sessionsDir = path.join(p.storagePath, "agents", req.params.slug, "sessions");
|
|
179
179
|
if (!fs.existsSync(sessionsDir)) return res.json([]);
|
|
180
180
|
const sessions = fs
|
|
181
181
|
.readdirSync(sessionsDir)
|
|
@@ -201,7 +201,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
201
201
|
if (!p) return;
|
|
202
202
|
const { title, body = "" } = req.body || {};
|
|
203
203
|
if (!title) return res.status(400).json({ error: "title required" });
|
|
204
|
-
const sessionsDir = path.join(p.
|
|
204
|
+
const sessionsDir = path.join(p.storagePath, "agents", req.params.slug, "sessions");
|
|
205
205
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
206
206
|
const titleSlug =
|
|
207
207
|
title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "session";
|
|
@@ -225,7 +225,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
225
225
|
if (!p) return;
|
|
226
226
|
const sid = req.params.sid;
|
|
227
227
|
const filename = sid.endsWith(".md") ? sid : `${sid}.md`;
|
|
228
|
-
const agentsDir = path.join(p.
|
|
228
|
+
const agentsDir = path.join(p.storagePath, "agents");
|
|
229
229
|
let found = null;
|
|
230
230
|
if (fs.existsSync(agentsDir)) {
|
|
231
231
|
for (const slug of fs.readdirSync(agentsDir)) {
|
|
@@ -335,7 +335,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
335
335
|
const p = project(req, res);
|
|
336
336
|
if (!p) return;
|
|
337
337
|
const { agent, channel, since, limit = "100" } = req.query;
|
|
338
|
-
const rows = readProjectMessages(p.
|
|
338
|
+
const rows = readProjectMessages(p.storagePath, {
|
|
339
339
|
channel: channel || undefined,
|
|
340
340
|
agent_slug: agent || undefined,
|
|
341
341
|
since: since || undefined,
|
|
@@ -362,7 +362,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
362
362
|
if (!p) return;
|
|
363
363
|
const { q, limit = "50" } = req.query;
|
|
364
364
|
if (!q) return res.status(400).json({ error: "q required" });
|
|
365
|
-
res.json(searchProjectMessages(p.
|
|
365
|
+
res.json(searchProjectMessages(p.storagePath, q, Math.min(parseInt(limit, 10) || 50, 500)));
|
|
366
366
|
});
|
|
367
367
|
|
|
368
368
|
// ---- Global messages (cross-project channels: telegram, direct, …) ----
|
|
@@ -421,7 +421,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
421
421
|
|
|
422
422
|
try {
|
|
423
423
|
const system = buildAgentSystem(p, agent);
|
|
424
|
-
const conv = startConversation({
|
|
424
|
+
const conv = startConversation({ storagePath: p.storagePath, agentSlug: agent.slug, engine: modelId, system });
|
|
425
425
|
appendTurn({ filePath: conv.path, role: "user", content: prompt });
|
|
426
426
|
|
|
427
427
|
const result = await callEngine({
|
|
@@ -470,7 +470,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
470
470
|
let compactSummary = null;
|
|
471
471
|
|
|
472
472
|
if (conversation_id) {
|
|
473
|
-
const existing = readConversation(p.
|
|
473
|
+
const existing = readConversation(p.storagePath, agent.slug, conversation_id);
|
|
474
474
|
if (!existing) return res.status(404).json({ error: `conversation ${conversation_id} not found` });
|
|
475
475
|
convPath = existing.path;
|
|
476
476
|
convId = conversation_id;
|
|
@@ -492,7 +492,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
492
492
|
const system = buildAgentSystem(p, agent, { extraParts });
|
|
493
493
|
|
|
494
494
|
if (!conversation_id) {
|
|
495
|
-
const conv = startConversation({
|
|
495
|
+
const conv = startConversation({ storagePath: p.storagePath, agentSlug: agent.slug, engine: modelId, system });
|
|
496
496
|
convPath = conv.path;
|
|
497
497
|
convId = conv.id;
|
|
498
498
|
}
|
|
@@ -523,14 +523,14 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
523
523
|
const agents = readAgents(p.path);
|
|
524
524
|
if (!agents.find((a) => a.slug === req.params.slug))
|
|
525
525
|
return res.status(404).json({ error: "agent not found" });
|
|
526
|
-
res.json(listConversations(p.
|
|
526
|
+
res.json(listConversations(p.storagePath, req.params.slug));
|
|
527
527
|
});
|
|
528
528
|
|
|
529
529
|
// GET /projects/:pid/agents/:slug/conversations/:id
|
|
530
530
|
app.get("/projects/:pid/agents/:slug/conversations/:id", (req, res) => {
|
|
531
531
|
const p = project(req, res);
|
|
532
532
|
if (!p) return;
|
|
533
|
-
const conv = readConversation(p.
|
|
533
|
+
const conv = readConversation(p.storagePath, req.params.slug, req.params.id);
|
|
534
534
|
if (!conv) return res.status(404).json({ error: "conversation not found" });
|
|
535
535
|
res.json(conv);
|
|
536
536
|
});
|
|
@@ -547,7 +547,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
547
547
|
if (!modelId) return res.status(400).json({ error: "agent has no model" });
|
|
548
548
|
try {
|
|
549
549
|
const result = await compactConversation({
|
|
550
|
-
|
|
550
|
+
storagePath: p.storagePath,
|
|
551
551
|
agentSlug: agent.slug,
|
|
552
552
|
filename: filename || null,
|
|
553
553
|
modelId,
|
|
@@ -625,7 +625,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
625
625
|
if (!agents.find((a) => a.slug === req.params.slug))
|
|
626
626
|
return res.status(404).json({ error: "agent not found" });
|
|
627
627
|
|
|
628
|
-
const messages = readProjectMessages(p.
|
|
628
|
+
const messages = readProjectMessages(p.storagePath, { agent_slug: req.params.slug });
|
|
629
629
|
const peers = new Map();
|
|
630
630
|
for (const m of messages) {
|
|
631
631
|
const peer = m.meta?.from || m.meta?.to || null;
|
|
@@ -679,6 +679,7 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
679
679
|
|
|
680
680
|
const session = createRuntimeSession({
|
|
681
681
|
projectRoot: p.path,
|
|
682
|
+
storageRoot: p.storagePath,
|
|
682
683
|
agentSlug: agent.slug,
|
|
683
684
|
runtime,
|
|
684
685
|
title: req.body?.title,
|
package/src/daemon/compact.js
CHANGED
|
@@ -52,8 +52,8 @@ Style: dense and factual. No pleasantries. No meta-commentary. Just the facts.
|
|
|
52
52
|
|
|
53
53
|
// Resolve the most-recent conversation file for an agent, or the one explicitly
|
|
54
54
|
// named. Returns the full filepath.
|
|
55
|
-
function resolveConvFile(
|
|
56
|
-
const dir = path.join(
|
|
55
|
+
function resolveConvFile(storagePath, agentSlug, filename) {
|
|
56
|
+
const dir = path.join(storagePath, "agents", agentSlug, "conversations");
|
|
57
57
|
if (!fs.existsSync(dir)) throw new Error(`no conversations dir for agent "${agentSlug}"`);
|
|
58
58
|
|
|
59
59
|
if (filename) {
|
|
@@ -76,13 +76,13 @@ function serializeFm(obj) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
export async function compactConversation({
|
|
79
|
-
|
|
79
|
+
storagePath,
|
|
80
80
|
agentSlug,
|
|
81
81
|
filename,
|
|
82
82
|
modelId,
|
|
83
83
|
config,
|
|
84
84
|
}) {
|
|
85
|
-
const filepath = resolveConvFile(
|
|
85
|
+
const filepath = resolveConvFile(storagePath, agentSlug, filename);
|
|
86
86
|
const raw = fs.readFileSync(filepath, "utf8");
|
|
87
87
|
const { fm, turns } = parseConversation(raw);
|
|
88
88
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
// Conversation storage: append-only markdown at
|
|
2
|
-
//
|
|
1
|
+
// Conversation storage: append-only markdown at ~/.apx/projects/<id>/agents/<slug>/conversations/
|
|
2
|
+
// Filesystem is source of truth. storagePath = ~/.apx/projects/<apx_id>
|
|
3
3
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
7
7
|
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
8
8
|
|
|
9
|
-
export function generateConversationId(
|
|
9
|
+
export function generateConversationId(storagePath, agentSlug) {
|
|
10
10
|
const today = new Date().toISOString().slice(0, 10);
|
|
11
|
-
const dir = path.join(
|
|
11
|
+
const dir = path.join(storagePath, "agents", agentSlug, "conversations");
|
|
12
12
|
let next = 1;
|
|
13
13
|
if (fs.existsSync(dir)) {
|
|
14
14
|
for (const f of fs.readdirSync(dir)) {
|
|
@@ -22,15 +22,15 @@ export function generateConversationId(projectRoot, agentSlug) {
|
|
|
22
22
|
return `${today}-${String(next).padStart(2, "0")}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function conversationPath(
|
|
25
|
+
export function conversationPath(storagePath, agentSlug, idOrFilename) {
|
|
26
26
|
const filename = idOrFilename.endsWith(".md") ? idOrFilename : `${idOrFilename}.md`;
|
|
27
|
-
return path.join(
|
|
27
|
+
return path.join(storagePath, "agents", agentSlug, "conversations", filename);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export function startConversation({
|
|
31
|
-
const dir = path.join(
|
|
30
|
+
export function startConversation({ storagePath, agentSlug, engine, system }) {
|
|
31
|
+
const dir = path.join(storagePath, "agents", agentSlug, "conversations");
|
|
32
32
|
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
-
const id = generateConversationId(
|
|
33
|
+
const id = generateConversationId(storagePath, agentSlug);
|
|
34
34
|
const file = path.join(dir, `${id}.md`);
|
|
35
35
|
const started = nowIso();
|
|
36
36
|
const fm =
|
|
@@ -84,14 +84,14 @@ export function parseConversation(text) {
|
|
|
84
84
|
return { fm, turns };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export function readConversation(
|
|
88
|
-
const p = conversationPath(
|
|
87
|
+
export function readConversation(storagePath, agentSlug, idOrFilename) {
|
|
88
|
+
const p = conversationPath(storagePath, agentSlug, idOrFilename);
|
|
89
89
|
if (!fs.existsSync(p)) return null;
|
|
90
90
|
return { ...parseConversation(fs.readFileSync(p, "utf8")), path: p };
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export function listConversations(
|
|
94
|
-
const dir = path.join(
|
|
93
|
+
export function listConversations(storagePath, agentSlug) {
|
|
94
|
+
const dir = path.join(storagePath, "agents", agentSlug, "conversations");
|
|
95
95
|
if (!fs.existsSync(dir)) return [];
|
|
96
96
|
return fs
|
|
97
97
|
.readdirSync(dir)
|
package/src/daemon/db.js
CHANGED
|
@@ -5,6 +5,12 @@ import path from "node:path";
|
|
|
5
5
|
import { appendMessageToFs } from "../core/messages-store.js";
|
|
6
6
|
import { effectiveConfig } from "./project-config.js";
|
|
7
7
|
import { readAgents } from "../core/parser.js";
|
|
8
|
+
import { getOrCreateApxId } from "../core/scaffold.js";
|
|
9
|
+
import {
|
|
10
|
+
ensureProjectStorage,
|
|
11
|
+
DEFAULT_PROJECT_ID,
|
|
12
|
+
DEFAULT_PROJECT_STORE,
|
|
13
|
+
} from "../core/config.js";
|
|
8
14
|
|
|
9
15
|
export class ProjectManager {
|
|
10
16
|
constructor(globalConfig = {}) {
|
|
@@ -30,19 +36,56 @@ export class ProjectManager {
|
|
|
30
36
|
}
|
|
31
37
|
// Ensure directories exist for projects initialized before they were added.
|
|
32
38
|
fs.mkdirSync(path.join(abs, ".apc", "commands"), { recursive: true });
|
|
33
|
-
|
|
39
|
+
|
|
40
|
+
// Ensure stable APX storage root exists (~/.apx/projects/<apx_id>/).
|
|
41
|
+
const apxId = getOrCreateApxId(abs);
|
|
42
|
+
const storagePath = ensureProjectStorage(apxId);
|
|
34
43
|
|
|
35
44
|
const entry = {
|
|
36
45
|
id: this._nextId++,
|
|
37
46
|
path: abs,
|
|
47
|
+
storagePath,
|
|
48
|
+
apxId,
|
|
38
49
|
config: effectiveConfig(this.globalConfig, abs),
|
|
39
50
|
};
|
|
40
|
-
|
|
51
|
+
// Project runtime messages stay in APX local storage.
|
|
52
|
+
entry.logMessage = (payload) => appendMessageToFs({ projectRoot: storagePath, ...payload });
|
|
41
53
|
this.byId.set(entry.id, entry);
|
|
42
54
|
this.byPath.set(abs, entry);
|
|
43
55
|
return entry;
|
|
44
56
|
}
|
|
45
57
|
|
|
58
|
+
// Register the always-available default project (no local .apc/ required).
|
|
59
|
+
// Called once at daemon startup. Uses id=0.
|
|
60
|
+
// The default project lives entirely at ~/.apx/projects/default/ and mirrors
|
|
61
|
+
// the APC structure so that parser functions can read agents/memory from it.
|
|
62
|
+
registerDefault() {
|
|
63
|
+
if (this.byId.has(0)) return this.byId.get(0);
|
|
64
|
+
// Create a minimal APC-compatible structure inside the storage root so that
|
|
65
|
+
// readAgents() and other parser functions work without a separate project dir.
|
|
66
|
+
const apcDir = path.join(DEFAULT_PROJECT_STORE, ".apc");
|
|
67
|
+
fs.mkdirSync(path.join(apcDir, "agents"), { recursive: true });
|
|
68
|
+
const projectJson = path.join(apcDir, "project.json");
|
|
69
|
+
if (!fs.existsSync(projectJson)) {
|
|
70
|
+
fs.writeFileSync(
|
|
71
|
+
projectJson,
|
|
72
|
+
JSON.stringify({ name: "default", apx_id: DEFAULT_PROJECT_ID, apx: "installed" }, null, 2) + "\n"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
// The default project uses its storagePath as both the APC root and the storage root.
|
|
76
|
+
const entry = {
|
|
77
|
+
id: 0,
|
|
78
|
+
path: DEFAULT_PROJECT_STORE,
|
|
79
|
+
storagePath: DEFAULT_PROJECT_STORE,
|
|
80
|
+
apxId: DEFAULT_PROJECT_ID,
|
|
81
|
+
config: effectiveConfig(this.globalConfig, DEFAULT_PROJECT_STORE),
|
|
82
|
+
};
|
|
83
|
+
entry.logMessage = (payload) => appendMessageToFs({ projectRoot: DEFAULT_PROJECT_STORE, ...payload });
|
|
84
|
+
this.byId.set(0, entry);
|
|
85
|
+
this.byPath.set(DEFAULT_PROJECT_STORE, entry);
|
|
86
|
+
return entry;
|
|
87
|
+
}
|
|
88
|
+
|
|
46
89
|
get(id) {
|
|
47
90
|
return this.byId.get(Number(id)) || null;
|
|
48
91
|
}
|
package/src/daemon/index.js
CHANGED
|
@@ -79,6 +79,9 @@ async function main() {
|
|
|
79
79
|
const projects = new ProjectManager(cfg);
|
|
80
80
|
const registries = new RegistryCache();
|
|
81
81
|
|
|
82
|
+
// Default project (id=0) is always available — no local .apc/ required.
|
|
83
|
+
projects.registerDefault();
|
|
84
|
+
|
|
82
85
|
// Load registered projects from config.
|
|
83
86
|
for (const entry of cfg.projects) {
|
|
84
87
|
try {
|
package/src/daemon/smoke.js
CHANGED
|
@@ -33,14 +33,13 @@ assert(agents.length === 2, `expected 2 agents, got ${agents.length}`);
|
|
|
33
33
|
assert(agents.find((a) => a.slug === "sofia"), "sofia missing");
|
|
34
34
|
assert(agents.find((a) => a.slug === "martin"), "martin missing");
|
|
35
35
|
|
|
36
|
-
// Sessions: scan .
|
|
36
|
+
// Sessions: scan APX local runtime storage.
|
|
37
37
|
const sofiaSessions = (() => {
|
|
38
|
-
const dir = path.join(entry.
|
|
38
|
+
const dir = path.join(entry.storagePath, "agents", "sofia", "sessions");
|
|
39
39
|
if (!fs.existsSync(dir)) return [];
|
|
40
40
|
return fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
41
41
|
})();
|
|
42
42
|
console.log("sofia sessions:", sofiaSessions);
|
|
43
|
-
assert(sofiaSessions.length >= 1, "expected at least one sofia session");
|
|
44
43
|
|
|
45
44
|
const reg = new McpRegistry(entry.path);
|
|
46
45
|
const list = reg.list();
|