@agentprojectcontext/apx 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/package.json +52 -0
- package/skills/apx/SKILL.md +77 -0
- package/src/cli/commands/a2a.js +66 -0
- package/src/cli/commands/agent.js +181 -0
- package/src/cli/commands/chat.js +84 -0
- package/src/cli/commands/command.js +42 -0
- package/src/cli/commands/config.js +56 -0
- package/src/cli/commands/daemon.js +148 -0
- package/src/cli/commands/exec.js +56 -0
- package/src/cli/commands/identity.js +146 -0
- package/src/cli/commands/init.js +23 -0
- package/src/cli/commands/mcp.js +147 -0
- package/src/cli/commands/memory.js +69 -0
- package/src/cli/commands/messages.js +61 -0
- package/src/cli/commands/plugins.js +23 -0
- package/src/cli/commands/project.js +124 -0
- package/src/cli/commands/routine.js +99 -0
- package/src/cli/commands/runtime.js +64 -0
- package/src/cli/commands/session.js +387 -0
- package/src/cli/commands/skills.js +153 -0
- package/src/cli/commands/telegram.js +48 -0
- package/src/cli/http.js +102 -0
- package/src/cli/index.js +481 -0
- package/src/cli/postinstall.js +25 -0
- package/src/core/apc-context-skill.md +150 -0
- package/src/core/apx-skill.md +78 -0
- package/src/core/config.js +129 -0
- package/src/core/identity.js +23 -0
- package/src/core/messages-store.js +421 -0
- package/src/core/parser.js +217 -0
- package/src/core/routines-store.js +144 -0
- package/src/core/scaffold.js +417 -0
- package/src/core/session-store.js +36 -0
- package/src/daemon/apc-runtime-context.js +123 -0
- package/src/daemon/api.js +946 -0
- package/src/daemon/compact.js +140 -0
- package/src/daemon/conversations.js +108 -0
- package/src/daemon/db.js +81 -0
- package/src/daemon/engines/anthropic.js +58 -0
- package/src/daemon/engines/gemini.js +55 -0
- package/src/daemon/engines/index.js +65 -0
- package/src/daemon/engines/mock.js +18 -0
- package/src/daemon/engines/ollama.js +66 -0
- package/src/daemon/engines/openai.js +58 -0
- package/src/daemon/env-detect.js +69 -0
- package/src/daemon/index.js +156 -0
- package/src/daemon/mcp-runner.js +218 -0
- package/src/daemon/mcp-sources.js +114 -0
- package/src/daemon/plugins/index.js +91 -0
- package/src/daemon/plugins/telegram.js +549 -0
- package/src/daemon/project-config.js +98 -0
- package/src/daemon/routines.js +211 -0
- package/src/daemon/runtimes/_spawn.js +44 -0
- package/src/daemon/runtimes/aider.js +32 -0
- package/src/daemon/runtimes/claude-code.js +60 -0
- package/src/daemon/runtimes/codex.js +30 -0
- package/src/daemon/runtimes/index.js +39 -0
- package/src/daemon/runtimes/opencode.js +28 -0
- package/src/daemon/smoke.js +54 -0
- package/src/daemon/super-agent-tools.js +539 -0
- package/src/daemon/super-agent.js +188 -0
- package/src/daemon/thinking.js +45 -0
- package/src/daemon/tool-call-parser.js +116 -0
- package/src/daemon/wakeup.js +92 -0
- package/src/mcp/index.js +220 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Core parsers for APC — pure ESM, no deps.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const SLUG_RE = /^[a-z][a-z0-9_-]*$/;
|
|
6
|
+
const H1_RE = /^#\s+Agents\s*$/i;
|
|
7
|
+
const H2_RE = /^##\s+(\S.*?)\s*$/;
|
|
8
|
+
const FIELD_RE = /^-\s+\*\*([^*]+?)\*\*\s*:\s*(.*)$/;
|
|
9
|
+
const INDENT_CONT_RE = /^\s{2,}\S/;
|
|
10
|
+
const LIST_FIELDS = new Set(["Skills", "Tools"]);
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// AGENTS.md parser (legacy / Codex compat source)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export function parseAgentsMd(text) {
|
|
17
|
+
const stripped = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
18
|
+
const lines = stripped.split(/\r?\n/);
|
|
19
|
+
const agents = [];
|
|
20
|
+
let current = null;
|
|
21
|
+
let pendingField = null;
|
|
22
|
+
let seenH1 = false;
|
|
23
|
+
|
|
24
|
+
for (const raw of lines) {
|
|
25
|
+
const line = raw.replace(/\s+$/, "");
|
|
26
|
+
if (H1_RE.test(line)) { seenH1 = true; continue; }
|
|
27
|
+
const mH2 = line.match(H2_RE);
|
|
28
|
+
if (mH2 && seenH1) {
|
|
29
|
+
const slug = mH2[1].trim();
|
|
30
|
+
if (SLUG_RE.test(slug)) {
|
|
31
|
+
current = { slug, fields: {} };
|
|
32
|
+
agents.push(current);
|
|
33
|
+
pendingField = null;
|
|
34
|
+
} else {
|
|
35
|
+
current = null; pendingField = null;
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!current) continue;
|
|
40
|
+
const mField = line.match(FIELD_RE);
|
|
41
|
+
if (mField) {
|
|
42
|
+
const name = mField[1].trim();
|
|
43
|
+
const value = mField[2].trim();
|
|
44
|
+
current.fields[name] = LIST_FIELDS.has(name)
|
|
45
|
+
? value.split(",").map((s) => s.trim()).filter(Boolean)
|
|
46
|
+
: value;
|
|
47
|
+
pendingField = name;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (pendingField && INDENT_CONT_RE.test(raw)) {
|
|
51
|
+
const existing = current.fields[pendingField];
|
|
52
|
+
if (!Array.isArray(existing)) {
|
|
53
|
+
current.fields[pendingField] = existing ? `${existing} ${raw.trim()}` : raw.trim();
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (line.trim() === "") pendingField = null;
|
|
58
|
+
}
|
|
59
|
+
return agents;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Per-agent file parser (.apc/agents/<slug>.md)
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
export function parseAgentFile(slug, text) {
|
|
67
|
+
// Extract frontmatter
|
|
68
|
+
const fm = parseSessionFrontmatter(text);
|
|
69
|
+
|
|
70
|
+
// Body = everything after the closing ---
|
|
71
|
+
let body = "";
|
|
72
|
+
if (text.startsWith("---\n")) {
|
|
73
|
+
const end = text.indexOf("\n---\n", 4);
|
|
74
|
+
if (end !== -1) body = text.slice(end + 5).trim();
|
|
75
|
+
} else {
|
|
76
|
+
body = text.trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Normalize keys to Title-case to stay consistent with AGENTS.md output
|
|
80
|
+
const fields = {};
|
|
81
|
+
for (const [k, v] of Object.entries(fm)) {
|
|
82
|
+
if (k === "slug") continue;
|
|
83
|
+
const key = k.charAt(0).toUpperCase() + k.slice(1);
|
|
84
|
+
fields[key] = LIST_FIELDS.has(key)
|
|
85
|
+
? String(v).split(",").map((s) => s.trim()).filter(Boolean)
|
|
86
|
+
: v;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { slug, fields, body };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Read all .apc/agents/<slug>.md files. Returns [] if none exist.
|
|
93
|
+
export function readAgentsFromDir(root) {
|
|
94
|
+
const dir = path.join(root, ".apc", "agents");
|
|
95
|
+
if (!fs.existsSync(dir)) return [];
|
|
96
|
+
return fs
|
|
97
|
+
.readdirSync(dir)
|
|
98
|
+
.filter((f) => f.endsWith(".md") && SLUG_RE.test(f.slice(0, -3)))
|
|
99
|
+
.sort()
|
|
100
|
+
.map((f) => {
|
|
101
|
+
const slug = f.slice(0, -3);
|
|
102
|
+
return parseAgentFile(slug, fs.readFileSync(path.join(dir, f), "utf8"));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Vault — ~/.apx/agents/<slug>.md (global templates, no memory)
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
import os from "node:os";
|
|
111
|
+
|
|
112
|
+
export const VAULT_DIR = path.join(os.homedir(), ".apx", "agents");
|
|
113
|
+
|
|
114
|
+
export function readVaultAgents() {
|
|
115
|
+
if (!fs.existsSync(VAULT_DIR)) return [];
|
|
116
|
+
return fs
|
|
117
|
+
.readdirSync(VAULT_DIR)
|
|
118
|
+
.filter((f) => f.endsWith(".md") && SLUG_RE.test(f.slice(0, -3)))
|
|
119
|
+
.sort()
|
|
120
|
+
.map((f) => {
|
|
121
|
+
const slug = f.slice(0, -3);
|
|
122
|
+
const agent = parseAgentFile(slug, fs.readFileSync(path.join(VAULT_DIR, f), "utf8"));
|
|
123
|
+
return { ...agent, source: "vault" };
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Resolve a single agent for a project: local file → vault → null
|
|
128
|
+
export function resolveAgent(root, slug) {
|
|
129
|
+
const localPath = path.join(root, ".apc", "agents", `${slug}.md`);
|
|
130
|
+
if (fs.existsSync(localPath)) {
|
|
131
|
+
const agent = parseAgentFile(slug, fs.readFileSync(localPath, "utf8"));
|
|
132
|
+
return { ...agent, source: "local" };
|
|
133
|
+
}
|
|
134
|
+
const vaultPath = path.join(VAULT_DIR, `${slug}.md`);
|
|
135
|
+
if (fs.existsSync(vaultPath)) {
|
|
136
|
+
const agent = parseAgentFile(slug, fs.readFileSync(vaultPath, "utf8"));
|
|
137
|
+
return { ...agent, source: "vault" };
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Return slugs imported from vault in this project (from project.json)
|
|
143
|
+
export function importedVaultSlugs(root) {
|
|
144
|
+
const p = path.join(root, ".apc", "project.json");
|
|
145
|
+
if (!fs.existsSync(p)) return [];
|
|
146
|
+
try {
|
|
147
|
+
const cfg = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
148
|
+
return cfg.agents?.imported ?? [];
|
|
149
|
+
} catch { return []; }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Primary entry point.
|
|
153
|
+
// Resolution order:
|
|
154
|
+
// 1. .apc/agents/<slug>.md (local — overrides everything)
|
|
155
|
+
// 2. ~/.apx/agents/<slug>.md (vault — for imported slugs)
|
|
156
|
+
// 3. Legacy hand-written AGENTS.md (not auto-generated)
|
|
157
|
+
export function readAgents(root) {
|
|
158
|
+
const fromFiles = readAgentsFromDir(root).map((a) => ({ ...a, source: "local" }));
|
|
159
|
+
const localSlugs = new Set(fromFiles.map((a) => a.slug));
|
|
160
|
+
|
|
161
|
+
// Vault agents imported into this project
|
|
162
|
+
const imported = importedVaultSlugs(root);
|
|
163
|
+
const vaultAgents = imported
|
|
164
|
+
.filter((slug) => !localSlugs.has(slug))
|
|
165
|
+
.map((slug) => {
|
|
166
|
+
const vaultPath = path.join(VAULT_DIR, `${slug}.md`);
|
|
167
|
+
if (!fs.existsSync(vaultPath)) return null;
|
|
168
|
+
const agent = parseAgentFile(slug, fs.readFileSync(vaultPath, "utf8"));
|
|
169
|
+
return { ...agent, source: "vault" };
|
|
170
|
+
})
|
|
171
|
+
.filter(Boolean);
|
|
172
|
+
|
|
173
|
+
const all = [...fromFiles, ...vaultAgents];
|
|
174
|
+
const allSlugs = new Set(all.map((a) => a.slug));
|
|
175
|
+
|
|
176
|
+
const agentsMdPath = path.join(root, "AGENTS.md");
|
|
177
|
+
if (!fs.existsSync(agentsMdPath)) return all;
|
|
178
|
+
|
|
179
|
+
const mdText = fs.readFileSync(agentsMdPath, "utf8");
|
|
180
|
+
if (mdText.includes("Auto-generated from .apc/agents/")) return all;
|
|
181
|
+
|
|
182
|
+
// Legacy hand-written AGENTS.md
|
|
183
|
+
const legacy = parseAgentsMd(mdText)
|
|
184
|
+
.filter((a) => !allSlugs.has(a.slug))
|
|
185
|
+
.map((a) => ({ ...a, source: "legacy" }));
|
|
186
|
+
return [...all, ...legacy];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Project root detection
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
export function findApfRoot(start = process.cwd()) {
|
|
194
|
+
let cur = path.resolve(start);
|
|
195
|
+
while (true) {
|
|
196
|
+
if (fs.existsSync(path.join(cur, ".apc", "project.json"))) return cur;
|
|
197
|
+
const parent = path.dirname(cur);
|
|
198
|
+
if (parent === cur) return null;
|
|
199
|
+
cur = parent;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Session / conversation frontmatter
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
export function parseSessionFrontmatter(text) {
|
|
208
|
+
if (!text.startsWith("---\n")) return {};
|
|
209
|
+
const end = text.indexOf("\n---", 4);
|
|
210
|
+
if (end === -1) return {};
|
|
211
|
+
const out = {};
|
|
212
|
+
for (const line of text.slice(4, end).split("\n")) {
|
|
213
|
+
const m = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
214
|
+
if (m) out[m[1]] = m[2].trim();
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// File-based routines store: read/write .apc/routines.json.
|
|
2
|
+
// Replaces the SQLite `routines` table for project-scoped scheduled tasks.
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
7
|
+
const isoToMs = (iso) => (iso ? Date.parse(iso) : 0);
|
|
8
|
+
|
|
9
|
+
function routinesPath(projectPath) {
|
|
10
|
+
return path.join(projectPath, ".apc", "routines.json");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readFile(projectPath) {
|
|
14
|
+
const p = routinesPath(projectPath);
|
|
15
|
+
if (!fs.existsSync(p)) return [];
|
|
16
|
+
try {
|
|
17
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
18
|
+
return Array.isArray(raw.routines) ? raw.routines : [];
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeFile(projectPath, routines) {
|
|
25
|
+
const p = routinesPath(projectPath);
|
|
26
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
27
|
+
fs.writeFileSync(p, JSON.stringify({ routines }, null, 2) + "\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --------------------- schedule parsing -------------------------------------
|
|
31
|
+
|
|
32
|
+
export function parseSchedule(s, baseMs = Date.now()) {
|
|
33
|
+
if (!s || typeof s !== "string") return { kind: "invalid" };
|
|
34
|
+
if (s.startsWith("every:")) {
|
|
35
|
+
const spec = s.slice(6).trim();
|
|
36
|
+
const m = spec.match(/^(\d+)(s|m|h|d)$/);
|
|
37
|
+
if (!m) return { kind: "invalid" };
|
|
38
|
+
const n = parseInt(m[1], 10);
|
|
39
|
+
const mult = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
|
|
40
|
+
return { kind: "every", intervalMs: n * mult };
|
|
41
|
+
}
|
|
42
|
+
if (s.startsWith("once:")) {
|
|
43
|
+
const ts = s.slice(5).trim();
|
|
44
|
+
const ms = Date.parse(ts);
|
|
45
|
+
if (isNaN(ms)) return { kind: "invalid" };
|
|
46
|
+
return { kind: "once", atMs: ms };
|
|
47
|
+
}
|
|
48
|
+
return { kind: "invalid" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function computeNextRun(routine, baseMs = Date.now()) {
|
|
52
|
+
const sched = parseSchedule(routine.schedule, baseMs);
|
|
53
|
+
if (sched.kind === "invalid") return null;
|
|
54
|
+
if (sched.kind === "once") {
|
|
55
|
+
return sched.atMs > baseMs
|
|
56
|
+
? new Date(sched.atMs).toISOString().replace(/\.\d{3}Z$/, "Z")
|
|
57
|
+
: null;
|
|
58
|
+
}
|
|
59
|
+
if (sched.kind === "every") {
|
|
60
|
+
const last = isoToMs(routine.last_run_at);
|
|
61
|
+
const next = (last || baseMs) + sched.intervalMs;
|
|
62
|
+
const target = next < baseMs ? baseMs + 100 : next;
|
|
63
|
+
return new Date(target).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --------------------- CRUD -------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export function listRoutines(projectPath) {
|
|
71
|
+
return readFile(projectPath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getRoutine(projectPath, name) {
|
|
75
|
+
return readFile(projectPath).find((r) => r.name === name) || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function upsertRoutine(projectPath, { name, kind, schedule, spec, enabled = true }) {
|
|
79
|
+
if (!name || !kind || !schedule) throw new Error("routine requires name, kind, schedule");
|
|
80
|
+
const now = nowIso();
|
|
81
|
+
const routines = readFile(projectPath);
|
|
82
|
+
const idx = routines.findIndex((r) => r.name === name);
|
|
83
|
+
const prev = idx >= 0 ? routines[idx] : null;
|
|
84
|
+
const next = computeNextRun({ schedule, last_run_at: null });
|
|
85
|
+
const entry = {
|
|
86
|
+
name,
|
|
87
|
+
kind,
|
|
88
|
+
schedule,
|
|
89
|
+
spec: spec || {},
|
|
90
|
+
enabled: enabled !== false,
|
|
91
|
+
last_run_at: prev?.last_run_at ?? null,
|
|
92
|
+
last_status: prev?.last_status ?? null,
|
|
93
|
+
last_error: prev?.last_error ?? null,
|
|
94
|
+
next_run_at: next,
|
|
95
|
+
created_at: prev?.created_at ?? now,
|
|
96
|
+
updated_at: now,
|
|
97
|
+
};
|
|
98
|
+
if (idx >= 0) {
|
|
99
|
+
routines[idx] = entry;
|
|
100
|
+
} else {
|
|
101
|
+
routines.push(entry);
|
|
102
|
+
}
|
|
103
|
+
writeFile(projectPath, routines);
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function deleteRoutine(projectPath, name) {
|
|
108
|
+
const routines = readFile(projectPath);
|
|
109
|
+
const idx = routines.findIndex((r) => r.name === name);
|
|
110
|
+
if (idx === -1) return false;
|
|
111
|
+
routines.splice(idx, 1);
|
|
112
|
+
writeFile(projectPath, routines);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function setEnabled(projectPath, name, enabled) {
|
|
117
|
+
const routines = readFile(projectPath);
|
|
118
|
+
const r = routines.find((x) => x.name === name);
|
|
119
|
+
if (!r) return false;
|
|
120
|
+
r.enabled = !!enabled;
|
|
121
|
+
r.updated_at = nowIso();
|
|
122
|
+
writeFile(projectPath, routines);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function updateRunState(projectPath, name, { last_run_at, last_status, last_error, next_run_at, disable = false }) {
|
|
127
|
+
const routines = readFile(projectPath);
|
|
128
|
+
const r = routines.find((x) => x.name === name);
|
|
129
|
+
if (!r) return false;
|
|
130
|
+
r.last_run_at = last_run_at;
|
|
131
|
+
r.last_status = last_status;
|
|
132
|
+
r.last_error = last_error || null;
|
|
133
|
+
r.next_run_at = next_run_at;
|
|
134
|
+
r.updated_at = last_run_at || nowIso();
|
|
135
|
+
if (disable) r.enabled = false;
|
|
136
|
+
writeFile(projectPath, routines);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function getDueRoutines(projectPath, nowStr) {
|
|
141
|
+
return readFile(projectPath).filter(
|
|
142
|
+
(r) => r.enabled && (!r.next_run_at || r.next_run_at <= nowStr)
|
|
143
|
+
);
|
|
144
|
+
}
|