@agentprojectcontext/apx 1.0.3 → 1.2.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/setup.js +336 -0
- package/src/cli/commands/update.js +75 -0
- package/src/cli/index.js +30 -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/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();
|