@agentprojectcontext/apx 1.66.0 → 1.68.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 +3 -2
- package/skills/apx/SKILL.md +3 -0
- package/src/core/agent/index.js +2 -0
- package/src/core/agent/judge.js +174 -0
- package/src/core/agent/model-router.js +107 -5
- package/src/core/agent/prompts/modes/code-build.md +1 -1
- package/src/core/agent/run-agent.js +149 -12
- package/src/core/agent/security.js +97 -0
- package/src/core/agent/stuck-detector.js +89 -0
- package/src/core/agent/super-agent.js +58 -17
- package/src/core/agent/tools/handlers/run-subagent.js +117 -0
- package/src/core/agent/tools/helpers.js +11 -1
- package/src/core/agent/tools/names.js +2 -0
- package/src/core/agent/tools/registry.js +10 -0
- package/src/core/artifacts/preview.js +392 -0
- package/src/core/artifacts/tunnel.js +169 -0
- package/src/core/config/index.js +61 -0
- package/src/core/config/redact.js +44 -0
- package/src/core/config/secret-values.js +132 -0
- package/src/core/engines/mock.js +15 -1
- package/src/core/logging.js +10 -3
- package/src/core/memory/compactor.js +65 -56
- package/src/core/memory/summarizer.js +125 -0
- package/src/core/stores/conversations-compactor.js +24 -31
- package/src/host/daemon/api/admin-config.js +5 -0
- package/src/host/daemon/api/artifact-preview.js +82 -0
- package/src/host/daemon/api/config.js +17 -5
- package/src/host/daemon/api/sessions.js +9 -0
- package/src/host/daemon/api/web.js +1 -1
- package/src/host/daemon/api.js +2 -0
- package/src/host/daemon/index.js +16 -1
- package/src/interfaces/acp/index.js +363 -0
- package/src/interfaces/acp/jsonrpc.js +180 -0
- package/src/interfaces/acp/session.js +205 -0
- package/src/interfaces/cli/commands/acp.js +10 -0
- package/src/interfaces/cli/commands/artifact.js +115 -0
- package/src/interfaces/cli/index.js +74 -0
- package/src/interfaces/web/dist/assets/index-D4BmWoDM.css +1 -0
- package/src/interfaces/web/dist/assets/index-vwd6yQVw.js +803 -0
- package/src/interfaces/web/dist/assets/index-vwd6yQVw.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +9 -9
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +145 -2
- package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +46 -31
- package/src/interfaces/web/src/components/config/project-config-sections.ts +9 -11
- package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +162 -0
- package/src/interfaces/web/src/components/settings/RoutingPanel.tsx +236 -0
- package/src/interfaces/web/src/i18n/en.ts +53 -0
- package/src/interfaces/web/src/i18n/es.ts +53 -0
- package/src/interfaces/web/src/lib/api/artifacts.ts +38 -0
- package/src/interfaces/web/src/lib/api/sessions.ts +2 -1
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +50 -60
- package/src/interfaces/web/src/screens/base/ModelsTab.tsx +4 -2
- package/src/interfaces/web/src/screens/base/SessionsTab.tsx +11 -5
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +110 -25
- package/src/interfaces/web/src/screens/project/MemoriesTab.tsx +7 -128
- package/src/interfaces/web/src/screens/project/Overview.tsx +3 -2
- package/src/interfaces/web/src/types/daemon.ts +16 -0
- package/src/interfaces/web/dist/assets/index-BuII-tAi.css +0 -1
- package/src/interfaces/web/dist/assets/index-YmMRG--4.js +0 -778
- package/src/interfaces/web/dist/assets/index-YmMRG--4.js.map +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Newline-delimited JSON-RPC 2.0 connection — the ACP stdio framing.
|
|
2
|
+
//
|
|
3
|
+
// Per the ACP transport spec (agentclientprotocol.com, transports.mdx):
|
|
4
|
+
// messages are individual JSON-RPC requests/notifications/responses, UTF-8,
|
|
5
|
+
// delimited by "\n", never containing embedded newlines. Both peers can act
|
|
6
|
+
// as caller and callee (the agent calls `session/request_permission` on the
|
|
7
|
+
// client), so this connection is symmetric: it dispatches incoming requests
|
|
8
|
+
// to registered handlers AND tracks ids of our own outgoing requests.
|
|
9
|
+
//
|
|
10
|
+
// Hand-rolled on purpose — the repo rule is no new npm dependencies, and the
|
|
11
|
+
// framing is small enough that a library would cost more than it saves.
|
|
12
|
+
|
|
13
|
+
export const JSONRPC_ERROR_CODES = Object.freeze({
|
|
14
|
+
PARSE_ERROR: -32700,
|
|
15
|
+
INVALID_REQUEST: -32600,
|
|
16
|
+
METHOD_NOT_FOUND: -32601,
|
|
17
|
+
INVALID_PARAMS: -32602,
|
|
18
|
+
INTERNAL_ERROR: -32603,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export class JsonRpcError extends Error {
|
|
22
|
+
constructor(code, message, data) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.code = code;
|
|
25
|
+
if (data !== undefined) this.data = data;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class JsonRpcConnection {
|
|
30
|
+
/**
|
|
31
|
+
* @param {{ input: import("node:stream").Readable,
|
|
32
|
+
* output: import("node:stream").Writable,
|
|
33
|
+
* onError?: (err: Error) => void }} opts
|
|
34
|
+
*/
|
|
35
|
+
constructor({ input, output, onError = null }) {
|
|
36
|
+
this.output = output;
|
|
37
|
+
this.onError = onError;
|
|
38
|
+
this.handlers = new Map();
|
|
39
|
+
this.pending = new Map(); // id → {resolve, reject} for our outgoing requests
|
|
40
|
+
this.nextId = 0;
|
|
41
|
+
this.buffer = "";
|
|
42
|
+
this.closed = false;
|
|
43
|
+
this._closeResolvers = [];
|
|
44
|
+
|
|
45
|
+
input.setEncoding?.("utf8");
|
|
46
|
+
input.on("data", (chunk) => this._onData(String(chunk)));
|
|
47
|
+
input.on("end", () => this._close());
|
|
48
|
+
input.on("close", () => this._close());
|
|
49
|
+
input.on("error", () => this._close());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Register a handler for an incoming method (request or notification). */
|
|
53
|
+
method(name, handler) {
|
|
54
|
+
this.handlers.set(name, handler);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolves when the peer closes its side of the pipe. */
|
|
59
|
+
whenClosed() {
|
|
60
|
+
if (this.closed) return Promise.resolve();
|
|
61
|
+
return new Promise((resolve) => this._closeResolvers.push(resolve));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Send a one-way notification to the peer. */
|
|
65
|
+
notify(method, params) {
|
|
66
|
+
this._send({ jsonrpc: "2.0", method, params });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Send a request to the peer and await its response. */
|
|
70
|
+
request(method, params) {
|
|
71
|
+
const id = ++this.nextId;
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
if (this.closed) return reject(new Error("connection closed"));
|
|
74
|
+
this.pending.set(id, { resolve, reject });
|
|
75
|
+
this._send({ jsonrpc: "2.0", id, method, params });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_close() {
|
|
80
|
+
if (this.closed) return;
|
|
81
|
+
this.closed = true;
|
|
82
|
+
for (const { reject } of this.pending.values()) {
|
|
83
|
+
reject(new Error("connection closed"));
|
|
84
|
+
}
|
|
85
|
+
this.pending.clear();
|
|
86
|
+
for (const resolve of this._closeResolvers) resolve();
|
|
87
|
+
this._closeResolvers = [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_send(msg) {
|
|
91
|
+
if (this.closed) return;
|
|
92
|
+
try {
|
|
93
|
+
this.output.write(JSON.stringify(msg) + "\n");
|
|
94
|
+
} catch (e) {
|
|
95
|
+
this.onError?.(e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_onData(text) {
|
|
100
|
+
this.buffer += text;
|
|
101
|
+
let idx;
|
|
102
|
+
while ((idx = this.buffer.indexOf("\n")) !== -1) {
|
|
103
|
+
const line = this.buffer.slice(0, idx).trim();
|
|
104
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
105
|
+
if (!line) continue;
|
|
106
|
+
let msg;
|
|
107
|
+
try {
|
|
108
|
+
msg = JSON.parse(line);
|
|
109
|
+
} catch {
|
|
110
|
+
this._send({
|
|
111
|
+
jsonrpc: "2.0",
|
|
112
|
+
id: null,
|
|
113
|
+
error: { code: JSONRPC_ERROR_CODES.PARSE_ERROR, message: "parse error" },
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Fire-and-forget: a long-running request (session/prompt) must not
|
|
118
|
+
// block later frames — `session/cancel` has to be processed while the
|
|
119
|
+
// prompt handler is still awaiting the daemon stream.
|
|
120
|
+
this._dispatch(msg).catch((e) => this.onError?.(e));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async _dispatch(msg) {
|
|
125
|
+
if (!msg || typeof msg !== "object") return;
|
|
126
|
+
|
|
127
|
+
if (typeof msg.method === "string") {
|
|
128
|
+
const hasId = msg.id !== undefined && msg.id !== null;
|
|
129
|
+
const handler = this.handlers.get(msg.method);
|
|
130
|
+
if (!handler) {
|
|
131
|
+
if (hasId) {
|
|
132
|
+
this._send({
|
|
133
|
+
jsonrpc: "2.0",
|
|
134
|
+
id: msg.id,
|
|
135
|
+
error: {
|
|
136
|
+
code: JSONRPC_ERROR_CODES.METHOD_NOT_FOUND,
|
|
137
|
+
message: `method not found: ${msg.method}`,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const result = await handler(msg.params ?? {});
|
|
145
|
+
if (hasId) this._send({ jsonrpc: "2.0", id: msg.id, result: result ?? null });
|
|
146
|
+
} catch (e) {
|
|
147
|
+
if (hasId) {
|
|
148
|
+
this._send({
|
|
149
|
+
jsonrpc: "2.0",
|
|
150
|
+
id: msg.id,
|
|
151
|
+
error: {
|
|
152
|
+
code: typeof e?.code === "number" ? e.code : JSONRPC_ERROR_CODES.INTERNAL_ERROR,
|
|
153
|
+
message: e?.message || "internal error",
|
|
154
|
+
...(e?.data !== undefined ? { data: e.data } : {}),
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
this.onError?.(e);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Response to one of our outgoing requests.
|
|
165
|
+
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
166
|
+
const { resolve, reject } = this.pending.get(msg.id);
|
|
167
|
+
this.pending.delete(msg.id);
|
|
168
|
+
if (msg.error) {
|
|
169
|
+
reject(
|
|
170
|
+
Object.assign(new Error(msg.error.message || "remote error"), {
|
|
171
|
+
code: msg.error.code,
|
|
172
|
+
data: msg.error.data,
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
resolve(msg.result);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// ACP session plumbing — daemon HTTP client, project resolution for a
|
|
2
|
+
// session's cwd, and the small mapping helpers between APX daemon stream
|
|
3
|
+
// events and ACP wire shapes.
|
|
4
|
+
//
|
|
5
|
+
// The daemon client is deliberately NOT the CLI's http.js: that module pins
|
|
6
|
+
// its base URL to env vars at import time, while tests (and future embeddings)
|
|
7
|
+
// need a per-instance {baseUrl, token}. The NDJSON reader mirrors
|
|
8
|
+
// src/interfaces/cli/http.js streamRequest so both surfaces parse the daemon
|
|
9
|
+
// stream identically.
|
|
10
|
+
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { findApfRoot } from "#core/apc/parser.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Minimal daemon HTTP client bound to an injected base URL + token.
|
|
16
|
+
* `token` may be a string or a () => string (re-read per request so a daemon
|
|
17
|
+
* restart with a rotated token keeps working mid-session).
|
|
18
|
+
*/
|
|
19
|
+
export function createDaemonClient({ baseUrl, token = "", ensureReady = null }) {
|
|
20
|
+
const authHeaders = () => {
|
|
21
|
+
const t = typeof token === "function" ? token() : token;
|
|
22
|
+
return t ? { authorization: `Bearer ${t}` } : {};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function request(method, p, body) {
|
|
26
|
+
if (ensureReady) await ensureReady();
|
|
27
|
+
const res = await fetch(`${baseUrl}${p}`, {
|
|
28
|
+
method,
|
|
29
|
+
headers: {
|
|
30
|
+
...(body ? { "content-type": "application/json" } : {}),
|
|
31
|
+
...authHeaders(),
|
|
32
|
+
},
|
|
33
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
34
|
+
});
|
|
35
|
+
const text = await res.text();
|
|
36
|
+
let json = null;
|
|
37
|
+
try {
|
|
38
|
+
json = text ? JSON.parse(text) : null;
|
|
39
|
+
} catch {
|
|
40
|
+
/* non-JSON body — handled below */
|
|
41
|
+
}
|
|
42
|
+
if (!res.ok) throw new Error(json?.error || `${method} ${p} → ${res.status}`);
|
|
43
|
+
return json;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* POST to an NDJSON stream endpoint. Awaits `onEvent` per event so a
|
|
48
|
+
* confirmation round-trip naturally back-pressures the stream. Returns the
|
|
49
|
+
* `result` of the `{type:"final"}` event, or null when aborted early.
|
|
50
|
+
*/
|
|
51
|
+
async function streamPost(p, body, onEvent, { signal } = {}) {
|
|
52
|
+
if (ensureReady) await ensureReady();
|
|
53
|
+
const res = await fetch(`${baseUrl}${p}`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "content-type": "application/json", ...authHeaders() },
|
|
56
|
+
body: JSON.stringify(body),
|
|
57
|
+
signal,
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const text = await res.text();
|
|
61
|
+
let json = null;
|
|
62
|
+
try {
|
|
63
|
+
json = text ? JSON.parse(text) : null;
|
|
64
|
+
} catch {}
|
|
65
|
+
throw new Error(json?.error || `POST ${p} → ${res.status}`);
|
|
66
|
+
}
|
|
67
|
+
if (!res.body?.getReader) {
|
|
68
|
+
throw new Error("streaming response is not supported by this Node.js runtime");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const reader = res.body.getReader();
|
|
72
|
+
const decoder = new TextDecoder();
|
|
73
|
+
let buffer = "";
|
|
74
|
+
let finalResult = null;
|
|
75
|
+
|
|
76
|
+
if (signal) {
|
|
77
|
+
signal.addEventListener("abort", () => reader.cancel().catch(() => {}), { once: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const handleLine = async (line) => {
|
|
81
|
+
if (!line.trim()) return;
|
|
82
|
+
const event = JSON.parse(line);
|
|
83
|
+
if (event.type === "final") finalResult = event.result;
|
|
84
|
+
if (event.type === "error") throw new Error(event.error || "stream error");
|
|
85
|
+
await onEvent?.(event);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
while (true) {
|
|
89
|
+
let chunk;
|
|
90
|
+
try {
|
|
91
|
+
chunk = await reader.read();
|
|
92
|
+
} catch {
|
|
93
|
+
break; // abort/cancel — treat as clean end, caller checks its own flag
|
|
94
|
+
}
|
|
95
|
+
if (chunk.done) break;
|
|
96
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
97
|
+
const lines = buffer.split(/\r?\n/);
|
|
98
|
+
buffer = lines.pop() || "";
|
|
99
|
+
for (const line of lines) await handleLine(line);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
buffer += decoder.decode();
|
|
103
|
+
if (buffer.trim()) {
|
|
104
|
+
try {
|
|
105
|
+
await handleLine(buffer);
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return finalResult;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
baseUrl,
|
|
114
|
+
get: (p) => request("GET", p),
|
|
115
|
+
post: (p, body) => request("POST", p, body),
|
|
116
|
+
streamPost,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve the APC project for an ACP session's cwd — same contract as the
|
|
122
|
+
* mcp-server surface: walk up to the .apc root, match a registered daemon
|
|
123
|
+
* project by path, register it when unknown.
|
|
124
|
+
*/
|
|
125
|
+
export async function resolveProjectForCwd(client, cwd) {
|
|
126
|
+
const root = findApfRoot(cwd || process.cwd());
|
|
127
|
+
if (!root) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`No APC project found at or above: ${cwd}. Run \`apx init\` in the workspace first.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const projects = await client.get("/projects");
|
|
133
|
+
const match = (projects || []).find(
|
|
134
|
+
(p) => path.resolve(p.path) === path.resolve(root)
|
|
135
|
+
);
|
|
136
|
+
if (match) return match;
|
|
137
|
+
return client.post("/projects", { path: root });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Flatten an ACP prompt (ContentBlock[]) into the text the daemon expects. */
|
|
141
|
+
export function extractPromptText(blocks) {
|
|
142
|
+
if (typeof blocks === "string") return blocks;
|
|
143
|
+
if (!Array.isArray(blocks)) return "";
|
|
144
|
+
const parts = [];
|
|
145
|
+
for (const block of blocks) {
|
|
146
|
+
if (!block || typeof block !== "object") continue;
|
|
147
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
148
|
+
parts.push(block.text);
|
|
149
|
+
} else if (block.type === "resource_link" && block.uri) {
|
|
150
|
+
// Baseline capability: resource links arrive as URIs; surface them to
|
|
151
|
+
// the model as plain references (we don't fetch on the agent side).
|
|
152
|
+
parts.push(String(block.uri));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return parts.join("\n").trim();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Best-effort mapping from APX tool names (snake_case verbs) to ACP ToolKind.
|
|
159
|
+
const KIND_RULES = [
|
|
160
|
+
[/^(search|find)_/, "search"],
|
|
161
|
+
[/^(read|list|get|show)_/, "read"],
|
|
162
|
+
[/^(edit|write|update|set|create|add|remember|import)_?/, "edit"],
|
|
163
|
+
[/^(delete|remove)_/, "delete"],
|
|
164
|
+
[/(shell|exec|run|call)/, "execute"],
|
|
165
|
+
[/(fetch|http|web|download)/, "fetch"],
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
export function toolKindFor(toolName) {
|
|
169
|
+
const name = String(toolName || "").toLowerCase();
|
|
170
|
+
for (const [re, kind] of KIND_RULES) {
|
|
171
|
+
if (re.test(name)) return kind;
|
|
172
|
+
}
|
|
173
|
+
return "other";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Compact text summary of a tool result for tool_call_update content. */
|
|
177
|
+
export function summarizeToolResult(result, max = 2000) {
|
|
178
|
+
let text;
|
|
179
|
+
if (result == null) text = "";
|
|
180
|
+
else if (typeof result === "string") text = result;
|
|
181
|
+
else {
|
|
182
|
+
try {
|
|
183
|
+
text = JSON.stringify(result, null, 2);
|
|
184
|
+
} catch {
|
|
185
|
+
text = String(result);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return text.length > max ? text.slice(0, max - 1) + "…" : text;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let sessionCounter = 0;
|
|
192
|
+
|
|
193
|
+
/** Per-connection session state. History feeds `previousMessages` so multi-turn
|
|
194
|
+
* ACP sessions keep context without any daemon-side session storage. */
|
|
195
|
+
export function createSession({ id, project, cwd }) {
|
|
196
|
+
sessionCounter += 1;
|
|
197
|
+
return {
|
|
198
|
+
id,
|
|
199
|
+
project,
|
|
200
|
+
cwd,
|
|
201
|
+
history: [], // [{role: "user"|"assistant", content: string}]
|
|
202
|
+
activeTurn: null, // { abort: AbortController, cancelled: boolean }
|
|
203
|
+
seq: sessionCounter,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// apx acp — serve the APX super-agent over the Agent Client Protocol on the
|
|
2
|
+
// current stdio. ACP clients (Zed, JetBrains, marimo, …) spawn this command
|
|
3
|
+
// as a subprocess and speak JSON-RPC over stdin/stdout, so the command must
|
|
4
|
+
// never print to stdout itself; all human-facing output goes to stderr.
|
|
5
|
+
|
|
6
|
+
export async function cmdAcp() {
|
|
7
|
+
const { startStdioAcpServer } = await import("#interfaces/acp/index.js");
|
|
8
|
+
process.stderr.write("apx acp: Agent Client Protocol server on stdio (ctrl-c to stop)\n");
|
|
9
|
+
await startStdioAcpServer();
|
|
10
|
+
}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import open from "open";
|
|
4
5
|
import { http } from "../http.js";
|
|
5
6
|
import { resolveProjectId } from "./project.js";
|
|
6
7
|
|
|
8
|
+
// Emit an OSC 8 terminal hyperlink when stdout is a TTY that likely supports
|
|
9
|
+
// it (iTerm2, modern VS Code, kitty, WezTerm, …). Falls back to the raw URL
|
|
10
|
+
// elsewhere so the link is always at least copy-pasteable.
|
|
11
|
+
function hyperlink(url, label = url) {
|
|
12
|
+
if (process.stdout.isTTY) {
|
|
13
|
+
return `\x1b]8;;${url}\x07${label}\x1b]8;;\x07`;
|
|
14
|
+
}
|
|
15
|
+
return label === url ? url : `${label} (${url})`;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
// First two bytes of an executable script. Used as a hint when the file
|
|
8
19
|
// doesn't have the exec bit but clearly intends to run (shebang line).
|
|
9
20
|
const SHEBANG = "#!";
|
|
@@ -142,3 +153,107 @@ export async function cmdArtifactRun(args) {
|
|
|
142
153
|
});
|
|
143
154
|
});
|
|
144
155
|
}
|
|
156
|
+
|
|
157
|
+
// `apx artifact preview <name> [--open] [--share] [--no-watch]`
|
|
158
|
+
//
|
|
159
|
+
// Asks the daemon to spin up an ephemeral local web server that renders the
|
|
160
|
+
// artifact (HTML / React / static) and prints an interactive localhost link.
|
|
161
|
+
// The page auto-reloads when the artifact file changes (unless --no-watch).
|
|
162
|
+
// With --open the link is opened in the default browser; with --share a public
|
|
163
|
+
// tunnel URL is created too.
|
|
164
|
+
export async function cmdArtifactPreview(args) {
|
|
165
|
+
const name = args._[0];
|
|
166
|
+
if (!name) throw new Error("apx artifact preview: missing <name>");
|
|
167
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
168
|
+
const watch = args.flags["no-watch"] ? false : true;
|
|
169
|
+
|
|
170
|
+
let view;
|
|
171
|
+
try {
|
|
172
|
+
view = await http.post(
|
|
173
|
+
`/projects/${pid}/artifacts/${encodeURIComponent(name)}/preview`,
|
|
174
|
+
{ watch }
|
|
175
|
+
);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
throw new Error(`could not preview "${name}": ${e.message}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`preview ready — ${view.kind} artifact "${view.name}"`);
|
|
181
|
+
console.log(` local: ${hyperlink(view.url)}`);
|
|
182
|
+
if (view.watch) console.log(" (auto-reloads on change — edit the artifact and the tab refreshes)");
|
|
183
|
+
console.log(` stop: apx artifact stop ${view.id}`);
|
|
184
|
+
|
|
185
|
+
if (args.flags.share) {
|
|
186
|
+
await sharePreview(view.id, { open: !!args.flags.open });
|
|
187
|
+
} else if (args.flags.open) {
|
|
188
|
+
await open(view.url);
|
|
189
|
+
console.log(" opened in your browser.");
|
|
190
|
+
} else {
|
|
191
|
+
console.log(` open: apx artifact preview ${name} --open`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Shared helper: open a tunnel for an existing preview id and print the URL.
|
|
196
|
+
async function sharePreview(previewId, { open: doOpen = false } = {}) {
|
|
197
|
+
let tunnel;
|
|
198
|
+
try {
|
|
199
|
+
tunnel = await http.post(`/previews/${previewId}/tunnel`, {});
|
|
200
|
+
} catch (e) {
|
|
201
|
+
throw new Error(`could not create tunnel: ${e.message}`);
|
|
202
|
+
}
|
|
203
|
+
console.log(` public: ${hyperlink(tunnel.url)} (${tunnel.provider})`);
|
|
204
|
+
console.log(" ⚠ anyone with this URL can reach the preview while it's open.");
|
|
205
|
+
if (doOpen) {
|
|
206
|
+
await open(tunnel.url);
|
|
207
|
+
console.log(" opened public URL in your browser.");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// `apx artifact share <name> [--open] [--no-watch]`
|
|
212
|
+
// Convenience: preview + tunnel in one shot.
|
|
213
|
+
export async function cmdArtifactShare(args) {
|
|
214
|
+
const name = args._[0];
|
|
215
|
+
if (!name) throw new Error("apx artifact share: missing <name>");
|
|
216
|
+
const pid = await resolveProjectId(args?.flags?.project);
|
|
217
|
+
const watch = args.flags["no-watch"] ? false : true;
|
|
218
|
+
|
|
219
|
+
const view = await http.post(
|
|
220
|
+
`/projects/${pid}/artifacts/${encodeURIComponent(name)}/preview`,
|
|
221
|
+
{ watch }
|
|
222
|
+
);
|
|
223
|
+
console.log(`sharing ${view.kind} artifact "${view.name}"`);
|
|
224
|
+
console.log(` local: ${hyperlink(view.url)}`);
|
|
225
|
+
await sharePreview(view.id, { open: !!args.flags.open });
|
|
226
|
+
console.log(` stop: apx artifact stop ${view.id}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// `apx artifact previews` — list running preview servers.
|
|
230
|
+
export async function cmdArtifactPreviews(args = {}) {
|
|
231
|
+
const rows = await http.get(`/previews`);
|
|
232
|
+
if (!rows.length) {
|
|
233
|
+
console.log("(no running previews)");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
console.log("ID".padEnd(10) + "NAME".padEnd(24) + "KIND".padEnd(8) + "URL");
|
|
237
|
+
for (const r of rows) {
|
|
238
|
+
console.log(
|
|
239
|
+
r.id.padEnd(10) +
|
|
240
|
+
String(r.name).slice(0, 22).padEnd(24) +
|
|
241
|
+
String(r.kind).padEnd(8) +
|
|
242
|
+
r.url + (r.tunnel ? ` → ${r.tunnel.url}` : "")
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// `apx artifact stop <id> | --all` — stop preview server(s).
|
|
248
|
+
export async function cmdArtifactStop(args) {
|
|
249
|
+
if (args.flags.all) {
|
|
250
|
+
const rows = await http.get(`/previews`);
|
|
251
|
+
for (const r of rows) await http.delete(`/previews/${r.id}`);
|
|
252
|
+
console.log(`stopped ${rows.length} preview(s)`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const id = args._[0];
|
|
256
|
+
if (!id) throw new Error("apx artifact stop: missing <id> (or --all)");
|
|
257
|
+
await http.delete(`/previews/${encodeURIComponent(id)}`);
|
|
258
|
+
console.log(`stopped preview ${id}`);
|
|
259
|
+
}
|
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
cmdConversationsGet,
|
|
92
92
|
} from "./commands/chat.js";
|
|
93
93
|
import { cmdCode } from "./commands/code.js";
|
|
94
|
+
import { cmdAcp } from "./commands/acp.js";
|
|
94
95
|
import { cmdRun, cmdEnvDetect } from "./commands/runtime.js";
|
|
95
96
|
import { cmdSend, cmdConnections } from "./commands/a2a.js";
|
|
96
97
|
import {
|
|
@@ -130,6 +131,10 @@ import {
|
|
|
130
131
|
cmdArtifactShow,
|
|
131
132
|
cmdArtifactRemove,
|
|
132
133
|
cmdArtifactRun,
|
|
134
|
+
cmdArtifactPreview,
|
|
135
|
+
cmdArtifactShare,
|
|
136
|
+
cmdArtifactPreviews,
|
|
137
|
+
cmdArtifactStop,
|
|
133
138
|
} from "./commands/artifact.js";
|
|
134
139
|
import {
|
|
135
140
|
cmdTaskAdd,
|
|
@@ -1133,6 +1138,17 @@ const HELP_TOPICS = new Map(Object.entries({
|
|
|
1133
1138
|
],
|
|
1134
1139
|
examples: ["apx chat reviewer", "apx chat reviewer --conversation abc123"],
|
|
1135
1140
|
}),
|
|
1141
|
+
acp: topic({
|
|
1142
|
+
title: "apx acp",
|
|
1143
|
+
summary: "Serve the APX super-agent over the Agent Client Protocol (ACP) on stdio.",
|
|
1144
|
+
usage: ["apx acp"],
|
|
1145
|
+
notes: [
|
|
1146
|
+
"For ACP clients (Zed, JetBrains, marimo, …) that spawn agents as subprocesses.",
|
|
1147
|
+
"stdout carries the protocol — logs go to stderr and ~/.apx/logs/apx.log.",
|
|
1148
|
+
"Sessions resolve the APC project from the client's workspace cwd.",
|
|
1149
|
+
],
|
|
1150
|
+
examples: ["apx acp"],
|
|
1151
|
+
}),
|
|
1136
1152
|
code: topic({
|
|
1137
1153
|
title: "apx code",
|
|
1138
1154
|
summary: "Start the APX terminal coding assistant with system and workspace context.",
|
|
@@ -1315,6 +1331,10 @@ const HELP_TOPICS = new Map(Object.entries({
|
|
|
1315
1331
|
["list | ls", "List artifacts in the project."],
|
|
1316
1332
|
["show <name>", "Print artifact content."],
|
|
1317
1333
|
["run <name> [args...]", "Execute a runnable artifact (shebang or +x). Stdio is inherited."],
|
|
1334
|
+
["preview <name> [--open] [--share]", "Serve HTML/React/static artifacts on an ephemeral local server with live-reload."],
|
|
1335
|
+
["share <name> [--open]", "Preview + open a public tunnel URL (cloudflared/localtunnel)."],
|
|
1336
|
+
["previews", "List running preview servers."],
|
|
1337
|
+
["stop <id> | --all", "Stop a preview server."],
|
|
1318
1338
|
["remove | rm <name>", "Delete an artifact."],
|
|
1319
1339
|
],
|
|
1320
1340
|
examples: [
|
|
@@ -1322,8 +1342,49 @@ const HELP_TOPICS = new Map(Object.entries({
|
|
|
1322
1342
|
"apx artifact list",
|
|
1323
1343
|
"apx artifact show check_asana.sh",
|
|
1324
1344
|
"apx artifact run check_asana.sh",
|
|
1345
|
+
"apx artifact preview dashboard.html --open",
|
|
1346
|
+
"apx artifact share report.jsx",
|
|
1347
|
+
],
|
|
1348
|
+
}),
|
|
1349
|
+
"artifact preview": topic({
|
|
1350
|
+
title: "apx artifact preview",
|
|
1351
|
+
summary: "Serve an artifact on an ephemeral local web server and print an interactive link. HTML, single-file React (.jsx/.tsx), static dirs, and text are all rendered; the page auto-reloads when the file changes.",
|
|
1352
|
+
usage: ["apx artifact preview <name> [--open] [--share] [--no-watch] [--project <name|id|path>]"],
|
|
1353
|
+
options: [
|
|
1354
|
+
["--open", "Open the local URL in your default browser."],
|
|
1355
|
+
["--share", "Also create a public tunnel URL for quick sharing."],
|
|
1356
|
+
["--no-watch", "Disable live-reload on file change."],
|
|
1357
|
+
["--project <name|id|path>", "Pin command to a specific project."],
|
|
1358
|
+
],
|
|
1359
|
+
examples: [
|
|
1360
|
+
"apx artifact preview dashboard.html --open",
|
|
1361
|
+
"apx artifact preview app.jsx --share",
|
|
1325
1362
|
],
|
|
1326
1363
|
}),
|
|
1364
|
+
"artifact share": topic({
|
|
1365
|
+
title: "apx artifact share",
|
|
1366
|
+
summary: "Preview an artifact and expose it through a secure public tunnel (cloudflared, falling back to localtunnel). Prints a temporary public URL anyone can open.",
|
|
1367
|
+
usage: ["apx artifact share <name> [--open] [--no-watch] [--project <name|id|path>]"],
|
|
1368
|
+
options: [
|
|
1369
|
+
["--open", "Open the public URL in your default browser."],
|
|
1370
|
+
["--no-watch", "Disable live-reload on file change."],
|
|
1371
|
+
["--project <name|id|path>", "Pin command to a specific project."],
|
|
1372
|
+
],
|
|
1373
|
+
examples: ["apx artifact share report.jsx --open"],
|
|
1374
|
+
}),
|
|
1375
|
+
"artifact previews": topic({
|
|
1376
|
+
title: "apx artifact previews",
|
|
1377
|
+
summary: "List running artifact preview servers (id, name, kind, local + public URLs).",
|
|
1378
|
+
usage: ["apx artifact previews"],
|
|
1379
|
+
examples: ["apx artifact previews"],
|
|
1380
|
+
}),
|
|
1381
|
+
"artifact stop": topic({
|
|
1382
|
+
title: "apx artifact stop",
|
|
1383
|
+
summary: "Stop a running preview server (and its tunnel).",
|
|
1384
|
+
usage: ["apx artifact stop <id>", "apx artifact stop --all"],
|
|
1385
|
+
options: [["--all", "Stop every running preview."]],
|
|
1386
|
+
examples: ["apx artifact stop 3f9a1c2b", "apx artifact stop --all"],
|
|
1387
|
+
}),
|
|
1327
1388
|
"artifact create": topic({
|
|
1328
1389
|
title: "apx artifact create",
|
|
1329
1390
|
summary: "Create a new managed artifact file.",
|
|
@@ -2099,6 +2160,7 @@ function buildHelp(version) {
|
|
|
2099
2160
|
hCmd("apx search \"query\"", 36, "web search (ddg | brave | browser) --mode <m> -n N"),
|
|
2100
2161
|
hCmd("apx conversations list", 36, "stored exec/chat conversations for <agent>"),
|
|
2101
2162
|
hCmd("apx conversations get", 36, "<agent> <id>"),
|
|
2163
|
+
hCmd("apx acp", 36, "serve the super-agent over the Agent Client Protocol (stdio, for IDEs)"),
|
|
2102
2164
|
|
|
2103
2165
|
hSec("Runtimes"),
|
|
2104
2166
|
hCmd("apx run <agent>", 36, "--runtime <id> \"prompt\" --timeout <s>"),
|
|
@@ -2137,6 +2199,9 @@ function buildHelp(version) {
|
|
|
2137
2199
|
hCmd("apx artifact create <name>", 36, "create managed file in project storage [--content '...'] [--project 0]"),
|
|
2138
2200
|
hCmd("apx artifact list", 36, "list artifacts"),
|
|
2139
2201
|
hCmd("apx artifact show <name>", 36, "print artifact content"),
|
|
2202
|
+
hCmd("apx artifact preview <name>",36, "serve HTML/React on a local URL w/ live-reload [--open] [--share]"),
|
|
2203
|
+
hCmd("apx artifact share <name>", 36, "preview + public tunnel URL"),
|
|
2204
|
+
hCmd("apx artifact previews", 36, "list running previews (stop with: apx artifact stop <id>)"),
|
|
2140
2205
|
hCmd("apx artifact remove <name>", 36, ""),
|
|
2141
2206
|
|
|
2142
2207
|
hSec("Commands & Skills"),
|
|
@@ -2516,6 +2581,11 @@ async function dispatch(cmd, rest) {
|
|
|
2516
2581
|
await cmdExec(parseArgs(rest));
|
|
2517
2582
|
break;
|
|
2518
2583
|
|
|
2584
|
+
case "acp":
|
|
2585
|
+
// ACP server owns stdio until the client closes the pipe.
|
|
2586
|
+
await cmdAcp(parseArgs(rest));
|
|
2587
|
+
return;
|
|
2588
|
+
|
|
2519
2589
|
case "search":
|
|
2520
2590
|
await cmdSearch(parseArgs(rest));
|
|
2521
2591
|
break;
|
|
@@ -2613,6 +2683,10 @@ async function dispatch(cmd, rest) {
|
|
|
2613
2683
|
else if (sub === "show" || sub === "get") await cmdArtifactShow(a);
|
|
2614
2684
|
else if (sub === "remove" || sub === "rm") await cmdArtifactRemove(a);
|
|
2615
2685
|
else if (sub === "run") await cmdArtifactRun(a);
|
|
2686
|
+
else if (sub === "preview" || sub === "serve") await cmdArtifactPreview(a);
|
|
2687
|
+
else if (sub === "share") await cmdArtifactShare(a);
|
|
2688
|
+
else if (sub === "previews") await cmdArtifactPreviews(a);
|
|
2689
|
+
else if (sub === "stop") await cmdArtifactStop(a);
|
|
2616
2690
|
else die(`unknown artifact subcommand: ${sub}`);
|
|
2617
2691
|
break;
|
|
2618
2692
|
}
|