@agentprojectcontext/apx 1.8.2 → 1.10.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 +2 -1
- package/package.json +4 -1
- package/skills/apx/SKILL.md +5 -0
- package/src/cli/commands/artifact.js +45 -0
- package/src/cli/commands/routine.js +15 -1
- package/src/cli/commands/runtime.js +1 -1
- package/src/cli/commands/sys.js +330 -0
- package/src/cli/index.js +100 -6
- package/src/cli/terminal-chat/renderer.js +412 -0
- package/src/core/apc-context-skill.md +2 -2
- package/src/core/apx-skill.md +4 -0
- package/src/core/artifacts-store.js +59 -0
- package/src/core/routines-store.js +40 -7
- package/src/daemon/apc-runtime-context.js +3 -2
- package/src/daemon/api.js +80 -2
- package/src/daemon/env-detect.js +1 -0
- package/src/daemon/routines.js +141 -13
- package/src/daemon/runtimes/claude-code.js +24 -6
- package/src/daemon/runtimes/cursor-agent.js +34 -0
- package/src/daemon/runtimes/gemini-cli.js +32 -0
- package/src/daemon/runtimes/index.js +8 -1
- package/src/daemon/runtimes/qwen-code.js +36 -0
- package/src/daemon/super-agent-tools/index.js +2 -0
- package/src/daemon/super-agent-tools/tools/call-runtime.js +112 -42
- package/src/daemon/super-agent-tools/tools/search-files.js +66 -0
- package/src/daemon/super-agent.js +6 -17
- package/src/mcp/index.js +1 -1
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
export const MODES = ["Build", "Plan", "Zen"];
|
|
5
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
6
|
+
|
|
7
|
+
export const C = {
|
|
8
|
+
reset: "\x1b[0m",
|
|
9
|
+
altOn: "\x1b[?1049h",
|
|
10
|
+
altOff: "\x1b[?1049l",
|
|
11
|
+
showCursor: "\x1b[?25h",
|
|
12
|
+
setBgBlack: "\x1b]11;#000000\x07",
|
|
13
|
+
resetBg: "\x1b]111\x07",
|
|
14
|
+
bg: "\x1b[48;2;0;0;0m",
|
|
15
|
+
panel: "\x1b[48;2;26;26;26m",
|
|
16
|
+
panel2: "\x1b[48;2;31;31;31m",
|
|
17
|
+
text: "\x1b[38;2;237;237;237m",
|
|
18
|
+
muted: "\x1b[38;2;135;135;135m",
|
|
19
|
+
dim: "\x1b[38;2;69;69;69m",
|
|
20
|
+
primary: "\x1b[38;2;82;168;255m",
|
|
21
|
+
warning: "\x1b[38;2;255;178;36m",
|
|
22
|
+
error: "\x1b[38;2;229;72;77m",
|
|
23
|
+
success: "\x1b[38;2;70;167;88m",
|
|
24
|
+
bold: "\x1b[1m",
|
|
25
|
+
normal: "\x1b[22m",
|
|
26
|
+
italic: "\x1b[3m",
|
|
27
|
+
noItalic: "\x1b[23m",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function titlecase(value) {
|
|
31
|
+
const clean = String(value || "").trim();
|
|
32
|
+
if (!clean) return "";
|
|
33
|
+
return clean.slice(0, 1).toUpperCase() + clean.slice(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readPackageVersion() {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(fs.readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version || "dev";
|
|
39
|
+
} catch {
|
|
40
|
+
return "dev";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function visible(text) {
|
|
45
|
+
return String(text).replace(ANSI_RE, "").length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stripAnsi(text) {
|
|
49
|
+
return String(text).replace(ANSI_RE, "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fit(text, width) {
|
|
53
|
+
const clean = stripAnsi(text).replace(/\r?\n/g, " ");
|
|
54
|
+
if (clean.length <= width) return text;
|
|
55
|
+
return clean.slice(0, Math.max(0, width - 1)) + "…";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function padAnsi(text, width) {
|
|
59
|
+
return String(text) + " ".repeat(Math.max(0, width - visible(text)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function terminalSize() {
|
|
63
|
+
return {
|
|
64
|
+
width: Math.max(40, process.stdout.columns || 80),
|
|
65
|
+
height: Math.max(12, process.stdout.rows || 24),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function moveTo(row, col) {
|
|
70
|
+
readline.cursorTo(process.stdout, Math.max(0, col), Math.max(0, row));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeAt(row, col, text, width, bg = C.bg) {
|
|
74
|
+
moveTo(row, col);
|
|
75
|
+
process.stdout.write(bg + padAnsi(text, width) + C.bg);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function clearFull() {
|
|
79
|
+
const { width, height } = terminalSize();
|
|
80
|
+
process.stdout.write(C.bg + "\x1b[2J\x1b[3J\x1b[H");
|
|
81
|
+
for (let row = 0; row < height; row++) {
|
|
82
|
+
process.stdout.write(C.bg + " ".repeat(width));
|
|
83
|
+
if (row < height - 1) process.stdout.write("\n");
|
|
84
|
+
}
|
|
85
|
+
process.stdout.write("\x1b[H" + C.bg);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function centerLeft(width, contentWidth) {
|
|
89
|
+
return Math.max(0, Math.floor((width - contentWidth) / 2));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function wrapText(text, width) {
|
|
93
|
+
const raw = String(text || "").replace(/\t/g, " ").split(/\r?\n/);
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const line of raw) {
|
|
96
|
+
if (!line) {
|
|
97
|
+
out.push("");
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
let rest = line;
|
|
101
|
+
while (rest.length > width) {
|
|
102
|
+
let cut = rest.lastIndexOf(" ", width);
|
|
103
|
+
if (cut < Math.floor(width * 0.5)) cut = width;
|
|
104
|
+
out.push(rest.slice(0, cut));
|
|
105
|
+
rest = rest.slice(cut).trimStart();
|
|
106
|
+
}
|
|
107
|
+
out.push(rest);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function renderLogo(termWidth, top) {
|
|
113
|
+
const lines = [
|
|
114
|
+
" █████╗ ██████╗ ██╗ ██╗",
|
|
115
|
+
"██╔══██╗██╔══██╗╚██╗██╔╝",
|
|
116
|
+
"███████║██████╔╝ ╚███╔╝ ",
|
|
117
|
+
"██╔══██║██╔═══╝ ██╔██╗ ",
|
|
118
|
+
"██║ ██║██║ ██╔╝ ██╗",
|
|
119
|
+
"╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝",
|
|
120
|
+
];
|
|
121
|
+
for (let i = 0; i < lines.length; i++) {
|
|
122
|
+
const line = lines[i];
|
|
123
|
+
const left = centerLeft(termWidth, visible(line));
|
|
124
|
+
const color = i < 2 ? C.dim : i < 4 ? C.muted : C.text;
|
|
125
|
+
writeAt(top + i, left, C.bold + color + line + C.normal, visible(line));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderModeMeta(currentModeIdx, activeAgent, activeModel, maxWidth) {
|
|
130
|
+
const modeText = MODES.map((mode, i) =>
|
|
131
|
+
i === currentModeIdx ? C.primary + mode : C.muted + mode
|
|
132
|
+
).join(C.muted + " · ");
|
|
133
|
+
const meta = `${modeText}${C.muted} · ${C.text}${C.bold}${activeAgent}${C.normal} ${C.muted}${activeModel}`;
|
|
134
|
+
return visible(meta) <= maxWidth ? meta : C.muted + fit(stripAnsi(meta), maxWidth);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function promptGeometry(centered, hasStarted, chatWidth) {
|
|
138
|
+
const { width, height } = terminalSize();
|
|
139
|
+
const boxWidth = hasStarted
|
|
140
|
+
? Math.max(36, chatWidth - 4)
|
|
141
|
+
: Math.min(75, Math.max(36, width - 4));
|
|
142
|
+
const left = centered ? centerLeft(width, boxWidth) : 2;
|
|
143
|
+
const top = hasStarted ? Math.max(0, height - 7) : Math.max(1, Math.floor(height / 2));
|
|
144
|
+
return { left, top, boxWidth };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function renderPromptBlock(state, chatWidth) {
|
|
148
|
+
const {
|
|
149
|
+
currentModeIdx,
|
|
150
|
+
activeAgent,
|
|
151
|
+
activeModel,
|
|
152
|
+
inputText,
|
|
153
|
+
cursorIndex,
|
|
154
|
+
hasStarted,
|
|
155
|
+
} = state;
|
|
156
|
+
const { left, top, boxWidth } = promptGeometry(!hasStarted, hasStarted, chatWidth);
|
|
157
|
+
const contentWidth = boxWidth - 3;
|
|
158
|
+
const placeholder = `Ask anything... "Fix broken tests"`;
|
|
159
|
+
const displayStart = Math.max(0, cursorIndex - contentWidth + 1);
|
|
160
|
+
const inputVisible = inputText.slice(displayStart, displayStart + contentWidth);
|
|
161
|
+
const beforeCursor = inputText.slice(displayStart, cursorIndex);
|
|
162
|
+
const promptLine = inputText
|
|
163
|
+
? C.text + fit(inputVisible, contentWidth)
|
|
164
|
+
: C.muted + C.italic + fit(placeholder, contentWidth) + C.noItalic;
|
|
165
|
+
const metaLine = renderModeMeta(currentModeIdx, activeAgent, activeModel, contentWidth);
|
|
166
|
+
|
|
167
|
+
writeAt(top, left, C.primary + "┃" + C.panel + " " + " ".repeat(contentWidth), boxWidth, C.bg);
|
|
168
|
+
writeAt(top + 1, left, C.primary + "┃" + C.panel + " " + padAnsi(promptLine, contentWidth), boxWidth, C.bg);
|
|
169
|
+
writeAt(top + 2, left, C.primary + "┃" + C.panel + " " + " ".repeat(contentWidth), boxWidth, C.bg);
|
|
170
|
+
writeAt(top + 3, left, C.primary + "┃" + C.panel + " " + padAnsi(metaLine, contentWidth), boxWidth, C.bg);
|
|
171
|
+
writeAt(top + 4, left, C.primary + "╹" + C.panel + " " + " ".repeat(contentWidth), boxWidth, C.bg);
|
|
172
|
+
|
|
173
|
+
const hotkeys =
|
|
174
|
+
C.bold + C.text + "tab" + C.normal + C.muted + " agents " +
|
|
175
|
+
C.bold + C.text + "ctrl+p" + C.normal + C.muted + " commands " +
|
|
176
|
+
C.bold + C.text + "enter" + C.normal + C.muted + " send";
|
|
177
|
+
const hotkeyLeft = Math.max(left, left + boxWidth - visible(hotkeys));
|
|
178
|
+
writeAt(top + 5, hotkeyLeft, hotkeys, visible(hotkeys), C.bg);
|
|
179
|
+
|
|
180
|
+
return { row: top + 1, col: left + 2 + visible(beforeCursor) };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function addLine(lines, text = "", bg = C.bg) {
|
|
184
|
+
lines.push({ text, bg });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function toolLabel(tool) {
|
|
188
|
+
return {
|
|
189
|
+
read_file: "Read",
|
|
190
|
+
write_file: "Wrote",
|
|
191
|
+
edit_file: "Edit",
|
|
192
|
+
search_files: "Search",
|
|
193
|
+
run_shell: "Shell",
|
|
194
|
+
list_files: "List",
|
|
195
|
+
list_projects: "Projects",
|
|
196
|
+
list_agents: "Agents",
|
|
197
|
+
tail_messages: "Messages",
|
|
198
|
+
}[tool] || tool;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseTraceResult(result) {
|
|
202
|
+
if (typeof result !== "string") return result;
|
|
203
|
+
try {
|
|
204
|
+
return JSON.parse(result);
|
|
205
|
+
} catch {
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function resultPreview(result) {
|
|
211
|
+
const parsed = parseTraceResult(result);
|
|
212
|
+
if (!parsed) return "";
|
|
213
|
+
if (typeof parsed === "string") return parsed;
|
|
214
|
+
if (parsed.error) return String(parsed.error);
|
|
215
|
+
if (parsed.content) return String(parsed.content);
|
|
216
|
+
if (parsed.stdout) return String(parsed.stdout);
|
|
217
|
+
if (parsed.path) return String(parsed.path);
|
|
218
|
+
return JSON.stringify(parsed);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function addToolBlock(lines, item, width) {
|
|
222
|
+
const trace = item.trace || {};
|
|
223
|
+
const args = trace.args || {};
|
|
224
|
+
const label = toolLabel(trace.tool);
|
|
225
|
+
const target = args.path || args.query || args.command || args.project || "";
|
|
226
|
+
const inner = Math.max(12, width - 12);
|
|
227
|
+
const margin = " ";
|
|
228
|
+
|
|
229
|
+
addLine(lines, "", C.bg);
|
|
230
|
+
addLine(lines, margin + C.muted + `→ ${label}${target ? " " + fit(String(target), inner) : ""}`, C.bg);
|
|
231
|
+
|
|
232
|
+
if (trace.tool === "write_file") {
|
|
233
|
+
const heading = `# Wrote ${args.path || "file"}`;
|
|
234
|
+
addLine(lines, margin + C.panel + " " + C.muted + heading + " ".repeat(Math.max(0, inner - visible(heading))), C.bg);
|
|
235
|
+
for (const chunk of wrapText(args.content || "", inner).slice(0, 8)) {
|
|
236
|
+
addLine(lines, margin + C.panel + " " + C.text + padAnsi(chunk, inner), C.bg);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (trace.tool === "edit_file") {
|
|
242
|
+
const heading = `← Edit ${args.path || "file"}`;
|
|
243
|
+
addLine(lines, margin + C.panel + " " + C.muted + heading + " ".repeat(Math.max(0, inner - visible(heading))), C.bg);
|
|
244
|
+
for (const chunk of wrapText(args.search || "", inner - 2).slice(0, 5)) {
|
|
245
|
+
addLine(lines, margin + C.panel + " " + C.error + "- " + padAnsi(chunk, inner - 2), C.bg);
|
|
246
|
+
}
|
|
247
|
+
for (const chunk of wrapText(args.replace || "", inner - 2).slice(0, 5)) {
|
|
248
|
+
addLine(lines, margin + C.panel + " " + C.success + "+ " + padAnsi(chunk, inner - 2), C.bg);
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const preview = resultPreview(trace.result);
|
|
254
|
+
if (!preview) return;
|
|
255
|
+
for (const chunk of wrapText(preview, inner).slice(0, 6)) {
|
|
256
|
+
addLine(lines, margin + C.dim + " " + C.muted + chunk, C.bg);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function transcriptLines(transcript, width) {
|
|
261
|
+
const lines = [];
|
|
262
|
+
const inner = Math.max(10, width - 8);
|
|
263
|
+
const margin = " ";
|
|
264
|
+
|
|
265
|
+
for (const item of transcript) {
|
|
266
|
+
if (item.type === "user") {
|
|
267
|
+
addLine(lines, "", C.bg);
|
|
268
|
+
const chunks = wrapText(item.text, inner - 1);
|
|
269
|
+
addLine(lines, margin + C.primary + "┃" + C.panel + " " + " ".repeat(inner), C.bg);
|
|
270
|
+
for (const chunk of chunks) {
|
|
271
|
+
addLine(lines, margin + C.primary + "┃" + C.panel + " " + C.text + padAnsi(chunk, inner), C.bg);
|
|
272
|
+
}
|
|
273
|
+
addLine(lines, margin + C.primary + "┃" + C.panel + " " + " ".repeat(inner), C.bg);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (item.type === "assistant") {
|
|
278
|
+
addLine(lines, "", C.bg);
|
|
279
|
+
addLine(lines, margin + C.primary + "■ " + C.text + C.bold + `${item.name}:` + C.normal, C.bg);
|
|
280
|
+
for (const raw of String(item.text || "").split(/\r?\n/)) {
|
|
281
|
+
const isThinking = raw.trim().startsWith("Thinking:");
|
|
282
|
+
const color = isThinking ? C.warning + C.italic : C.text;
|
|
283
|
+
const end = isThinking ? C.noItalic : "";
|
|
284
|
+
const prefix = isThinking ? margin + C.dim + "┃ " : margin + " ";
|
|
285
|
+
if (isThinking) addLine(lines, "", C.bg);
|
|
286
|
+
for (const chunk of wrapText(raw, width - 6)) {
|
|
287
|
+
addLine(lines, prefix + color + chunk + end, C.bg);
|
|
288
|
+
}
|
|
289
|
+
if (isThinking) addLine(lines, "", C.bg);
|
|
290
|
+
}
|
|
291
|
+
if (item.meta) addLine(lines, margin + " " + C.muted + item.meta, C.bg);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (item.type === "tool") {
|
|
296
|
+
addToolBlock(lines, item, width);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (item.type === "status") {
|
|
301
|
+
addLine(lines, margin + C.muted + C.italic + item.text + C.noItalic, C.bg);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (item.type === "error") {
|
|
306
|
+
addLine(lines, margin + C.error + "✖ Error: " + C.text + item.text, C.bg);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return lines;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderChat(transcript, chatWidth, height, promptTop) {
|
|
314
|
+
const maxRows = Math.max(1, promptTop - 1);
|
|
315
|
+
const lines = transcriptLines(transcript, chatWidth - 2);
|
|
316
|
+
const slice = lines.slice(Math.max(0, lines.length - maxRows));
|
|
317
|
+
|
|
318
|
+
for (let i = 0; i < slice.length && i < maxRows; i++) {
|
|
319
|
+
writeAt(i, 0, slice[i].text, chatWidth - 1, slice[i].bg);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function renderSidebar(state) {
|
|
324
|
+
const { width, height } = terminalSize();
|
|
325
|
+
if (width < 84) return null;
|
|
326
|
+
|
|
327
|
+
const sideWidth = Math.min(34, Math.max(28, Math.floor(width * 0.3)));
|
|
328
|
+
const left = width - sideWidth;
|
|
329
|
+
for (let row = 0; row < height; row++) {
|
|
330
|
+
writeAt(row, left, "", sideWidth, C.panel);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const contentWidth = sideWidth - 4;
|
|
334
|
+
const totalTokens = state.usage.input + state.usage.output;
|
|
335
|
+
|
|
336
|
+
writeAt(1, left + 2, C.text + C.bold + "Sesión" + C.normal, contentWidth, C.panel);
|
|
337
|
+
writeAt(2, left + 2, C.muted + fit(state.sessionTitle || "chat local", contentWidth), contentWidth, C.panel);
|
|
338
|
+
writeAt(3, left + 2, C.muted + "agent " + C.text + state.activeAgent, contentWidth, C.panel);
|
|
339
|
+
writeAt(4, left + 2, C.muted + "app " + C.text + `APX ${state.version}`, contentWidth, C.panel);
|
|
340
|
+
|
|
341
|
+
writeAt(6, left + 2, C.text + C.bold + "Modelo" + C.normal, contentWidth, C.panel);
|
|
342
|
+
const modelLines = wrapText(state.activeModel || "(none)", contentWidth).slice(0, 2);
|
|
343
|
+
modelLines.forEach((line, index) => {
|
|
344
|
+
writeAt(7 + index, left + 2, C.muted + line, contentWidth, C.panel);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
writeAt(10, left + 2, C.text + C.bold + "Contexto" + C.normal, contentWidth, C.panel);
|
|
348
|
+
writeAt(11, left + 2, C.muted + `${totalTokens.toLocaleString()} tokens total`, contentWidth, C.panel);
|
|
349
|
+
writeAt(12, left + 2, C.muted + `${state.usage.input.toLocaleString()} in · ${state.usage.output.toLocaleString()} out`, contentWidth, C.panel);
|
|
350
|
+
writeAt(13, left + 2, C.muted + `${state.usage.percent}% usado`, contentWidth, C.panel);
|
|
351
|
+
writeAt(14, left + 2, C.muted + "$0.00 spent", contentWidth, C.panel);
|
|
352
|
+
|
|
353
|
+
writeAt(16, left + 2, C.text + C.bold + "LSP" + C.normal, contentWidth, C.panel);
|
|
354
|
+
writeAt(17, left + 2, C.muted + "LSPs are disabled", contentWidth, C.panel);
|
|
355
|
+
|
|
356
|
+
const cwdLines = wrapText(process.cwd(), contentWidth).slice(-4);
|
|
357
|
+
let row = Math.max(19, height - cwdLines.length - 4);
|
|
358
|
+
writeAt(row++, left + 2, C.text + C.bold + "Directorio" + C.normal, contentWidth, C.panel);
|
|
359
|
+
for (const line of cwdLines) writeAt(row++, left + 2, C.muted + line, contentWidth, C.panel);
|
|
360
|
+
writeAt(height - 1, left + 2, C.success + "• " + C.text + "APX" + C.muted + ` ${state.version}`, contentWidth, C.panel);
|
|
361
|
+
|
|
362
|
+
return { left, width: sideWidth };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function renderPaletteOverlay(state) {
|
|
366
|
+
const { width, height } = terminalSize();
|
|
367
|
+
const title = state.paletteState === "main" ? "COMMAND PALETTE" : "SELECT MODEL";
|
|
368
|
+
const boxWidth = Math.min(
|
|
369
|
+
62,
|
|
370
|
+
Math.max(32, Math.max(title.length + 8, ...state.paletteOptions.map((x) => visible(x) + 8)))
|
|
371
|
+
);
|
|
372
|
+
const boxHeight = state.paletteOptions.length + 4;
|
|
373
|
+
const left = centerLeft(width, boxWidth);
|
|
374
|
+
const top = Math.max(1, Math.floor((height - boxHeight) / 2));
|
|
375
|
+
|
|
376
|
+
writeAt(top, left, C.text + C.bold + " " + title + C.normal, boxWidth, C.panel);
|
|
377
|
+
writeAt(top + 1, left, C.dim + "▀".repeat(boxWidth), boxWidth, C.panel);
|
|
378
|
+
for (let i = 0; i < state.paletteOptions.length; i++) {
|
|
379
|
+
const active = i === state.paletteSelection;
|
|
380
|
+
const marker = active ? "›" : " ";
|
|
381
|
+
const bg = active ? C.panel2 : C.panel;
|
|
382
|
+
const fg = active ? C.primary + C.bold : C.text;
|
|
383
|
+
writeAt(top + 2 + i, left, fg + ` ${marker} ${state.paletteOptions[i]}` + C.normal, boxWidth, bg);
|
|
384
|
+
}
|
|
385
|
+
writeAt(top + 2 + state.paletteOptions.length, left, C.dim + "▄".repeat(boxWidth), boxWidth, C.panel);
|
|
386
|
+
writeAt(
|
|
387
|
+
top + 3 + state.paletteOptions.length,
|
|
388
|
+
left,
|
|
389
|
+
C.muted + "↑↓ select " + C.text + C.bold + "enter" + C.normal + C.muted + " choose " + C.text + C.bold + "esc" + C.normal + C.muted + " close",
|
|
390
|
+
boxWidth,
|
|
391
|
+
C.bg
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function renderTerminalChat(state) {
|
|
396
|
+
clearFull();
|
|
397
|
+
const { width, height } = terminalSize();
|
|
398
|
+
const sidebar = state.hasStarted ? renderSidebar(state) : null;
|
|
399
|
+
const chatWidth = sidebar ? sidebar.left : width;
|
|
400
|
+
|
|
401
|
+
if (!state.hasStarted) {
|
|
402
|
+
renderLogo(chatWidth, Math.max(1, Math.floor(height / 2) - 8));
|
|
403
|
+
} else {
|
|
404
|
+
const prompt = promptGeometry(false, true, chatWidth);
|
|
405
|
+
renderChat(state.transcript, chatWidth, height, prompt.top);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const cursor = renderPromptBlock(state, chatWidth);
|
|
409
|
+
|
|
410
|
+
if (state.inCommandPalette) renderPaletteOverlay(state);
|
|
411
|
+
if (!state.inCommandPalette) moveTo(cursor.row, cursor.col);
|
|
412
|
+
}
|
|
@@ -92,8 +92,8 @@ sanitized fact to `.apc/agents/<slug>/memory.md` only when useful and safe.
|
|
|
92
92
|
## APX
|
|
93
93
|
|
|
94
94
|
APX can provide a local daemon, MCP management, Telegram bridge, routines, and runtime dispatch
|
|
95
|
-
across Codex, Claude Code, OpenCode, Aider,
|
|
96
|
-
not APC portable-core requirements.
|
|
95
|
+
across Codex, Claude Code, OpenCode, Aider, Cursor Agent, Gemini CLI, Qwen Code, or direct LLM
|
|
96
|
+
engines. Those are APX runtime features, not APC portable-core requirements.
|
|
97
97
|
|
|
98
98
|
The APX super-agent uses `~/.apx/projects/default` for system-level work when no project is named.
|
|
99
99
|
APX routines can run heartbeat, shell, Telegram, project agent, or super-agent tasks on a schedule.
|
package/src/core/apx-skill.md
CHANGED
|
@@ -26,6 +26,10 @@ Use `apx run` only when:
|
|
|
26
26
|
apx run <slug> --runtime claude-code "<prompt>"
|
|
27
27
|
apx run <slug> --runtime codex "<prompt>"
|
|
28
28
|
apx run <slug> --runtime opencode "<prompt>"
|
|
29
|
+
apx run <slug> --runtime aider "<prompt>"
|
|
30
|
+
apx run <slug> --runtime cursor-agent "<prompt>"
|
|
31
|
+
apx run <slug> --runtime gemini-cli "<prompt>"
|
|
32
|
+
apx run <slug> --runtime qwen-code "<prompt>"
|
|
29
33
|
|
|
30
34
|
# Example: run the qa agent in codex with a specific task
|
|
31
35
|
apx run qa --runtime codex "run the full test suite and report failures"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Artifacts: managed files stored in storagePath/artifacts/.
|
|
2
|
+
// Agents, routines, and shell scripts can create and reference these files.
|
|
3
|
+
// Path: ~/.apx/projects/{apxId}/artifacts/<name>
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export const ARTIFACTS_SKIP_SIGNAL = "APX_SKIP";
|
|
8
|
+
|
|
9
|
+
export function artifactsDir(storagePath) {
|
|
10
|
+
return path.join(storagePath, "artifacts");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function artifactPath(storagePath, name) {
|
|
14
|
+
return path.join(artifactsDir(storagePath), name);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Resolve "artifact:<name>" shorthand in command strings.
|
|
18
|
+
export function resolveArtifactRef(cmd, storagePath) {
|
|
19
|
+
if (typeof cmd === "string" && cmd.startsWith("artifact:")) {
|
|
20
|
+
const name = cmd.slice(9).trim();
|
|
21
|
+
return artifactPath(storagePath, name);
|
|
22
|
+
}
|
|
23
|
+
return cmd;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createArtifact(storagePath, name, content = "") {
|
|
27
|
+
const dir = artifactsDir(storagePath);
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
const p = artifactPath(storagePath, name);
|
|
30
|
+
if (fs.existsSync(p)) throw new Error(`artifact "${name}" already exists at ${p}`);
|
|
31
|
+
fs.writeFileSync(p, content);
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function listArtifacts(storagePath) {
|
|
36
|
+
const dir = artifactsDir(storagePath);
|
|
37
|
+
if (!fs.existsSync(dir)) return [];
|
|
38
|
+
return fs.readdirSync(dir)
|
|
39
|
+
.filter((f) => !f.startsWith("."))
|
|
40
|
+
.sort()
|
|
41
|
+
.map((f) => {
|
|
42
|
+
const p = path.join(dir, f);
|
|
43
|
+
const stat = fs.statSync(p);
|
|
44
|
+
return { name: f, path: p, size: stat.size, modified: stat.mtime.toISOString() };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function readArtifact(storagePath, name) {
|
|
49
|
+
const p = artifactPath(storagePath, name);
|
|
50
|
+
if (!fs.existsSync(p)) throw new Error(`artifact "${name}" not found`);
|
|
51
|
+
return { name, path: p, content: fs.readFileSync(p, "utf8") };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function removeArtifact(storagePath, name) {
|
|
55
|
+
const p = artifactPath(storagePath, name);
|
|
56
|
+
if (!fs.existsSync(p)) return false;
|
|
57
|
+
fs.unlinkSync(p);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Replaces the SQLite `routines` table for project-scoped scheduled tasks.
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import cronParser from "cron-parser";
|
|
5
6
|
|
|
6
7
|
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
7
8
|
const isoToMs = (iso) => (iso ? Date.parse(iso) : 0);
|
|
@@ -32,6 +33,7 @@ function writeFile(projectPath, routines) {
|
|
|
32
33
|
|
|
33
34
|
export function parseSchedule(s, baseMs = Date.now()) {
|
|
34
35
|
if (!s || typeof s !== "string") return { kind: "invalid" };
|
|
36
|
+
|
|
35
37
|
if (s.startsWith("every:")) {
|
|
36
38
|
const spec = s.slice(6).trim();
|
|
37
39
|
const m = spec.match(/^(\d+)(s|m|h|d)$/);
|
|
@@ -40,13 +42,21 @@ export function parseSchedule(s, baseMs = Date.now()) {
|
|
|
40
42
|
const mult = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
|
|
41
43
|
return { kind: "every", intervalMs: n * mult };
|
|
42
44
|
}
|
|
45
|
+
|
|
43
46
|
if (s.startsWith("once:")) {
|
|
44
47
|
const ts = s.slice(5).trim();
|
|
45
48
|
const ms = Date.parse(ts);
|
|
46
49
|
if (isNaN(ms)) return { kind: "invalid" };
|
|
47
50
|
return { kind: "once", atMs: ms };
|
|
48
51
|
}
|
|
49
|
-
|
|
52
|
+
|
|
53
|
+
// Fallback: Try parsing as standard cron expression using cron-parser
|
|
54
|
+
try {
|
|
55
|
+
const interval = cronParser.parseExpression(s, { currentDate: new Date(baseMs) });
|
|
56
|
+
return { kind: "cron", parser: interval };
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return { kind: "invalid" };
|
|
59
|
+
}
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
export function computeNextRun(routine, baseMs = Date.now()) {
|
|
@@ -63,6 +73,14 @@ export function computeNextRun(routine, baseMs = Date.now()) {
|
|
|
63
73
|
const target = next < baseMs ? baseMs + 100 : next;
|
|
64
74
|
return new Date(target).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
65
75
|
}
|
|
76
|
+
if (sched.kind === "cron") {
|
|
77
|
+
try {
|
|
78
|
+
const nextDate = sched.parser.next();
|
|
79
|
+
return nextDate.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
66
84
|
return null;
|
|
67
85
|
}
|
|
68
86
|
|
|
@@ -76,10 +94,10 @@ export function getRoutine(projectPath, name) {
|
|
|
76
94
|
return readFile(projectPath).find((r) => r.name === name) || null;
|
|
77
95
|
}
|
|
78
96
|
|
|
79
|
-
export function upsertRoutine(
|
|
97
|
+
export function upsertRoutine(storagePath, { name, kind, schedule, spec, enabled = true, permission_mode, allowed_tools, pre_commands, post_commands, skip_prompt_on }) {
|
|
80
98
|
if (!name || !kind || !schedule) throw new Error("routine requires name, kind, schedule");
|
|
81
99
|
const now = nowIso();
|
|
82
|
-
const routines = readFile(
|
|
100
|
+
const routines = readFile(storagePath);
|
|
83
101
|
const idx = routines.findIndex((r) => r.name === name);
|
|
84
102
|
const prev = idx >= 0 ? routines[idx] : null;
|
|
85
103
|
const next = computeNextRun({ schedule, last_run_at: null });
|
|
@@ -90,6 +108,16 @@ export function upsertRoutine(projectPath, { name, kind, schedule, spec, enabled
|
|
|
90
108
|
spec: spec || {},
|
|
91
109
|
permission_mode: permission_mode || prev?.permission_mode || null,
|
|
92
110
|
allowed_tools: Array.isArray(allowed_tools) ? allowed_tools : (prev?.allowed_tools || []),
|
|
111
|
+
// Pipeline fields
|
|
112
|
+
pre_commands: Array.isArray(pre_commands) ? pre_commands : (prev?.pre_commands || []),
|
|
113
|
+
post_commands: Array.isArray(post_commands) ? post_commands : (prev?.post_commands || []),
|
|
114
|
+
// When to skip phase 2 (the LLM call):
|
|
115
|
+
// "signal" — (default) skip if APX_SKIP found in pre_commands stdout
|
|
116
|
+
// "pre_failure" — skip if any pre_command exits != 0
|
|
117
|
+
// "pre_success" — skip if all pre_commands exit 0
|
|
118
|
+
// "always" — never run the LLM (shell-only routine)
|
|
119
|
+
// "never" — always run the LLM regardless of pre_commands
|
|
120
|
+
skip_prompt_on: skip_prompt_on || prev?.skip_prompt_on || "signal",
|
|
93
121
|
enabled: enabled !== false,
|
|
94
122
|
last_run_at: prev?.last_run_at ?? null,
|
|
95
123
|
last_status: prev?.last_status ?? null,
|
|
@@ -103,7 +131,7 @@ export function upsertRoutine(projectPath, { name, kind, schedule, spec, enabled
|
|
|
103
131
|
} else {
|
|
104
132
|
routines.push(entry);
|
|
105
133
|
}
|
|
106
|
-
writeFile(
|
|
134
|
+
writeFile(storagePath, routines);
|
|
107
135
|
return entry;
|
|
108
136
|
}
|
|
109
137
|
|
|
@@ -141,7 +169,12 @@ export function updateRunState(projectPath, name, { last_run_at, last_status, la
|
|
|
141
169
|
}
|
|
142
170
|
|
|
143
171
|
export function getDueRoutines(projectPath, nowStr) {
|
|
144
|
-
return readFile(projectPath).filter(
|
|
145
|
-
|
|
146
|
-
|
|
172
|
+
return readFile(projectPath).filter((r) => {
|
|
173
|
+
if (!r.enabled) return false;
|
|
174
|
+
// CRITICAL: If the schedule cannot be parsed, NEVER run it.
|
|
175
|
+
// Otherwise, an invalid schedule (like a cron string) sets next_run_at to null,
|
|
176
|
+
// which previously caused it to be considered ALWAYS due and spam execution every 5 seconds!
|
|
177
|
+
if (parseSchedule(r.schedule).kind === "invalid") return false;
|
|
178
|
+
return (!r.next_run_at || r.next_run_at <= nowStr);
|
|
179
|
+
});
|
|
147
180
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Helpers that wrap external runtimes (Claude Code, Codex, OpenCode, Aider
|
|
1
|
+
// Helpers that wrap external runtimes (Claude Code, Codex, OpenCode, Aider,
|
|
2
|
+
// Cursor Agent, Gemini CLI, Qwen Code)
|
|
2
3
|
// with APC awareness:
|
|
3
4
|
//
|
|
4
5
|
// 1. Create an APX runtime session BEFORE the runtime starts.
|
|
@@ -6,7 +7,7 @@
|
|
|
6
7
|
// runtime knows the session id, the cwd of the project, and the apx
|
|
7
8
|
// commands it can use to update memory / append session notes.
|
|
8
9
|
// 3. After the runtime returns, capture the external transcript path
|
|
9
|
-
// (Claude Code gives one
|
|
10
|
+
// (Claude Code gives one; most other runtimes don't yet) and write it
|
|
10
11
|
// into the APX session frontmatter.
|
|
11
12
|
// 4. Close the session with a synthesised result (truncated stdout).
|
|
12
13
|
//
|