@agentprojectcontext/apx 1.14.1 → 1.15.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/package.json +2 -1
- package/skills/apc-context/SKILL.md +68 -18
- package/skills/apx/SKILL.md +89 -33
- package/src/cli/commands/sys.js +249 -21
- package/src/cli/commands/telegram.js +8 -2
- package/src/cli/http.js +24 -7
- package/src/cli/index.js +10 -3
- package/src/cli/postinstall.js +54 -4
- package/src/cli/terminal-chat/renderer.js +60 -3
- package/src/core/logging.js +37 -0
- package/src/core/scaffold.js +70 -56
- package/src/daemon/api.js +29 -2
- package/src/daemon/engines/anthropic.js +2 -1
- package/src/daemon/engines/gemini.js +2 -1
- package/src/daemon/engines/index.js +3 -3
- package/src/daemon/engines/ollama.js +2 -1
- package/src/daemon/engines/openai.js +2 -1
- package/src/daemon/plugins/telegram.js +20 -1
- package/src/daemon/skills-loader.js +31 -66
- package/src/daemon/smoke.js +9 -1
- package/src/daemon/super-agent-tools/index.js +2 -0
- package/src/daemon/super-agent-tools/tools/ask-questions.js +28 -0
- package/src/daemon/super-agent.js +97 -9
- package/src/core/apc-context-skill.md +0 -105
- package/src/core/apx-skill.md +0 -135
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { APX_HOME } from "./config.js";
|
|
4
|
+
|
|
5
|
+
export const LOG_DIR = path.join(APX_HOME, "logs");
|
|
6
|
+
export const ERROR_TRACE_PATH = path.join(LOG_DIR, "errors.jsonl");
|
|
7
|
+
|
|
8
|
+
const SECRET_KEY_RE = /(token|secret|password|api[_-]?key|authorization|bot[_-]?token)/i;
|
|
9
|
+
|
|
10
|
+
function redact(value, seen = new WeakSet()) {
|
|
11
|
+
if (value === null || value === undefined) return value;
|
|
12
|
+
if (typeof value !== "object") return value;
|
|
13
|
+
if (seen.has(value)) return "[circular]";
|
|
14
|
+
seen.add(value);
|
|
15
|
+
|
|
16
|
+
if (Array.isArray(value)) return value.map((item) => redact(item, seen));
|
|
17
|
+
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const [key, val] of Object.entries(value)) {
|
|
20
|
+
out[key] = SECRET_KEY_RE.test(key) ? "[redacted]" : redact(val, seen);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function appendErrorTrace(record) {
|
|
26
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
27
|
+
const entry = {
|
|
28
|
+
ts: new Date().toISOString(),
|
|
29
|
+
...redact(record),
|
|
30
|
+
};
|
|
31
|
+
fs.appendFileSync(ERROR_TRACE_PATH, JSON.stringify(entry) + "\n", "utf8");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function previewText(text, max = 500) {
|
|
35
|
+
const clean = String(text || "").replace(/\s+/g, " ").trim();
|
|
36
|
+
return clean.length > max ? clean.slice(0, max - 1) + "…" : clean;
|
|
37
|
+
}
|
package/src/core/scaffold.js
CHANGED
|
@@ -6,9 +6,43 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { readAgents, readAgentsFromDir, VAULT_DIR } from "./parser.js";
|
|
7
7
|
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
10
|
+
const BUNDLED_SKILLS_DIR = path.join(PACKAGE_ROOT, "skills");
|
|
11
|
+
const RUNTIME_SKILLS_DIR = path.join(__dirname, "runtime-skills");
|
|
9
12
|
|
|
10
13
|
export const SPEC_VERSION = "0.1.0";
|
|
11
14
|
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Bundled skills — single source of truth lives at <packageRoot>/skills/<slug>/SKILL.md
|
|
17
|
+
// with proper frontmatter. The `apc-context` copy is refreshed on every
|
|
18
|
+
// install/update from the canonical APC repo (see src/cli/postinstall.js).
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
function readBundledSkill(slug) {
|
|
22
|
+
const file = path.join(BUNDLED_SKILLS_DIR, slug, "SKILL.md");
|
|
23
|
+
if (!fs.existsSync(file)) return null;
|
|
24
|
+
return fs.readFileSync(file, "utf8");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Split frontmatter and body from a SKILL.md. Used by IDE targets that need
|
|
28
|
+
// to re-wrap the body in their own rule-file frontmatter.
|
|
29
|
+
function splitFrontmatter(raw) {
|
|
30
|
+
if (!raw.startsWith("---")) return { fm: "", body: raw };
|
|
31
|
+
const end = raw.indexOf("\n---", 3);
|
|
32
|
+
if (end < 0) return { fm: "", body: raw };
|
|
33
|
+
const fm = raw.slice(0, end + 4);
|
|
34
|
+
const body = raw.slice(end + 4).replace(/^\n/, "");
|
|
35
|
+
return { fm, body };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Pull description from frontmatter so cursor/.mdc rule files can advertise
|
|
39
|
+
// the same activation trigger.
|
|
40
|
+
function readDescription(raw) {
|
|
41
|
+
const { fm } = splitFrontmatter(raw);
|
|
42
|
+
const m = fm.match(/^description:\s*"?(.*?)"?\s*$/m);
|
|
43
|
+
return m ? m[1] : "";
|
|
44
|
+
}
|
|
45
|
+
|
|
12
46
|
// ---------------------------------------------------------------------------
|
|
13
47
|
// IDE skill targets — written during `apx init` and `apx skills add`
|
|
14
48
|
// ---------------------------------------------------------------------------
|
|
@@ -21,7 +55,8 @@ export const IDE_TARGETS = [
|
|
|
21
55
|
label: "Claude Code",
|
|
22
56
|
ideDir: ".claude",
|
|
23
57
|
file: ".claude/skills/apx/SKILL.md",
|
|
24
|
-
|
|
58
|
+
// Claude Code consumes SKILL.md with its native frontmatter as-is.
|
|
59
|
+
render: (raw) => raw,
|
|
25
60
|
append: false,
|
|
26
61
|
},
|
|
27
62
|
{
|
|
@@ -29,8 +64,11 @@ export const IDE_TARGETS = [
|
|
|
29
64
|
label: "Cursor",
|
|
30
65
|
ideDir: ".cursor",
|
|
31
66
|
file: ".cursor/rules/apx.mdc",
|
|
32
|
-
render: (
|
|
33
|
-
|
|
67
|
+
render: (raw) => {
|
|
68
|
+
const { body } = splitFrontmatter(raw);
|
|
69
|
+
const desc = readDescription(raw);
|
|
70
|
+
return `---\ndescription: ${desc}\n---\n\n${body}`;
|
|
71
|
+
},
|
|
34
72
|
append: false,
|
|
35
73
|
},
|
|
36
74
|
{
|
|
@@ -38,8 +76,11 @@ export const IDE_TARGETS = [
|
|
|
38
76
|
label: "Windsurf",
|
|
39
77
|
ideDir: ".windsurf",
|
|
40
78
|
file: ".windsurf/rules/apx.md",
|
|
41
|
-
render: (
|
|
42
|
-
|
|
79
|
+
render: (raw) => {
|
|
80
|
+
const { body } = splitFrontmatter(raw);
|
|
81
|
+
const desc = readDescription(raw);
|
|
82
|
+
return `---\ntrigger: model_decision\ndescription: ${desc}\n---\n\n${body}`;
|
|
83
|
+
},
|
|
43
84
|
append: false,
|
|
44
85
|
},
|
|
45
86
|
{
|
|
@@ -47,7 +88,10 @@ export const IDE_TARGETS = [
|
|
|
47
88
|
label: "GitHub Copilot",
|
|
48
89
|
ideDir: ".github",
|
|
49
90
|
file: ".github/copilot-instructions.md",
|
|
50
|
-
render: (
|
|
91
|
+
render: (raw) => {
|
|
92
|
+
const { body } = splitFrontmatter(raw);
|
|
93
|
+
return `\n<!-- apx-skill -->\n${body}\n<!-- /apx-skill -->\n`;
|
|
94
|
+
},
|
|
51
95
|
append: true,
|
|
52
96
|
guard: "<!-- apx-skill -->",
|
|
53
97
|
},
|
|
@@ -56,13 +100,16 @@ export const IDE_TARGETS = [
|
|
|
56
100
|
label: "Trae",
|
|
57
101
|
ideDir: ".trae",
|
|
58
102
|
file: ".trae/rules/project_rules.md",
|
|
59
|
-
render: (
|
|
103
|
+
render: (raw) => {
|
|
104
|
+
const { body } = splitFrontmatter(raw);
|
|
105
|
+
return `\n<!-- apx-skill -->\n${body}\n<!-- /apx-skill -->\n`;
|
|
106
|
+
},
|
|
60
107
|
append: true,
|
|
61
108
|
guard: "<!-- apx-skill -->",
|
|
62
109
|
},
|
|
63
110
|
];
|
|
64
111
|
|
|
65
|
-
// Global targets (absolute paths, use ~/<dir>/skills
|
|
112
|
+
// Global targets (absolute paths, use ~/<dir>/skills/<slug>/SKILL.md format).
|
|
66
113
|
// These dirs are read by Claude Code, Cursor (compat), and tools adopting the skills.sh spec.
|
|
67
114
|
const GLOBAL_SKILL_DIRS = [
|
|
68
115
|
path.join(os.homedir(), ".claude", "skills"), // Claude Code + Cursor legacy compat
|
|
@@ -71,52 +118,23 @@ const GLOBAL_SKILL_DIRS = [
|
|
|
71
118
|
path.join(os.homedir(), ".agents", "skills"), // Antigravity/other skills.sh adopters
|
|
72
119
|
];
|
|
73
120
|
|
|
74
|
-
function buildApcContextSkillMd(content) {
|
|
75
|
-
const frontmatter = [
|
|
76
|
-
"---",
|
|
77
|
-
"name: apc-context",
|
|
78
|
-
"description: \"ALWAYS activate when the project has a .apc/ directory or AGENTS.md file. Do not wait to be asked. Read .apc/ before making any assumption about agents, memory, or project structure. Activate on: .apc/, AGENTS.md, 'which agents', 'list agents', 'agent context', 'who are the agents', any question about agents or memory in this project. IMPORTANT: if .apc/migrate.md exists, open the conversation with a migration offer before answering anything else. If the user declines, delete .apc/migrate.md immediately so it is not shown again.\"",
|
|
79
|
-
"homepage: https://github.com/agentprojectcontext/agentprojectcontext",
|
|
80
|
-
"---",
|
|
81
|
-
"",
|
|
82
|
-
].join("\n");
|
|
83
|
-
return frontmatter + content;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function buildSkillMd(content) {
|
|
87
|
-
const frontmatter = [
|
|
88
|
-
"---",
|
|
89
|
-
"name: apx",
|
|
90
|
-
"description: \"APX CLI skill. Activate when: user asks to run or coordinate agents, use MCP tools from .apc/mcps.json, install agents from a team workspace, or explicitly mentions apx commands. Do NOT activate just because .apc/ exists — that is handled by the apc-context skill. Activate on: 'apx run', 'apx exec', 'run an agent', 'coordinate agents', 'MCP not working', 'install agent', 'team agents', 'apx memory', 'daemon'.\"",
|
|
91
|
-
"homepage: https://github.com/agentprojectcontext/apx",
|
|
92
|
-
"---",
|
|
93
|
-
"",
|
|
94
|
-
].join("\n");
|
|
95
|
-
return frontmatter + content;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
121
|
function readRuntimeSkillFiles() {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return fs.readdirSync(skillsDir)
|
|
122
|
+
if (!fs.existsSync(RUNTIME_SKILLS_DIR)) return [];
|
|
123
|
+
return fs.readdirSync(RUNTIME_SKILLS_DIR)
|
|
103
124
|
.filter((name) => name.endsWith(".md"))
|
|
104
125
|
.sort()
|
|
105
126
|
.map((name) => ({
|
|
106
127
|
slug: path.basename(name, ".md"),
|
|
107
|
-
md: fs.readFileSync(path.join(
|
|
128
|
+
md: fs.readFileSync(path.join(RUNTIME_SKILLS_DIR, name), "utf8").trim(),
|
|
108
129
|
}));
|
|
109
130
|
}
|
|
110
131
|
|
|
111
132
|
// Install APX + APC context skills into IDE rule files. Returns an array of result objects.
|
|
112
133
|
// targetIds: array of target ids to install; null = all project targets.
|
|
113
134
|
export function installIdeSkills(root, targetIds = null) {
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
117
|
-
|
|
118
|
-
const apxContent = fs.readFileSync(apxSrc, "utf8").trim();
|
|
119
|
-
const apcContent = fs.existsSync(apcSrc) ? fs.readFileSync(apcSrc, "utf8").trim() : null;
|
|
135
|
+
const apxRaw = readBundledSkill("apx");
|
|
136
|
+
const apcRaw = readBundledSkill("apc-context");
|
|
137
|
+
if (!apxRaw) return [];
|
|
120
138
|
|
|
121
139
|
const targets = targetIds
|
|
122
140
|
? IDE_TARGETS.filter((t) => targetIds.includes(t.id))
|
|
@@ -129,10 +147,9 @@ export function installIdeSkills(root, targetIds = null) {
|
|
|
129
147
|
continue;
|
|
130
148
|
}
|
|
131
149
|
|
|
132
|
-
// Install APX skill
|
|
133
150
|
const dest = path.join(root, t.file);
|
|
134
151
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
135
|
-
const rendered = t.render(
|
|
152
|
+
const rendered = t.render(apxRaw);
|
|
136
153
|
if (t.append) {
|
|
137
154
|
const existing = fs.existsSync(dest) ? fs.readFileSync(dest, "utf8") : "";
|
|
138
155
|
if (t.guard && existing.includes(t.guard)) {
|
|
@@ -147,31 +164,28 @@ export function installIdeSkills(root, targetIds = null) {
|
|
|
147
164
|
results.push({ ...t, status: existed ? "updated" : "created" });
|
|
148
165
|
}
|
|
149
166
|
|
|
150
|
-
// Install APC context skill alongside
|
|
151
|
-
if (
|
|
167
|
+
// Install APC context skill alongside Claude Code (dir-style skills dir).
|
|
168
|
+
if (apcRaw && t.id === "claude-code") {
|
|
152
169
|
const apcDest = path.join(root, ".claude", "skills", "apc-context", "SKILL.md");
|
|
153
170
|
fs.mkdirSync(path.dirname(apcDest), { recursive: true });
|
|
154
171
|
const existed = fs.existsSync(apcDest);
|
|
155
|
-
fs.writeFileSync(apcDest,
|
|
172
|
+
fs.writeFileSync(apcDest, apcRaw, "utf8");
|
|
156
173
|
results.push({ ...t, id: "claude-code/apc-context", label: "Claude Code (apc-context)", file: apcDest, status: existed ? "updated" : "created" });
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
176
|
return results;
|
|
160
177
|
}
|
|
161
178
|
|
|
162
|
-
// Install
|
|
179
|
+
// Install bundled APX/APC skills + runtime docs to global ~/.../skills/ dirs.
|
|
163
180
|
// Returns an array of result objects with { dir, skill, status }.
|
|
164
181
|
export function installGlobalSkills() {
|
|
165
182
|
const results = [];
|
|
166
183
|
|
|
167
|
-
const apxSrc = path.join(__dirname, "apx-skill.md");
|
|
168
|
-
const apcSrc = path.join(__dirname, "apc-context-skill.md");
|
|
169
|
-
|
|
170
184
|
const skills = [];
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
185
|
+
const apxRaw = readBundledSkill("apx");
|
|
186
|
+
const apcRaw = readBundledSkill("apc-context");
|
|
187
|
+
if (apxRaw) skills.push({ slug: "apx", md: apxRaw });
|
|
188
|
+
if (apcRaw) skills.push({ slug: "apc-context", md: apcRaw });
|
|
175
189
|
skills.push(...readRuntimeSkillFiles());
|
|
176
190
|
|
|
177
191
|
for (const base of GLOBAL_SKILL_DIRS) {
|
package/src/daemon/api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
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
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { execFile } from "node:child_process";
|
|
5
6
|
import express from "express";
|
|
6
7
|
import { buildBrowserRouter } from "./tools/browser.js";
|
|
@@ -49,6 +50,7 @@ import { readAgents } from "../core/parser.js";
|
|
|
49
50
|
import { parseSessionFrontmatter } from "../core/parser.js";
|
|
50
51
|
import { writeAgentFile, ensureAgentDir, regenerateAgentsMd } from "../core/scaffold.js";
|
|
51
52
|
import { buildAgentSystem } from "../core/agent-system.js";
|
|
53
|
+
import { appendErrorTrace, previewText } from "../core/logging.js";
|
|
52
54
|
import {
|
|
53
55
|
createArtifact,
|
|
54
56
|
listArtifacts,
|
|
@@ -58,11 +60,34 @@ import {
|
|
|
58
60
|
|
|
59
61
|
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
60
62
|
|
|
63
|
+
function appendSuperAgentErrorTrace(req, error, details = {}) {
|
|
64
|
+
appendErrorTrace({
|
|
65
|
+
trace_id: req.apxTraceId,
|
|
66
|
+
surface: "daemon_api",
|
|
67
|
+
route: `${req.method} ${req.route?.path || req.path}`,
|
|
68
|
+
project_id: req.params?.pid || null,
|
|
69
|
+
channel: /Channel:\s*([^\n]+)/i.exec(details.contextNote || "")?.[1]?.trim() || null,
|
|
70
|
+
model: details.model || null,
|
|
71
|
+
stream: !!details.stream,
|
|
72
|
+
prompt_preview: previewText(details.prompt),
|
|
73
|
+
previous_messages: Array.isArray(details.previousMessages) ? details.previousMessages.length : 0,
|
|
74
|
+
error: {
|
|
75
|
+
message: error?.message || String(error),
|
|
76
|
+
stack: error?.stack || null,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
61
81
|
export function buildApi({ projects, registries, plugins, scheduler, version, startedAt, addProjectGlobally, config }) {
|
|
62
82
|
const telegram = plugins?.get("telegram");
|
|
63
83
|
|
|
64
84
|
const app = express();
|
|
65
85
|
app.use(express.json({ limit: "2mb" }));
|
|
86
|
+
app.use((req, res, next) => {
|
|
87
|
+
req.apxTraceId = req.get("x-apx-trace-id") || randomUUID();
|
|
88
|
+
res.setHeader("x-apx-trace-id", req.apxTraceId);
|
|
89
|
+
next();
|
|
90
|
+
});
|
|
66
91
|
|
|
67
92
|
// ---- Tool routers (fetch / browser / search / glob / grep / registry) ----
|
|
68
93
|
// fetch = native HTTP, no Chromium → fast, cheap, default for REST/HTML
|
|
@@ -636,7 +661,8 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
636
661
|
});
|
|
637
662
|
res.end();
|
|
638
663
|
} catch (e) {
|
|
639
|
-
|
|
664
|
+
appendSuperAgentErrorTrace(req, e, { prompt, contextNote, previousMessages, model, stream: true });
|
|
665
|
+
send({ type: "error", trace_id: req.apxTraceId, error: `${e.message} (trace: ${req.apxTraceId})` });
|
|
640
666
|
res.end();
|
|
641
667
|
}
|
|
642
668
|
});
|
|
@@ -665,7 +691,8 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
665
691
|
trace: saResult.trace,
|
|
666
692
|
});
|
|
667
693
|
} catch (e) {
|
|
668
|
-
|
|
694
|
+
appendSuperAgentErrorTrace(req, e, { prompt, contextNote, previousMessages, model, stream: false });
|
|
695
|
+
res.status(500).json({ error: e.message, trace_id: req.apxTraceId });
|
|
669
696
|
}
|
|
670
697
|
});
|
|
671
698
|
|
|
@@ -11,7 +11,7 @@ function getKey(config) {
|
|
|
11
11
|
export default {
|
|
12
12
|
id: "anthropic",
|
|
13
13
|
|
|
14
|
-
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice }) {
|
|
14
|
+
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice, signal }) {
|
|
15
15
|
const key = getKey(config);
|
|
16
16
|
if (!key) throw new Error("anthropic: no api_key (set ANTHROPIC_API_KEY or engines.anthropic.api_key)");
|
|
17
17
|
if (!model) throw new Error("anthropic: model required");
|
|
@@ -47,6 +47,7 @@ export default {
|
|
|
47
47
|
"anthropic-version": API_VERSION,
|
|
48
48
|
},
|
|
49
49
|
body: JSON.stringify(body),
|
|
50
|
+
signal,
|
|
50
51
|
});
|
|
51
52
|
const json = await res.json();
|
|
52
53
|
if (!res.ok) {
|
|
@@ -10,7 +10,7 @@ function getKey(config) {
|
|
|
10
10
|
export default {
|
|
11
11
|
id: "gemini",
|
|
12
12
|
|
|
13
|
-
async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, config = {} }) {
|
|
13
|
+
async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, config = {}, signal }) {
|
|
14
14
|
const key = getKey(config);
|
|
15
15
|
if (!key) throw new Error("gemini: no api_key (set GEMINI_API_KEY or engines.gemini.api_key)");
|
|
16
16
|
if (!model) throw new Error("gemini: model required");
|
|
@@ -33,6 +33,7 @@ export default {
|
|
|
33
33
|
method: "POST",
|
|
34
34
|
headers: { "content-type": "application/json" },
|
|
35
35
|
body: JSON.stringify(body),
|
|
36
|
+
signal,
|
|
36
37
|
});
|
|
37
38
|
const json = await res.json();
|
|
38
39
|
if (!res.ok) {
|
|
@@ -46,11 +46,10 @@ export function getAdapter(provider) {
|
|
|
46
46
|
return a;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
export async function callEngine({ modelId, system, messages, config, temperature, maxTokens, tools, toolChoice }) {
|
|
49
|
+
export async function callEngine({ modelId, system, messages, config, temperature, maxTokens, tools, toolChoice, signal }) {
|
|
50
50
|
const { provider, model } = resolveProvider(modelId);
|
|
51
51
|
const adapter = getAdapter(provider);
|
|
52
|
-
const providerCfg =
|
|
53
|
-
(config && config.engines && config.engines[provider]) || {};
|
|
52
|
+
const providerCfg = (config && config.engines && config.engines[provider]) || {};
|
|
54
53
|
return adapter.chat({
|
|
55
54
|
system,
|
|
56
55
|
messages,
|
|
@@ -60,6 +59,7 @@ export async function callEngine({ modelId, system, messages, config, temperatur
|
|
|
60
59
|
tools,
|
|
61
60
|
toolChoice,
|
|
62
61
|
config: providerCfg,
|
|
62
|
+
signal,
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -8,7 +8,7 @@ function baseUrl(config) {
|
|
|
8
8
|
export default {
|
|
9
9
|
id: "ollama",
|
|
10
10
|
|
|
11
|
-
async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, tools, config = {} }) {
|
|
11
|
+
async chat({ system, messages, model, temperature = 0.7, maxTokens = 1024, tools, config = {}, signal }) {
|
|
12
12
|
if (!model) throw new Error("ollama: model required");
|
|
13
13
|
|
|
14
14
|
// The caller can pass `messages` as either:
|
|
@@ -45,6 +45,7 @@ export default {
|
|
|
45
45
|
method: "POST",
|
|
46
46
|
headers: { "content-type": "application/json" },
|
|
47
47
|
body: JSON.stringify(body),
|
|
48
|
+
signal,
|
|
48
49
|
});
|
|
49
50
|
if (!res.ok) {
|
|
50
51
|
const text = await res.text();
|
|
@@ -10,7 +10,7 @@ function getKey(config) {
|
|
|
10
10
|
export default {
|
|
11
11
|
id: "openai",
|
|
12
12
|
|
|
13
|
-
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice }) {
|
|
13
|
+
async chat({ system, messages, model, temperature = 1.0, maxTokens = 1024, config = {}, tools, toolChoice, signal }) {
|
|
14
14
|
const key = getKey(config);
|
|
15
15
|
if (!key) throw new Error("openai: no api_key (set OPENAI_API_KEY or engines.openai.api_key)");
|
|
16
16
|
if (!model) throw new Error("openai: model required");
|
|
@@ -52,6 +52,7 @@ export default {
|
|
|
52
52
|
authorization: `Bearer ${key}`,
|
|
53
53
|
},
|
|
54
54
|
body: JSON.stringify(body),
|
|
55
|
+
signal,
|
|
55
56
|
});
|
|
56
57
|
const json = await res.json();
|
|
57
58
|
if (!res.ok) {
|
|
@@ -290,6 +290,7 @@ class ChannelPoller {
|
|
|
290
290
|
this.polling = false;
|
|
291
291
|
this.lastError = null;
|
|
292
292
|
this.lastUpdateAt = null;
|
|
293
|
+
this.activeRequests = new Map(); // chat_id -> AbortController
|
|
293
294
|
}
|
|
294
295
|
|
|
295
296
|
resolveProject() {
|
|
@@ -390,7 +391,19 @@ class ChannelPoller {
|
|
|
390
391
|
? "@" + msg.from.username
|
|
391
392
|
: `${msg.from?.first_name || ""} ${msg.from?.last_name || ""}`.trim() || "unknown";
|
|
392
393
|
const chat_id = msg.chat?.id;
|
|
393
|
-
|
|
394
|
+
|
|
395
|
+
// Default Interrupt: abort any running request for this chat_id
|
|
396
|
+
if (chat_id) {
|
|
397
|
+
const prev = this.activeRequests.get(chat_id);
|
|
398
|
+
if (prev) {
|
|
399
|
+
this.log(`telegram[${this.channel.name}] interrupting previous request for chat ${chat_id}`);
|
|
400
|
+
prev.abort();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const abortCtrl = new AbortController();
|
|
404
|
+
if (chat_id) this.activeRequests.set(chat_id, abortCtrl);
|
|
405
|
+
|
|
406
|
+
let text = msg.text || msg.caption || "";
|
|
394
407
|
|
|
395
408
|
// ── Incoming photo handling ───────────────────────────────────────────
|
|
396
409
|
if (msg.photo && msg.photo.length > 0) {
|
|
@@ -617,16 +630,22 @@ class ChannelPoller {
|
|
|
617
630
|
prompt: text,
|
|
618
631
|
previousMessages,
|
|
619
632
|
contextNote: `You are replying inside Telegram right now. Telegram channel="${this.channel.name}", author=${author}, chat_id=${chat_id}. Keep the reply plain-text and concise. Previous turns of this chat are included only for local conversational context; re-call tools for facts.`,
|
|
633
|
+
signal: abortCtrl.signal,
|
|
620
634
|
});
|
|
621
635
|
replyText = sa.text;
|
|
622
636
|
replyAuthor = sa.name;
|
|
623
637
|
saTrace = sa.trace;
|
|
624
638
|
saUsage = sa.usage;
|
|
625
639
|
} catch (e) {
|
|
640
|
+
if (abortCtrl.signal.aborted) {
|
|
641
|
+
this.log(`telegram[${this.channel.name}] request aborted for chat ${chat_id}`);
|
|
642
|
+
return; // don't send reply if aborted
|
|
643
|
+
}
|
|
626
644
|
this.log(`telegram[${this.channel.name}] super-agent failed: ${e.message}`);
|
|
627
645
|
}
|
|
628
646
|
}
|
|
629
647
|
|
|
648
|
+
if (chat_id) this.activeRequests.delete(chat_id);
|
|
630
649
|
if (!replyText) {
|
|
631
650
|
stopTyping();
|
|
632
651
|
return;
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
// daemon/skills-loader.js
|
|
2
2
|
// Discover and load APX skills on-demand for the super-agent.
|
|
3
3
|
//
|
|
4
|
-
// The super-agent reads skills from immutable INTERNAL sources under
|
|
5
|
-
//
|
|
6
|
-
// guarantees apx/apc/runtime knowledge is always available regardless
|
|
7
|
-
// what the user does to ~/.apx/skills
|
|
8
|
-
// <package>/skills/ are a separate concern (scaffold.js handles them) and
|
|
9
|
-
// the loader does NOT read from there.
|
|
4
|
+
// The super-agent reads skills from immutable INTERNAL sources under the
|
|
5
|
+
// package root — they ship with apx and can never be deleted by the user.
|
|
6
|
+
// This guarantees apx/apc/runtime knowledge is always available regardless
|
|
7
|
+
// of what the user does to ~/.apx/skills/ or per-project overrides.
|
|
10
8
|
//
|
|
11
9
|
// Discovery order (priority high → low):
|
|
12
|
-
// 1. <projectPath>/.apc/skills/<slug>.md
|
|
13
|
-
// 1b.<projectPath>/.apc/skills/<slug>/SKILL.md
|
|
14
|
-
// 2. ~/.apx/skills/<slug>/SKILL.md
|
|
15
|
-
// 3. <packageRoot>/
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
10
|
+
// 1. <projectPath>/.apc/skills/<slug>.md ← project-scoped (flat)
|
|
11
|
+
// 1b.<projectPath>/.apc/skills/<slug>/SKILL.md ← project-scoped (dir)
|
|
12
|
+
// 2. ~/.apx/skills/<slug>/SKILL.md ← user-installed global
|
|
13
|
+
// 3. <packageRoot>/skills/<slug>/SKILL.md ← bundled core skills
|
|
14
|
+
// (apx, apc-context)
|
|
15
|
+
// 4. <packageRoot>/src/core/runtime-skills/<slug>.md
|
|
16
|
+
// (claude-code, codex-cli,
|
|
17
|
+
// opencode-cli, openrouter)
|
|
20
18
|
//
|
|
21
|
-
// A slug found in a higher-priority location SHADOWS lower ones
|
|
22
|
-
//
|
|
23
|
-
//
|
|
19
|
+
// A slug found in a higher-priority location SHADOWS lower ones. A user can
|
|
20
|
+
// override the bundled apc-context by dropping `~/.apx/skills/apc-context/SKILL.md`,
|
|
21
|
+
// but the bundled copy stays in the package as a safety net.
|
|
22
|
+
//
|
|
23
|
+
// Note: the bundled `apc-context` skill is REFRESHED from the canonical apc
|
|
24
|
+
// repo on every npm install / update (see src/cli/postinstall.js). APC is a
|
|
25
|
+
// living standard, so its skill content is not pinned to an apx version.
|
|
24
26
|
|
|
25
27
|
import fs from "node:fs";
|
|
26
28
|
import path from "node:path";
|
|
@@ -32,38 +34,8 @@ const __dirname = path.dirname(__filename);
|
|
|
32
34
|
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
33
35
|
|
|
34
36
|
const RUNTIME_SKILLS_DIR = path.join(PACKAGE_ROOT, "src", "core", "runtime-skills");
|
|
37
|
+
const BUNDLED_SKILLS_DIR = path.join(PACKAGE_ROOT, "skills");
|
|
35
38
|
const GLOBAL_DIR = path.join(os.homedir(), ".apx", "skills");
|
|
36
|
-
const CORE_DIR = path.join(PACKAGE_ROOT, "src", "core");
|
|
37
|
-
|
|
38
|
-
// Intrinsic built-in skills whose source files (src/core/*-skill.md) do NOT
|
|
39
|
-
// carry frontmatter — the scaffold.js wrapper adds frontmatter when copying
|
|
40
|
-
// these out to external IDE skill dirs. For the super-agent's catalog we
|
|
41
|
-
// supply slug + description inline. Keep in sync with scaffold.js.
|
|
42
|
-
const INTRINSIC = [
|
|
43
|
-
{
|
|
44
|
-
slug: "apx",
|
|
45
|
-
file: path.join(CORE_DIR, "apx-skill.md"),
|
|
46
|
-
description:
|
|
47
|
-
"APX CLI skill. Activate when: user asks to run or coordinate agents, " +
|
|
48
|
-
"use MCP tools from .apc/mcps.json, install agents from a team workspace, " +
|
|
49
|
-
"or explicitly mentions apx commands. Do NOT activate just because .apc/ exists — " +
|
|
50
|
-
"that is handled by the apc-context skill. Activate on: 'apx run', 'apx exec', " +
|
|
51
|
-
"'run an agent', 'coordinate agents', 'MCP not working', 'install agent', " +
|
|
52
|
-
"'team agents', 'apx memory', 'daemon'.",
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
slug: "apc-context",
|
|
56
|
-
file: path.join(CORE_DIR, "apc-context-skill.md"),
|
|
57
|
-
description:
|
|
58
|
-
"ALWAYS activate when the project has a .apc/ directory or AGENTS.md file. " +
|
|
59
|
-
"Do not wait to be asked. Read .apc/ before making any assumption about agents, " +
|
|
60
|
-
"memory, or project structure. Activate on: .apc/, AGENTS.md, 'which agents', " +
|
|
61
|
-
"'list agents', 'agent context', 'who are the agents', any question about agents " +
|
|
62
|
-
"or memory in this project. IMPORTANT: if .apc/migrate.md exists, open the " +
|
|
63
|
-
"conversation with a migration offer before answering anything else. If the user " +
|
|
64
|
-
"declines, delete .apc/migrate.md immediately so it is not shown again.",
|
|
65
|
-
},
|
|
66
|
-
];
|
|
67
39
|
|
|
68
40
|
// ---------------------------------------------------------------------------
|
|
69
41
|
// Frontmatter parsing (minimal — handles the YAML we ship)
|
|
@@ -153,15 +125,11 @@ export function listSkills({ projectPath } = {}) {
|
|
|
153
125
|
// priority 2: user-installed global
|
|
154
126
|
found.push(...scanDirStyle(GLOBAL_DIR, "global"));
|
|
155
127
|
|
|
156
|
-
// priority 3:
|
|
157
|
-
found.push(...
|
|
128
|
+
// priority 3: bundled core skills (apx, apc-context)
|
|
129
|
+
found.push(...scanDirStyle(BUNDLED_SKILLS_DIR, "builtin"));
|
|
158
130
|
|
|
159
|
-
// priority 4:
|
|
160
|
-
|
|
161
|
-
if (fs.existsSync(it.file)) {
|
|
162
|
-
found.push({ slug: it.slug, source: "builtin", file: it.file, _description: it.description });
|
|
163
|
-
}
|
|
164
|
-
}
|
|
131
|
+
// priority 4: runtime docs (claude-code, codex-cli, opencode-cli, openrouter)
|
|
132
|
+
found.push(...scanFlatStyle(RUNTIME_SKILLS_DIR, "builtin"));
|
|
165
133
|
|
|
166
134
|
// dedupe by slug (first-wins = higher priority shadows lower)
|
|
167
135
|
const seen = new Set();
|
|
@@ -170,15 +138,12 @@ export function listSkills({ projectPath } = {}) {
|
|
|
170
138
|
if (seen.has(entry.slug)) continue;
|
|
171
139
|
seen.add(entry.slug);
|
|
172
140
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
description = fm.description || "";
|
|
180
|
-
} catch { /* unreadable — skip description */ }
|
|
181
|
-
}
|
|
141
|
+
let description = "";
|
|
142
|
+
try {
|
|
143
|
+
const raw = fs.readFileSync(entry.file, "utf8");
|
|
144
|
+
const { fm } = parseFrontmatter(raw);
|
|
145
|
+
description = fm.description || "";
|
|
146
|
+
} catch { /* unreadable — skip description */ }
|
|
182
147
|
|
|
183
148
|
result.push({
|
|
184
149
|
slug: entry.slug,
|
|
@@ -223,6 +188,6 @@ export function loadSkill(slug, { projectPath } = {}) {
|
|
|
223
188
|
// Useful for diagnostics
|
|
224
189
|
export const SKILL_LOCATIONS = {
|
|
225
190
|
runtime_skills: RUNTIME_SKILLS_DIR,
|
|
226
|
-
|
|
191
|
+
bundled: BUNDLED_SKILLS_DIR,
|
|
227
192
|
global: GLOBAL_DIR,
|
|
228
193
|
};
|
package/src/daemon/smoke.js
CHANGED
|
@@ -14,7 +14,14 @@ import { readAgents } from "../core/parser.js";
|
|
|
14
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
15
|
const __dirname = path.dirname(__filename);
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const EXAMPLE_CANDIDATES = [
|
|
18
|
+
path.resolve(__dirname, "..", "..", "examples", "my-first-project"),
|
|
19
|
+
path.resolve(__dirname, "..", "..", "..", "apc", "examples", "my-first-project"),
|
|
20
|
+
];
|
|
21
|
+
const EXAMPLE = EXAMPLE_CANDIDATES.find((p) =>
|
|
22
|
+
fs.existsSync(path.join(p, "AGENTS.md")) &&
|
|
23
|
+
fs.existsSync(path.join(p, ".apc", "project.json"))
|
|
24
|
+
);
|
|
18
25
|
|
|
19
26
|
function assert(cond, msg) {
|
|
20
27
|
if (!cond) {
|
|
@@ -24,6 +31,7 @@ function assert(cond, msg) {
|
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
const projects = new ProjectManager();
|
|
34
|
+
assert(EXAMPLE, `example project missing; checked ${EXAMPLE_CANDIDATES.join(", ")}`);
|
|
27
35
|
const entry = projects.register(EXAMPLE);
|
|
28
36
|
console.log("registered project", entry.id, entry.path);
|
|
29
37
|
|
|
@@ -22,6 +22,7 @@ import searchFiles from "./tools/search-files.js";
|
|
|
22
22
|
import listSkills from "./tools/list-skills.js";
|
|
23
23
|
import loadSkill from "./tools/load-skill.js";
|
|
24
24
|
import transcribeAudio from "./tools/transcribe-audio.js";
|
|
25
|
+
import askQuestions from "./tools/ask-questions.js";
|
|
25
26
|
import { createPermissionGuard } from "./helpers.js";
|
|
26
27
|
import { buildBridgedTools, DEFAULT_CATEGORIES } from "./registry-bridge.js";
|
|
27
28
|
|
|
@@ -50,6 +51,7 @@ const NATIVE_TOOLS = [
|
|
|
50
51
|
listSkills,
|
|
51
52
|
loadSkill,
|
|
52
53
|
transcribeAudio,
|
|
54
|
+
askQuestions,
|
|
53
55
|
];
|
|
54
56
|
|
|
55
57
|
// Registry-backed bridges. Categories can be overridden per-process via env
|