@agentprojectcontext/apx 1.14.1 → 1.15.1
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 +10 -2
- package/skills/apc-context/SKILL.md +68 -18
- package/skills/apx/SKILL.md +89 -33
- package/src/cli/commands/daemon.js +39 -7
- 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-tools/tools/transcribe-audio.js +2 -2
- package/src/daemon/super-agent.js +97 -9
- package/src/daemon/transcription.js +154 -48
- package/src/daemon/whisper-server.py +202 -0
- package/src/daemon/whisper-transcribe.py +3 -1
- package/src/core/apc-context-skill.md +0 -105
- package/src/core/apx-skill.md +0 -135
package/src/cli/commands/sys.js
CHANGED
|
@@ -13,6 +13,12 @@ import {
|
|
|
13
13
|
|
|
14
14
|
const MAIN_PALETTE_OPTIONS = ["Switch model", "Connect provider", "Open editor", "Exit"];
|
|
15
15
|
|
|
16
|
+
// Message Actions overlay options for a queued message
|
|
17
|
+
const MSG_ACTION_SEND = "Send now (interrupt current)";
|
|
18
|
+
const MSG_ACTION_COPY = "Copy message text";
|
|
19
|
+
const MSG_ACTION_QUESTION = "Ask about this...";
|
|
20
|
+
const MSG_ACTION_REMOVE = "Remove from queue";
|
|
21
|
+
|
|
16
22
|
export async function cmdSys(args) {
|
|
17
23
|
const pid = await resolveProjectId(args?.flags?.project);
|
|
18
24
|
const cfg = readConfig();
|
|
@@ -34,16 +40,25 @@ export async function cmdSys(args) {
|
|
|
34
40
|
usage: { input: 0, output: 0, percent: 0 },
|
|
35
41
|
chatScrollOffset: 0,
|
|
36
42
|
transcript: [],
|
|
43
|
+
// Message Actions overlay state
|
|
44
|
+
inMsgActions: false,
|
|
45
|
+
msgActionsTarget: null, // { text } of the targeted message
|
|
46
|
+
msgActionsSelection: 0,
|
|
47
|
+
msgActionsOptions: [MSG_ACTION_SEND, MSG_ACTION_COPY, MSG_ACTION_QUESTION, MSG_ACTION_REMOVE],
|
|
37
48
|
};
|
|
38
49
|
|
|
39
50
|
const previousMessages = [];
|
|
40
51
|
const pendingPrompts = [];
|
|
41
52
|
let restored = false;
|
|
42
53
|
let isRequesting = false;
|
|
54
|
+
// AbortController for the current in-flight LLM request
|
|
55
|
+
let currentAbortCtrl = null;
|
|
43
56
|
|
|
44
57
|
function restoreTerminal() {
|
|
45
58
|
if (restored) return;
|
|
46
59
|
restored = true;
|
|
60
|
+
// Disable mouse tracking before exit
|
|
61
|
+
process.stdout.write("\x1b[?1000l\x1b[?1015l\x1b[?1006l");
|
|
47
62
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
48
63
|
process.stdout.write(C.reset + C.showCursor + C.resetBg + C.altOff);
|
|
49
64
|
}
|
|
@@ -67,6 +82,8 @@ export async function cmdSys(args) {
|
|
|
67
82
|
readline.emitKeypressEvents(process.stdin);
|
|
68
83
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
69
84
|
process.stdout.write(C.altOn + C.setBgBlack + C.showCursor + C.bg);
|
|
85
|
+
// Enable xterm mouse button tracking (X10 + SGR extended for wide terminals)
|
|
86
|
+
process.stdout.write("\x1b[?1000h\x1b[?1015h\x1b[?1006h");
|
|
70
87
|
process.once("exit", restoreTerminal);
|
|
71
88
|
process.once("SIGINT", close);
|
|
72
89
|
process.once("SIGTERM", close);
|
|
@@ -75,25 +92,72 @@ export async function cmdSys(args) {
|
|
|
75
92
|
|
|
76
93
|
renderScreen();
|
|
77
94
|
|
|
95
|
+
// Handle raw mouse tracking bytes before readline keypress
|
|
96
|
+
process.stdin.on("data", (chunk) => {
|
|
97
|
+
const raw = typeof chunk === "string" ? chunk : chunk.toString("binary");
|
|
98
|
+
// SGR mouse: ESC [ < Pb ; Px ; Py M/m
|
|
99
|
+
const sgrMatch = raw.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
|
|
100
|
+
if (sgrMatch) {
|
|
101
|
+
const btn = parseInt(sgrMatch[1], 10);
|
|
102
|
+
const col = parseInt(sgrMatch[2], 10) - 1;
|
|
103
|
+
const row = parseInt(sgrMatch[3], 10) - 1;
|
|
104
|
+
const press = sgrMatch[4] === "M";
|
|
105
|
+
if (press && btn === 0) {
|
|
106
|
+
handleMouseClick(col, row, state, pendingPrompts, renderScreen, () => {
|
|
107
|
+
// interrupt callback: abort current request then flush queue
|
|
108
|
+
if (currentAbortCtrl) currentAbortCtrl.abort();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
78
115
|
process.stdin.on("keypress", async (str, key) => {
|
|
79
116
|
if (key.ctrl && key.name === "c") {
|
|
117
|
+
// If a request is running, interrupt it first; second Ctrl-C exits
|
|
118
|
+
if (isRequesting && currentAbortCtrl) {
|
|
119
|
+
currentAbortCtrl.abort();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
80
122
|
close();
|
|
81
123
|
}
|
|
82
124
|
|
|
125
|
+
// Ctrl+I = interrupt current request and immediately send first queued prompt
|
|
126
|
+
if (key.ctrl && key.name === "i" && isRequesting) {
|
|
127
|
+
if (currentAbortCtrl) currentAbortCtrl.abort();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
83
131
|
if (key.ctrl && key.name === "p") {
|
|
84
132
|
state.inCommandPalette = !state.inCommandPalette;
|
|
133
|
+
state.inMsgActions = false;
|
|
85
134
|
resetPalette();
|
|
86
135
|
renderScreen();
|
|
87
136
|
return;
|
|
88
137
|
}
|
|
89
138
|
|
|
90
|
-
if (key.name === "escape"
|
|
91
|
-
if (state.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
139
|
+
if (key.name === "escape") {
|
|
140
|
+
if (state.inMsgActions) {
|
|
141
|
+
state.inMsgActions = false;
|
|
142
|
+
state.msgActionsTarget = null;
|
|
143
|
+
renderScreen();
|
|
144
|
+
return;
|
|
95
145
|
}
|
|
96
|
-
|
|
146
|
+
if (state.inCommandPalette) {
|
|
147
|
+
if (state.paletteState !== "main") {
|
|
148
|
+
resetPalette();
|
|
149
|
+
} else {
|
|
150
|
+
state.inCommandPalette = false;
|
|
151
|
+
}
|
|
152
|
+
renderScreen();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (state.inMsgActions) {
|
|
158
|
+
await handleMsgActionsKey(key, state, pendingPrompts, renderScreen, () => {
|
|
159
|
+
if (currentAbortCtrl) currentAbortCtrl.abort();
|
|
160
|
+
});
|
|
97
161
|
return;
|
|
98
162
|
}
|
|
99
163
|
|
|
@@ -105,14 +169,23 @@ export async function cmdSys(args) {
|
|
|
105
169
|
if (handleScrollKey(key, state, renderScreen)) return;
|
|
106
170
|
|
|
107
171
|
if (isReturnKey(key)) {
|
|
172
|
+
if (isExitCommand(state.inputText)) {
|
|
173
|
+
close();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
108
177
|
if (isRequesting) {
|
|
109
178
|
queuePrompt(state, pendingPrompts, renderScreen);
|
|
110
179
|
return;
|
|
111
180
|
}
|
|
112
181
|
|
|
113
182
|
isRequesting = true;
|
|
114
|
-
await submitPromptQueue(
|
|
183
|
+
await submitPromptQueue(
|
|
184
|
+
pid, state, previousMessages, pendingPrompts, renderScreen, close,
|
|
185
|
+
(ctrl) => { currentAbortCtrl = ctrl; }
|
|
186
|
+
);
|
|
115
187
|
isRequesting = false;
|
|
188
|
+
currentAbortCtrl = null;
|
|
116
189
|
return;
|
|
117
190
|
}
|
|
118
191
|
|
|
@@ -120,10 +193,123 @@ export async function cmdSys(args) {
|
|
|
120
193
|
});
|
|
121
194
|
}
|
|
122
195
|
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Mouse click → Message Actions overlay
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Determine if a click at (col, row) lands on a queued user message bubble,
|
|
202
|
+
* and if so open the Message Actions overlay for it.
|
|
203
|
+
*/
|
|
204
|
+
function handleMouseClick(col, row, state, pendingPrompts, renderScreen, onInterrupt) {
|
|
205
|
+
if (!state.hasStarted) return;
|
|
206
|
+
|
|
207
|
+
// Find the message bubble that was clicked.
|
|
208
|
+
// We look for both queued and regular messages in the transcript.
|
|
209
|
+
// Transcript is rendered from bottom to top in terms of logic,
|
|
210
|
+
// but we'll use a simple heuristic for now.
|
|
211
|
+
const allUserMessages = state.transcript.filter(t => t.type === "user");
|
|
212
|
+
if (allUserMessages.length === 0) return;
|
|
213
|
+
|
|
214
|
+
const { width } = { width: process.stdout.columns || 80 };
|
|
215
|
+
if (col > Math.floor(width * 0.8)) return;
|
|
216
|
+
|
|
217
|
+
// For now, just pick the most recent one if clicked in the main area.
|
|
218
|
+
// In a real app we'd map row to transcript index precisely.
|
|
219
|
+
state.inMsgActions = true;
|
|
220
|
+
state.msgActionsTarget = allUserMessages[allUserMessages.length - 1];
|
|
221
|
+
state.msgActionsSelection = 0;
|
|
222
|
+
|
|
223
|
+
// Filter options: "Send now" only for queued items
|
|
224
|
+
const isQueued = state.msgActionsTarget.meta === "queued";
|
|
225
|
+
state.msgActionsOptions = isQueued
|
|
226
|
+
? [MSG_ACTION_SEND, MSG_ACTION_COPY, MSG_ACTION_QUESTION, MSG_ACTION_REMOVE]
|
|
227
|
+
: [MSG_ACTION_COPY, MSG_ACTION_QUESTION];
|
|
228
|
+
|
|
229
|
+
renderScreen();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Keyboard nav inside the Message Actions overlay */
|
|
233
|
+
async function handleMsgActionsKey(key, state, pendingPrompts, renderScreen, onInterrupt) {
|
|
234
|
+
if (key.name === "up") {
|
|
235
|
+
state.msgActionsSelection = Math.max(0, state.msgActionsSelection - 1);
|
|
236
|
+
renderScreen();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (key.name === "down") {
|
|
240
|
+
state.msgActionsSelection = Math.min(
|
|
241
|
+
state.msgActionsOptions.length - 1,
|
|
242
|
+
state.msgActionsSelection + 1
|
|
243
|
+
);
|
|
244
|
+
renderScreen();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (key.name !== "return") {
|
|
248
|
+
renderScreen();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const selected = state.msgActionsOptions[state.msgActionsSelection];
|
|
253
|
+
const target = state.msgActionsTarget;
|
|
254
|
+
|
|
255
|
+
// Close overlay first
|
|
256
|
+
state.inMsgActions = false;
|
|
257
|
+
state.msgActionsTarget = null;
|
|
258
|
+
|
|
259
|
+
if (selected === MSG_ACTION_REMOVE) {
|
|
260
|
+
// Remove from pendingPrompts and transcript
|
|
261
|
+
const idx = pendingPrompts.findIndex((p) => p.text === target?.text);
|
|
262
|
+
if (idx >= 0) pendingPrompts.splice(idx, 1);
|
|
263
|
+
const tidx = state.transcript.indexOf(target);
|
|
264
|
+
if (tidx >= 0) state.transcript.splice(tidx, 1);
|
|
265
|
+
renderScreen();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (selected === MSG_ACTION_COPY) {
|
|
270
|
+
// Best-effort clipboard via pbcopy (macOS) / xclip (Linux)
|
|
271
|
+
try {
|
|
272
|
+
const { execSync } = await import("node:child_process");
|
|
273
|
+
const cmd = process.platform === "darwin" ? "pbcopy" : "xclip -selection clipboard";
|
|
274
|
+
execSync(cmd, { input: target?.text || "", stdio: ["pipe", "ignore", "ignore"] });
|
|
275
|
+
} catch {}
|
|
276
|
+
state.transcript.push({ type: "status", text: "Copied to clipboard" });
|
|
277
|
+
renderScreen();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (selected === MSG_ACTION_QUESTION) {
|
|
282
|
+
const text = target?.text || "";
|
|
283
|
+
state.inputText = `Pregunta sobre esto: "${text.slice(0, 50)}${text.length > 50 ? "..." : ""}"\n\n`;
|
|
284
|
+
state.cursorIndex = state.inputText.length;
|
|
285
|
+
renderScreen();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (selected === MSG_ACTION_SEND) {
|
|
290
|
+
// Promote the queued item to front of queue, then interrupt current request
|
|
291
|
+
const idx = pendingPrompts.findIndex((p) => p.text === target?.text);
|
|
292
|
+
if (idx > 0) {
|
|
293
|
+
const [entry] = pendingPrompts.splice(idx, 1);
|
|
294
|
+
pendingPrompts.unshift(entry);
|
|
295
|
+
}
|
|
296
|
+
// Signal the interrupt — the running submitPromptQueue will pick it up
|
|
297
|
+
onInterrupt();
|
|
298
|
+
renderScreen();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
renderScreen();
|
|
303
|
+
}
|
|
304
|
+
|
|
123
305
|
export function isReturnKey(key) {
|
|
124
306
|
return key?.name === "return" || key?.name === "enter";
|
|
125
307
|
}
|
|
126
308
|
|
|
309
|
+
export function isExitCommand(text) {
|
|
310
|
+
return /^(exit|quit)$/i.test(String(text || "").trim());
|
|
311
|
+
}
|
|
312
|
+
|
|
127
313
|
export function handleScrollKey(key, state, renderScreen) {
|
|
128
314
|
if (!state.hasStarted || !key) return false;
|
|
129
315
|
const pageSize = key.name === "pageup" || key.name === "pagedown" ? 8 : 3;
|
|
@@ -329,11 +515,15 @@ function queuePrompt(state, pendingPrompts, renderScreen) {
|
|
|
329
515
|
renderScreen();
|
|
330
516
|
}
|
|
331
517
|
|
|
332
|
-
async function submitPromptQueue(
|
|
518
|
+
async function submitPromptQueue(
|
|
519
|
+
pid, state, previousMessages, pendingPrompts, renderScreen, close,
|
|
520
|
+
setAbortCtrl = () => {}
|
|
521
|
+
) {
|
|
333
522
|
const firstText = state.inputText.trim();
|
|
334
523
|
if (!firstText) return;
|
|
335
|
-
if (
|
|
524
|
+
if (isExitCommand(firstText)) {
|
|
336
525
|
close();
|
|
526
|
+
return;
|
|
337
527
|
}
|
|
338
528
|
|
|
339
529
|
state.hasStarted = true;
|
|
@@ -343,20 +533,29 @@ async function submitPromptQueue(pid, state, previousMessages, pendingPrompts, r
|
|
|
343
533
|
|
|
344
534
|
const firstItem = { type: "user", text: firstText };
|
|
345
535
|
state.transcript.push(firstItem);
|
|
346
|
-
await runPrompt(
|
|
536
|
+
await runPrompt(
|
|
537
|
+
pid, state, previousMessages, renderScreen, firstText, firstItem, setAbortCtrl
|
|
538
|
+
);
|
|
347
539
|
|
|
348
540
|
while (pendingPrompts.length > 0) {
|
|
349
541
|
const queued = pendingPrompts.shift();
|
|
350
542
|
delete queued.item.meta;
|
|
351
|
-
await runPrompt(
|
|
543
|
+
await runPrompt(
|
|
544
|
+
pid, state, previousMessages, renderScreen, queued.text, queued.item, setAbortCtrl
|
|
545
|
+
);
|
|
352
546
|
}
|
|
353
547
|
}
|
|
354
548
|
|
|
355
|
-
async function runPrompt(
|
|
549
|
+
async function runPrompt(
|
|
550
|
+
pid, state, previousMessages, renderScreen, text, userItem,
|
|
551
|
+
setAbortCtrl = () => {}
|
|
552
|
+
) {
|
|
356
553
|
appendLiveItem(state, { type: "status", text: "Thinking...", active: true });
|
|
357
554
|
renderScreen();
|
|
358
555
|
|
|
359
556
|
const startTime = Date.now();
|
|
557
|
+
const abortCtrl = http.createAbortController();
|
|
558
|
+
setAbortCtrl(abortCtrl);
|
|
360
559
|
|
|
361
560
|
try {
|
|
362
561
|
const cwd = process.cwd();
|
|
@@ -372,28 +571,49 @@ async function runPrompt(pid, state, previousMessages, renderScreen, text, userI
|
|
|
372
571
|
};
|
|
373
572
|
|
|
374
573
|
let result;
|
|
574
|
+
let interrupted = false;
|
|
375
575
|
try {
|
|
376
576
|
result = await http.streamPost(
|
|
377
577
|
`/projects/${pid}/super-agent/chat/stream`,
|
|
378
578
|
body,
|
|
379
|
-
(event) => handleProgressEvent(event, state, renderScreen)
|
|
579
|
+
(event) => handleProgressEvent(event, state, renderScreen),
|
|
580
|
+
{ signal: abortCtrl.signal }
|
|
380
581
|
);
|
|
381
582
|
} catch (e) {
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
appendLiveItem(state, {
|
|
583
|
+
if (abortCtrl.signal.aborted) {
|
|
584
|
+
// Interrupted by user — show notice and continue to next queued prompt
|
|
585
|
+
interrupted = true;
|
|
586
|
+
removeStatus(state);
|
|
587
|
+
appendLiveItem(state, {
|
|
588
|
+
type: "status",
|
|
589
|
+
text: `\u26a1 Interrupted — ${text.slice(0, 60)}${text.length > 60 ? "\u2026" : ""}`,
|
|
590
|
+
});
|
|
591
|
+
} else if (e.status !== 404) {
|
|
592
|
+
throw e;
|
|
593
|
+
} else {
|
|
594
|
+
result = await http.post(
|
|
595
|
+
`/projects/${pid}/super-agent/chat`, body,
|
|
596
|
+
{ signal: abortCtrl.signal }
|
|
597
|
+
);
|
|
598
|
+
removeStatus(state);
|
|
599
|
+
for (const trace of result.trace || []) {
|
|
600
|
+
appendLiveItem(state, { type: "tool", trace });
|
|
601
|
+
}
|
|
387
602
|
}
|
|
388
603
|
}
|
|
389
604
|
|
|
390
|
-
|
|
605
|
+
if (!interrupted && result) {
|
|
606
|
+
completeSuperAgentResult(result, text, startTime, state, previousMessages);
|
|
607
|
+
}
|
|
391
608
|
} catch (e) {
|
|
392
|
-
|
|
393
|
-
|
|
609
|
+
if (!abortCtrl.signal.aborted) {
|
|
610
|
+
removeStatus(state);
|
|
611
|
+
appendLiveItem(state, { type: "error", text: e.message });
|
|
612
|
+
}
|
|
394
613
|
}
|
|
395
614
|
|
|
396
615
|
if (userItem) delete userItem.meta;
|
|
616
|
+
setAbortCtrl(null);
|
|
397
617
|
renderScreen();
|
|
398
618
|
}
|
|
399
619
|
|
|
@@ -424,6 +644,14 @@ function handleProgressEvent(event, state, renderScreen) {
|
|
|
424
644
|
return;
|
|
425
645
|
}
|
|
426
646
|
|
|
647
|
+
if (event.type === "model_retry") {
|
|
648
|
+
const status = [...state.transcript].reverse().find((item) => item?.type === "status" && item?.active);
|
|
649
|
+
if (status) status.text = "Retrying with tool fallback...";
|
|
650
|
+
else appendLiveItem(state, { type: "status", text: "Retrying with tool fallback...", active: true });
|
|
651
|
+
renderScreen();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
427
655
|
if (event.type === "assistant_text" && event.text) {
|
|
428
656
|
removeStatus(state);
|
|
429
657
|
appendLiveItem(state, {
|
|
@@ -4,8 +4,14 @@ export async function cmdTelegramSend(args) {
|
|
|
4
4
|
const text = args._[0];
|
|
5
5
|
if (!text) throw new Error("apx telegram send: missing <text>");
|
|
6
6
|
const chat_id = args.flags.chat === true ? undefined : args.flags.chat;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// --interrupt / --force: send immediately bypassing any pending agent queue
|
|
8
|
+
const interrupt = !!(args.flags.interrupt || args.flags.force);
|
|
9
|
+
const result = await http.post("/telegram/send", { chat_id, text, interrupt });
|
|
10
|
+
if (interrupt) {
|
|
11
|
+
console.log(`⚡ sent (interrupt, message_id=${result.message_id})`);
|
|
12
|
+
} else {
|
|
13
|
+
console.log(`✅ sent (message_id=${result.message_id})`);
|
|
14
|
+
}
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
export async function cmdTelegramStatus() {
|
package/src/cli/http.js
CHANGED
|
@@ -75,6 +75,7 @@ async function request(method, path, body, opts = {}) {
|
|
|
75
75
|
method,
|
|
76
76
|
headers: body ? { "content-type": "application/json" } : {},
|
|
77
77
|
body: body ? JSON.stringify(body) : undefined,
|
|
78
|
+
signal: opts.signal,
|
|
78
79
|
});
|
|
79
80
|
const text = await res.text();
|
|
80
81
|
let json;
|
|
@@ -101,6 +102,7 @@ async function streamRequest(method, path, body, onEvent, opts = {}) {
|
|
|
101
102
|
method,
|
|
102
103
|
headers: body ? { "content-type": "application/json" } : {},
|
|
103
104
|
body: body ? JSON.stringify(body) : undefined,
|
|
105
|
+
signal: opts.signal,
|
|
104
106
|
});
|
|
105
107
|
|
|
106
108
|
if (!res.ok) {
|
|
@@ -121,10 +123,21 @@ async function streamRequest(method, path, body, onEvent, opts = {}) {
|
|
|
121
123
|
let buffer = "";
|
|
122
124
|
let finalResult = null;
|
|
123
125
|
|
|
126
|
+
// Register abort handler to cancel the reader
|
|
127
|
+
if (opts.signal) {
|
|
128
|
+
opts.signal.addEventListener("abort", () => reader.cancel().catch(() => {}), { once: true });
|
|
129
|
+
}
|
|
130
|
+
|
|
124
131
|
while (true) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
let chunk;
|
|
133
|
+
try {
|
|
134
|
+
chunk = await reader.read();
|
|
135
|
+
} catch (e) {
|
|
136
|
+
// AbortError or cancel — treat as clean end
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if (chunk.done) break;
|
|
140
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
128
141
|
const lines = buffer.split(/\r?\n/);
|
|
129
142
|
buffer = lines.pop() || "";
|
|
130
143
|
for (const line of lines) {
|
|
@@ -138,10 +151,12 @@ async function streamRequest(method, path, body, onEvent, opts = {}) {
|
|
|
138
151
|
|
|
139
152
|
buffer += decoder.decode();
|
|
140
153
|
if (buffer.trim()) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
154
|
+
try {
|
|
155
|
+
const event = JSON.parse(buffer);
|
|
156
|
+
if (event.type === "final") finalResult = event.result;
|
|
157
|
+
if (event.type === "error") throw new Error(event.error || "stream error");
|
|
158
|
+
await onEvent?.(event);
|
|
159
|
+
} catch {}
|
|
145
160
|
}
|
|
146
161
|
|
|
147
162
|
return finalResult;
|
|
@@ -156,4 +171,6 @@ export const http = {
|
|
|
156
171
|
delete: (p, opts) => request("DELETE", p, undefined, opts),
|
|
157
172
|
baseUrl,
|
|
158
173
|
ping,
|
|
174
|
+
/** Create a fresh AbortController for cancelling in-flight requests. */
|
|
175
|
+
createAbortController: () => new AbortController(),
|
|
159
176
|
};
|
package/src/cli/index.js
CHANGED
|
@@ -607,9 +607,16 @@ const HELP_TOPICS = new Map(Object.entries({
|
|
|
607
607
|
"telegram send": topic({
|
|
608
608
|
title: "apx telegram send",
|
|
609
609
|
summary: "Send a Telegram message through the configured bridge.",
|
|
610
|
-
usage: ["apx telegram send \"<text>\" [--chat <id>]"],
|
|
611
|
-
options: [
|
|
612
|
-
|
|
610
|
+
usage: ["apx telegram send \"<text>\" [--chat <id>] [--interrupt]"],
|
|
611
|
+
options: [
|
|
612
|
+
["--chat <id>", "Override configured chat id."],
|
|
613
|
+
["--interrupt", "Send immediately, bypassing any pending agent queue (alias: --force)."],
|
|
614
|
+
["--force", "Alias for --interrupt."],
|
|
615
|
+
],
|
|
616
|
+
examples: [
|
|
617
|
+
"apx telegram send \"Deploy finished\" --chat 123456",
|
|
618
|
+
"apx telegram send \"Urgent!\" --interrupt",
|
|
619
|
+
],
|
|
613
620
|
}),
|
|
614
621
|
"telegram status": topic({
|
|
615
622
|
title: "apx telegram status",
|
package/src/cli/postinstall.js
CHANGED
|
@@ -1,10 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Runs automatically after `npm install -g apx`.
|
|
3
|
-
//
|
|
4
|
-
|
|
2
|
+
// Runs automatically after `npm install -g apx` and `npm update -g apx`.
|
|
3
|
+
//
|
|
4
|
+
// Two-step process:
|
|
5
|
+
// 1. Refresh the bundled `apc-context` skill from the canonical APC repo.
|
|
6
|
+
// APC is a living standard — we always want the latest copy at install
|
|
7
|
+
// time. If the network call fails, the bundled snapshot that ships
|
|
8
|
+
// with the npm tarball is used.
|
|
9
|
+
// 2. Propagate APX + APC skills (+ runtime docs) into every global skill
|
|
10
|
+
// directory (~/.claude/skills, ~/.cursor/skills, ~/.codex/skills,
|
|
11
|
+
// ~/.agents/skills).
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
5
14
|
import os from "node:os";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { installGlobalSkills } from "../core/scaffold.js";
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
|
|
20
|
+
const APC_SKILL_LOCAL = path.join(PACKAGE_ROOT, "skills", "apc-context", "SKILL.md");
|
|
21
|
+
const APC_SKILL_REMOTE =
|
|
22
|
+
"https://raw.githubusercontent.com/agentprojectcontext/agentprojectcontext/main/skills/apc-context/SKILL.md";
|
|
23
|
+
|
|
24
|
+
async function refreshApcSkill() {
|
|
25
|
+
if (process.env.APX_SKIP_SKILL_REFRESH) return { status: "skipped" };
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const fetchImpl = globalThis.fetch || (await import("node-fetch")).default;
|
|
29
|
+
const ac = new AbortController();
|
|
30
|
+
const timer = setTimeout(() => ac.abort(), 5000);
|
|
31
|
+
|
|
32
|
+
const res = await fetchImpl(APC_SKILL_REMOTE, { signal: ac.signal });
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
|
|
35
|
+
if (!res.ok) return { status: "fallback", reason: `HTTP ${res.status}` };
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
|
|
38
|
+
if (!text.startsWith("---") || !/name:\s*apc-context/.test(text)) {
|
|
39
|
+
return { status: "fallback", reason: "remote payload not a SKILL.md" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fs.mkdirSync(path.dirname(APC_SKILL_LOCAL), { recursive: true });
|
|
43
|
+
fs.writeFileSync(APC_SKILL_LOCAL, text, "utf8");
|
|
44
|
+
return { status: "refreshed" };
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return { status: "fallback", reason: err?.message || String(err) };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
6
49
|
|
|
7
50
|
try {
|
|
51
|
+
const refresh = await refreshApcSkill();
|
|
52
|
+
|
|
8
53
|
const results = installGlobalSkills();
|
|
9
54
|
if (results.length === 0) process.exit(0);
|
|
10
55
|
|
|
@@ -19,7 +64,12 @@ try {
|
|
|
19
64
|
for (const [skill, dirs] of Object.entries(bySkill)) {
|
|
20
65
|
console.log(` ${skill.padEnd(14)} → ${dirs.join(", ")}`);
|
|
21
66
|
}
|
|
67
|
+
if (refresh.status === "refreshed") {
|
|
68
|
+
console.log(" apc-context refreshed from agentprojectcontext/agentprojectcontext@main");
|
|
69
|
+
} else if (refresh.status === "fallback") {
|
|
70
|
+
console.log(` apc-context: using bundled snapshot (${refresh.reason})`);
|
|
71
|
+
}
|
|
22
72
|
console.log("");
|
|
23
73
|
} catch {
|
|
24
|
-
// Non-fatal — don't break the install
|
|
74
|
+
// Non-fatal — don't break the install.
|
|
25
75
|
}
|
|
@@ -173,6 +173,12 @@ function renderPromptBlock(state, chatWidth) {
|
|
|
173
173
|
const hotkeys =
|
|
174
174
|
C.bold + C.text + "tab" + C.normal + C.muted + " agents " +
|
|
175
175
|
C.bold + C.text + "ctrl+p" + C.normal + C.muted + " commands " +
|
|
176
|
+
(state.hasStarted && state.transcript?.some((t) => t.type === "user" && t.meta === "queued")
|
|
177
|
+
? C.bold + C.warning + "ctrl+i" + C.normal + C.muted + " interrupt "
|
|
178
|
+
: "") +
|
|
179
|
+
(state.hasStarted && state.transcript?.length > 0
|
|
180
|
+
? C.bold + C.text + "click" + C.normal + C.muted + " actions "
|
|
181
|
+
: "") +
|
|
176
182
|
C.bold + C.text + "enter" + C.normal + C.muted + " send";
|
|
177
183
|
const hotkeyLeft = Math.max(left, left + boxWidth - visible(hotkeys));
|
|
178
184
|
writeAt(top + 5, hotkeyLeft, hotkeys, visible(hotkeys), C.bg);
|
|
@@ -276,7 +282,11 @@ function transcriptLines(transcript, width) {
|
|
|
276
282
|
addLine(lines, margin + C.primary + "┃" + C.panel + " " + C.text + padAnsi(chunk, inner), C.bg);
|
|
277
283
|
}
|
|
278
284
|
if (item.meta) {
|
|
279
|
-
|
|
285
|
+
// Show QUEUED badge with distinct highlight color
|
|
286
|
+
const badgeText = item.meta === "queued"
|
|
287
|
+
? C.warning + C.bold + "QUEUED" + C.normal + C.muted + " click to send/remove"
|
|
288
|
+
: C.muted + item.meta;
|
|
289
|
+
addLine(lines, margin + C.primary + "┃" + C.panel + " " + badgeText + padAnsi("", Math.max(0, inner - (item.meta === "queued" ? 28 : visible(item.meta)))), C.bg);
|
|
280
290
|
}
|
|
281
291
|
addLine(lines, margin + C.primary + "┃" + C.panel + " " + " ".repeat(inner), C.bg);
|
|
282
292
|
continue;
|
|
@@ -301,7 +311,23 @@ function transcriptLines(transcript, width) {
|
|
|
301
311
|
}
|
|
302
312
|
|
|
303
313
|
if (item.type === "tool") {
|
|
304
|
-
|
|
314
|
+
const trace = item.trace || {};
|
|
315
|
+
const isQuestion = trace.tool === "ask_questions";
|
|
316
|
+
const label = isQuestion ? C.warning + C.bold + "QUESTION" : C.muted + "TOOL";
|
|
317
|
+
const name = isQuestion ? "" : C.text + trace.tool;
|
|
318
|
+
|
|
319
|
+
if (isQuestion && trace.args?.questions) {
|
|
320
|
+
addLine(lines, "", C.bg);
|
|
321
|
+
addLine(lines, margin + label + C.muted + " (…)" + C.bg, C.bg);
|
|
322
|
+
for (const q of trace.args.questions) {
|
|
323
|
+
const qWrapped = wrapText(`• ${q}`, inner - 2);
|
|
324
|
+
for (const line of qWrapped) {
|
|
325
|
+
addLine(lines, margin + C.primary + "┃ " + C.text + padAnsi(line, inner - 2), C.bg);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
addLine(lines, margin + label + " " + name + C.bg, C.bg);
|
|
330
|
+
}
|
|
305
331
|
continue;
|
|
306
332
|
}
|
|
307
333
|
|
|
@@ -408,6 +434,36 @@ function renderPaletteOverlay(state) {
|
|
|
408
434
|
);
|
|
409
435
|
}
|
|
410
436
|
|
|
437
|
+
function renderMsgActionsOverlay(state) {
|
|
438
|
+
const { width, height } = terminalSize();
|
|
439
|
+
const title = "Message Actions";
|
|
440
|
+
const opts = state.msgActionsOptions;
|
|
441
|
+
const preview = fit(state.msgActionsTarget?.text || "", 42);
|
|
442
|
+
const boxWidth = Math.min(62, Math.max(title.length + 8, preview.length + 6, ...opts.map((x) => visible(x) + 8)));
|
|
443
|
+
const boxHeight = opts.length + 5;
|
|
444
|
+
const left = centerLeft(width, boxWidth);
|
|
445
|
+
const top = Math.max(1, Math.floor((height - boxHeight) / 2));
|
|
446
|
+
|
|
447
|
+
writeAt(top, left, C.text + C.bold + " " + title + C.normal, boxWidth, C.panel);
|
|
448
|
+
writeAt(top + 1, left, C.muted + " " + C.italic + preview + C.noItalic, boxWidth, C.panel);
|
|
449
|
+
writeAt(top + 2, left, C.dim + "▀".repeat(boxWidth), boxWidth, C.panel);
|
|
450
|
+
for (let i = 0; i < opts.length; i++) {
|
|
451
|
+
const active = i === state.msgActionsSelection;
|
|
452
|
+
const marker = active ? "›" : " ";
|
|
453
|
+
const bg = active ? C.panel2 : C.panel;
|
|
454
|
+
const fg = active ? C.primary + C.bold : C.text;
|
|
455
|
+
writeAt(top + 3 + i, left, fg + ` ${marker} ${opts[i]}` + C.normal, boxWidth, bg);
|
|
456
|
+
}
|
|
457
|
+
writeAt(top + 3 + opts.length, left, C.dim + "▄".repeat(boxWidth), boxWidth, C.panel);
|
|
458
|
+
writeAt(
|
|
459
|
+
top + 4 + opts.length,
|
|
460
|
+
left,
|
|
461
|
+
C.muted + "↑↓ select " + C.text + C.bold + "enter" + C.normal + C.muted + " choose " + C.text + C.bold + "esc" + C.normal + C.muted + " close",
|
|
462
|
+
boxWidth,
|
|
463
|
+
C.bg
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
411
467
|
export function renderTerminalChat(state) {
|
|
412
468
|
clearFull();
|
|
413
469
|
const { width, height } = terminalSize();
|
|
@@ -424,5 +480,6 @@ export function renderTerminalChat(state) {
|
|
|
424
480
|
const cursor = renderPromptBlock(state, chatWidth);
|
|
425
481
|
|
|
426
482
|
if (state.inCommandPalette) renderPaletteOverlay(state);
|
|
427
|
-
if (
|
|
483
|
+
if (state.inMsgActions) renderMsgActionsOverlay(state);
|
|
484
|
+
if (!state.inCommandPalette && !state.inMsgActions) moveTo(cursor.row, cursor.col);
|
|
428
485
|
}
|