@agentprojectcontext/apx 1.77.0 → 1.77.2
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentprojectcontext/apx",
|
|
3
|
-
"version": "1.77.
|
|
3
|
+
"version": "1.77.2",
|
|
4
4
|
"description": "APX — unified CLI + daemon for the Agent Project Context (APC) standard.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"upgrade": "pnpm install && pnpm add -g .",
|
|
41
41
|
"prepare": "node scripts/install-githooks.js",
|
|
42
42
|
"prepack": "node scripts/sync-apc-skill.js && node scripts/build-web.js",
|
|
43
|
-
"postinstall": "node src/interfaces/cli/postinstall.js"
|
|
43
|
+
"postinstall": "node src/interfaces/cli/postinstall.js",
|
|
44
|
+
"smoke:seam": "node --test --test-reporter=spec tests/smoke/*.smoke.js"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -25,6 +25,7 @@ const API_PREFIXES = [
|
|
|
25
25
|
"/messages", "/sessions", "/tools", "/mcp", "/voice", "/tts", "/desktop", "/overlay",
|
|
26
26
|
"/transcribe", "/run", "/files", "/memory", "/env", "/pair", "/deck",
|
|
27
27
|
"/super-agent", "/identity", "/skills", "/profiles", "/inbox",
|
|
28
|
+
"/tasks", "/agents", "/plugins", "/previews", "/embeddings",
|
|
28
29
|
];
|
|
29
30
|
|
|
30
31
|
export function isApiPath(p) {
|
package/src/host/daemon/db.js
CHANGED
|
@@ -126,6 +126,12 @@ export class ProjectManager {
|
|
|
126
126
|
name,
|
|
127
127
|
kind,
|
|
128
128
|
agents: readAgents(e.path).length,
|
|
129
|
+
// Where this project's runtime state lives. The CLI needs it to reach
|
|
130
|
+
// per-routine memory and anything else stored outside the repo — it has
|
|
131
|
+
// no other way to resolve it, and `apx routine memory` was silently
|
|
132
|
+
// broken for want of these two fields.
|
|
133
|
+
apx_id: e.apxId || null,
|
|
134
|
+
storage_path: e.storagePath || null,
|
|
129
135
|
};
|
|
130
136
|
});
|
|
131
137
|
}
|
package/src/host/daemon/index.js
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
// Must run before any outbound fetch — see the module header for why.
|
|
4
4
|
import "#core/net/ipv4-first.js";
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
+
import http from "node:http";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { randomBytes } from "node:crypto";
|
|
10
|
+
|
|
11
|
+
// Loopback is always bound, no matter what `host` is configured to.
|
|
12
|
+
const LOOPBACK_HOST = "127.0.0.1";
|
|
9
13
|
import {
|
|
10
14
|
readConfig,
|
|
11
15
|
writeConfig,
|
|
@@ -233,9 +237,29 @@ async function main() {
|
|
|
233
237
|
plugins.installRoutes(app);
|
|
234
238
|
|
|
235
239
|
let callbackReconciler = null;
|
|
236
|
-
|
|
240
|
+
|
|
241
|
+
// Loopback is ALWAYS bound, whatever `host` says.
|
|
242
|
+
//
|
|
243
|
+
// Binding a single specific LAN address (what `apx panel share` configures)
|
|
244
|
+
// excludes 127.0.0.1 — and the CLI, the desktop app and /admin/web-token all
|
|
245
|
+
// reach the daemon over loopback. Sharing the panel with the phone must not
|
|
246
|
+
// take the local toolchain down with it, so a non-loopback host means TWO
|
|
247
|
+
// listeners on one app, not a move.
|
|
248
|
+
const extraHosts =
|
|
249
|
+
host && host !== LOOPBACK_HOST && host !== "0.0.0.0" && host !== "::" ? [host] : [];
|
|
250
|
+
const secondary = [];
|
|
251
|
+
|
|
252
|
+
const server = app.listen(port, extraHosts.length ? LOOPBACK_HOST : host, () => {
|
|
237
253
|
writePid();
|
|
238
|
-
|
|
254
|
+
for (const h of extraHosts) {
|
|
255
|
+
// A secondary bind failing must never take the daemon down — the local
|
|
256
|
+
// one is already up and is the one everything depends on.
|
|
257
|
+
const s2 = http.createServer(app);
|
|
258
|
+
s2.on("error", (e) => log(`bind ${h}:${port} failed (${e.code || e.message}) — local access unaffected`));
|
|
259
|
+
s2.listen(port, h, () => log(`apx-daemon also listening on http://${h}:${port}`));
|
|
260
|
+
secondary.push(s2);
|
|
261
|
+
}
|
|
262
|
+
log(`apx-daemon ${PKG.version} listening on http://${extraHosts.length ? LOOPBACK_HOST : host}:${port}`);
|
|
239
263
|
log(`projects: ${projects.list().length} | plugins: ${Object.keys(plugins.status()).join(", ") || "(none)"}`);
|
|
240
264
|
plugins.startAll();
|
|
241
265
|
scheduler.start();
|
|
@@ -317,6 +341,11 @@ async function main() {
|
|
|
317
341
|
import("./whisper-server.js").then(({ shutdownWhisperServer }) => {
|
|
318
342
|
shutdownWhisperServer().catch(() => {});
|
|
319
343
|
}).catch(() => {});
|
|
344
|
+
// Close the LAN listener(s) too, or the port stays held after SIGTERM and
|
|
345
|
+
// the next `apx restart` fails with "already running".
|
|
346
|
+
for (const s2 of secondary) {
|
|
347
|
+
try { s2.close(); } catch { /* already down */ }
|
|
348
|
+
}
|
|
320
349
|
server.close(() => {
|
|
321
350
|
clearPid();
|
|
322
351
|
process.exit(0);
|
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
// the local network. Loopback stays the default, sharing is always explicit,
|
|
9
9
|
// and the daemon's auth is untouched — the URL carries the token because
|
|
10
10
|
// /admin/web-token is loopback-only by design, so a phone cannot fetch it.
|
|
11
|
-
import fs from "node:fs";
|
|
12
11
|
import { readConfig, writeConfig, effectivePort, effectiveHost } from "#core/config/index.js";
|
|
13
|
-
import { TOKEN_PATH } from "#core/config/paths.js";
|
|
14
12
|
import { detectLanAddresses, validateBindHost, isLoopback, isWildcard } from "#core/net/lan.js";
|
|
15
13
|
|
|
16
14
|
export const PANEL_USAGE = {
|
|
@@ -19,21 +17,6 @@ export const PANEL_USAGE = {
|
|
|
19
17
|
unshare: "apx panel unshare",
|
|
20
18
|
};
|
|
21
19
|
|
|
22
|
-
function readToken() {
|
|
23
|
-
try {
|
|
24
|
-
return fs.readFileSync(TOKEN_PATH, "utf8").trim();
|
|
25
|
-
} catch {
|
|
26
|
-
return "";
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function panelUrl(host, port, token) {
|
|
31
|
-
// /admin/web-token refuses anything that is not loopback, so the panel on a
|
|
32
|
-
// phone can only authenticate from the fragment. The fragment never reaches
|
|
33
|
-
// the server or a proxy log — that is why it is a fragment and not a query.
|
|
34
|
-
return `http://${host}:${port}/` + (token ? `#token=${token}` : "");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
20
|
export async function cmdPanelStatus() {
|
|
38
21
|
const cfg = readConfig();
|
|
39
22
|
const host = effectiveHost(cfg);
|
|
@@ -92,18 +75,27 @@ export async function cmdPanelShare(args) {
|
|
|
92
75
|
cfg.host = host;
|
|
93
76
|
writeConfig(cfg);
|
|
94
77
|
|
|
95
|
-
const token = readToken();
|
|
96
78
|
const iface = candidates.find((c) => c.address === host)?.iface;
|
|
97
79
|
|
|
98
80
|
console.log(`panel shared on ${host}:${port}${iface ? ` (${iface})` : ""}`);
|
|
99
81
|
console.log("");
|
|
100
|
-
console.log(
|
|
101
|
-
console.log(
|
|
82
|
+
console.log(` URL for the other device: http://${host}:${port}/`);
|
|
83
|
+
console.log("");
|
|
84
|
+
// Deliberately NOT the daemon's master token. Handing that to a phone gives
|
|
85
|
+
// it the same power as the CLI, cannot be revoked without rotating the token
|
|
86
|
+
// every local tool depends on, and leaves no record the device exists —
|
|
87
|
+
// which is the exact thing pairing was built to avoid (see token-store.js).
|
|
88
|
+
// It would not even last: the master token is regenerated on every restart.
|
|
89
|
+
console.log(" Authorise the device by PAIRING it, not by copying a token:");
|
|
90
|
+
console.log(" the panel → Settings → Devices → Pair device, then scan the QR.");
|
|
91
|
+
console.log("");
|
|
92
|
+
console.log(" Pairing gives that device its OWN token, which survives restarts,");
|
|
93
|
+
console.log(" shows up under Paired devices, and can be revoked on its own without");
|
|
94
|
+
console.log(" disturbing anything else.");
|
|
102
95
|
console.log("");
|
|
103
96
|
// Say plainly what changed. A LAN can be a café or a coworking space.
|
|
104
97
|
console.log(" What this means: anyone on this network can now REACH the panel.");
|
|
105
|
-
console.log(" They
|
|
106
|
-
console.log(" URL like a password, and run `apx panel unshare` when you are done.");
|
|
98
|
+
console.log(" They cannot use it without pairing. Run `apx panel unshare` when done.");
|
|
107
99
|
if (candidates.length > 1) {
|
|
108
100
|
const others = candidates.filter((c) => c.address !== host).map((c) => `${c.address} (${c.iface})`);
|
|
109
101
|
console.log(` Other addresses on this machine: ${others.join(", ")}`);
|