@agentprojectcontext/apx 1.8.2 → 1.9.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 +4 -1
- package/src/cli/commands/artifact.js +45 -0
- package/src/cli/commands/routine.js +15 -1
- package/src/cli/commands/sys.js +325 -0
- package/src/cli/index.js +97 -3
- package/src/cli/terminal-chat/renderer.js +412 -0
- package/src/core/artifacts-store.js +59 -0
- package/src/core/routines-store.js +40 -7
- package/src/daemon/api.js +80 -2
- package/src/daemon/routines.js +141 -13
- package/src/daemon/runtimes/claude-code.js +24 -6
- package/src/daemon/super-agent-tools/index.js +2 -0
- package/src/daemon/super-agent-tools/tools/call-runtime.js +111 -41
- package/src/daemon/super-agent-tools/tools/search-files.js +66 -0
- package/src/daemon/super-agent.js +6 -17
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|