@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/src/daemon/api.js CHANGED
@@ -1,4 +1,4 @@
1
- // Express REST API for APX. See docs/APX-DAEMON.md §4.
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.path, ".apc", "agents", req.params.slug, "sessions");
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.path, ".apc", "agents", req.params.slug, "sessions");
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.path, ".apc", "agents");
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.path, {
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.path, q, Math.min(parseInt(limit, 10) || 50, 500)));
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({ projectRoot: p.path, agentSlug: agent.slug, engine: modelId, system });
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.path, agent.slug, conversation_id);
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({ projectRoot: p.path, agentSlug: agent.slug, engine: modelId, system });
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.path, req.params.slug));
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.path, req.params.slug, req.params.id);
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
- projectRoot: p.path,
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.path, { agent_slug: req.params.slug });
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,
@@ -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(projectRoot, agentSlug, filename) {
56
- const dir = path.join(projectRoot, ".apc", "agents", agentSlug, "conversations");
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
- projectRoot,
79
+ storagePath,
80
80
  agentSlug,
81
81
  filename,
82
82
  modelId,
83
83
  config,
84
84
  }) {
85
- const filepath = resolveConvFile(projectRoot, agentSlug, filename);
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 .apc/agents/<slug>/conversations/
2
- // with SQLite mirror for fast querying. Filesystem is source of truth.
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(projectRoot, agentSlug) {
9
+ export function generateConversationId(storagePath, agentSlug) {
10
10
  const today = new Date().toISOString().slice(0, 10);
11
- const dir = path.join(projectRoot, ".apc", "agents", agentSlug, "conversations");
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(projectRoot, agentSlug, idOrFilename) {
25
+ export function conversationPath(storagePath, agentSlug, idOrFilename) {
26
26
  const filename = idOrFilename.endsWith(".md") ? idOrFilename : `${idOrFilename}.md`;
27
- return path.join(projectRoot, ".apc", "agents", agentSlug, "conversations", filename);
27
+ return path.join(storagePath, "agents", agentSlug, "conversations", filename);
28
28
  }
29
29
 
30
- export function startConversation({ projectRoot, agentSlug, engine, system }) {
31
- const dir = path.join(projectRoot, ".apc", "agents", agentSlug, "conversations");
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(projectRoot, agentSlug);
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(projectRoot, agentSlug, idOrFilename) {
88
- const p = conversationPath(projectRoot, agentSlug, idOrFilename);
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(projectRoot, agentSlug) {
94
- const dir = path.join(projectRoot, ".apc", "agents", agentSlug, "conversations");
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
- fs.mkdirSync(path.join(abs, ".apc", "messages"), { recursive: true });
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
- entry.logMessage = (payload) => appendMessageToFs({ projectRoot: abs, ...payload });
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
  }
@@ -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 {
@@ -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 .apc/agents/sofia/sessions/
36
+ // Sessions: scan APX local runtime storage.
37
37
  const sofiaSessions = (() => {
38
- const dir = path.join(entry.path, ".apc", "agents", "sofia", "sessions");
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();