@agentprojectcontext/apx 1.75.0 → 1.77.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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/core/net/lan.js +114 -0
  3. package/src/core/profiles/bundled/secretary/PROFILE.md +6 -5
  4. package/src/core/profiles/bundled/secretary/profile.json +19 -6
  5. package/src/core/runtime-skills/apx-task/SKILL.md +4 -0
  6. package/src/core/stores/agent-inbox.js +153 -0
  7. package/src/core/stores/conversations.js +21 -1
  8. package/src/core/stores/tasks.js +70 -1
  9. package/src/host/daemon/api/inbox.js +45 -0
  10. package/src/host/daemon/api/tasks.js +36 -15
  11. package/src/host/daemon/api/web.js +2 -2
  12. package/src/host/daemon/api.js +2 -0
  13. package/src/interfaces/cli/commands/panel.js +131 -0
  14. package/src/interfaces/cli/commands/task.js +44 -13
  15. package/src/interfaces/cli/index.js +35 -1
  16. package/src/interfaces/web/dist/assets/{index-CXeqTvfy.js → index-BWWpTTSd.js} +135 -135
  17. package/src/interfaces/web/dist/assets/index-BWWpTTSd.js.map +1 -0
  18. package/src/interfaces/web/dist/assets/index-D_EJEA1n.css +1 -0
  19. package/src/interfaces/web/dist/index.html +2 -2
  20. package/src/interfaces/web/src/App.tsx +2 -0
  21. package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +20 -1
  22. package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +105 -72
  23. package/src/interfaces/web/src/hooks/useInbox.ts +12 -0
  24. package/src/interfaces/web/src/i18n/en.ts +17 -0
  25. package/src/interfaces/web/src/i18n/es.ts +17 -0
  26. package/src/interfaces/web/src/lib/api/inbox.ts +26 -0
  27. package/src/interfaces/web/src/lib/api/tasks.ts +6 -2
  28. package/src/interfaces/web/src/screens/InboxScreen.tsx +103 -0
  29. package/src/interfaces/web/src/screens/SettingsScreen.tsx +1 -1
  30. package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +21 -3
  31. package/src/core/profiles/bundled/secretary/PROFILE.es.md +0 -44
  32. package/src/interfaces/web/dist/assets/index-CQ5kyFej.css +0 -1
  33. package/src/interfaces/web/dist/assets/index-CXeqTvfy.js.map +0 -1
@@ -0,0 +1,131 @@
1
+ // apx panel — reach the admin panel from another device on the same network.
2
+ //
3
+ // apx panel status
4
+ // apx panel share [--host 192.168.1.40]
5
+ // apx panel unshare
6
+ //
7
+ // This binds a second, specific LAN address. It is NOT a tunnel: nothing leaves
8
+ // the local network. Loopback stays the default, sharing is always explicit,
9
+ // and the daemon's auth is untouched — the URL carries the token because
10
+ // /admin/web-token is loopback-only by design, so a phone cannot fetch it.
11
+ import fs from "node:fs";
12
+ import { readConfig, writeConfig, effectivePort, effectiveHost } from "#core/config/index.js";
13
+ import { TOKEN_PATH } from "#core/config/paths.js";
14
+ import { detectLanAddresses, validateBindHost, isLoopback, isWildcard } from "#core/net/lan.js";
15
+
16
+ export const PANEL_USAGE = {
17
+ status: "apx panel status",
18
+ share: "apx panel share [--host <ip>]",
19
+ unshare: "apx panel unshare",
20
+ };
21
+
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
+ export async function cmdPanelStatus() {
38
+ const cfg = readConfig();
39
+ const host = effectiveHost(cfg);
40
+ const port = effectivePort(cfg);
41
+
42
+ if (isLoopback(host)) {
43
+ console.log(`panel: local only — http://${host}:${port}`);
44
+ console.log(" Nothing on your network can reach it.");
45
+ console.log(" To reach it from your phone: apx panel share");
46
+ return;
47
+ }
48
+
49
+ // 0.0.0.0 is a bigger promise than "reachable on my network": it also covers
50
+ // every interface that appears LATER — a VPN, a hotspot, a bridged container.
51
+ // Worth calling out separately rather than reporting it as ordinary sharing.
52
+ if (isWildcard(host)) {
53
+ console.log(`panel: bound to EVERY interface — ${host}:${port}`);
54
+ const addrs = detectLanAddresses();
55
+ if (addrs.length) {
56
+ console.log(` Currently reachable at: ${addrs.map((a) => `http://${a.address}:${port}`).join(", ")}`);
57
+ }
58
+ console.log(" This also covers interfaces that appear later — a VPN, a hotspot, a");
59
+ console.log(" container bridge. Auth still applies, but the surface is wider than");
60
+ console.log(" it needs to be.");
61
+ console.log("");
62
+ console.log(" Narrow it to one address: apx panel share");
63
+ console.log(" Turn it off entirely: apx panel unshare");
64
+ return;
65
+ }
66
+
67
+ console.log(`panel: SHARED on your network — http://${host}:${port}`);
68
+ console.log(` Anyone on this network who has the URL and the token can open it.`);
69
+ console.log(` To stop: apx panel unshare`);
70
+ }
71
+
72
+ export async function cmdPanelShare(args) {
73
+ const cfg = readConfig();
74
+ const port = effectivePort(cfg);
75
+
76
+ const requested = args?.flags?.host;
77
+ const candidates = detectLanAddresses();
78
+
79
+ if (!requested && candidates.length === 0) {
80
+ console.error("apx panel share: no network address found on this machine.");
81
+ console.error(" Are you connected to a network? Pass one explicitly with --host <ip>.");
82
+ process.exit(1);
83
+ }
84
+
85
+ const host = requested || candidates[0].address;
86
+ const check = validateBindHost(host);
87
+ if (!check.ok) {
88
+ console.error(`apx panel share: ${check.reason}`);
89
+ process.exit(1);
90
+ }
91
+
92
+ cfg.host = host;
93
+ writeConfig(cfg);
94
+
95
+ const token = readToken();
96
+ const iface = candidates.find((c) => c.address === host)?.iface;
97
+
98
+ console.log(`panel shared on ${host}:${port}${iface ? ` (${iface})` : ""}`);
99
+ console.log("");
100
+ console.log(" Open this on your phone — the token is in the URL:");
101
+ console.log(` ${panelUrl(host, port, token)}`);
102
+ console.log("");
103
+ // Say plainly what changed. A LAN can be a café or a coworking space.
104
+ console.log(" What this means: anyone on this network can now REACH the panel.");
105
+ console.log(" They still need the token above, which is not guessable — treat that");
106
+ console.log(" URL like a password, and run `apx panel unshare` when you are done.");
107
+ if (candidates.length > 1) {
108
+ const others = candidates.filter((c) => c.address !== host).map((c) => `${c.address} (${c.iface})`);
109
+ console.log(` Other addresses on this machine: ${others.join(", ")}`);
110
+ }
111
+ console.log("");
112
+ console.log(" Restart the daemon for the new binding to take effect: apx restart");
113
+ }
114
+
115
+ export async function cmdPanelUnshare() {
116
+ const cfg = readConfig();
117
+ const port = effectivePort(cfg);
118
+ const was = effectiveHost(cfg);
119
+
120
+ if (isLoopback(was)) {
121
+ console.log("panel is already local only — nothing to do");
122
+ return;
123
+ }
124
+
125
+ cfg.host = "127.0.0.1";
126
+ writeConfig(cfg);
127
+
128
+ console.log(`panel is local only again — http://127.0.0.1:${port}`);
129
+ console.log(` Was ${was}. Nothing on your network can reach it now.`);
130
+ console.log(" Restart the daemon to apply: apx restart");
131
+ }
@@ -1,7 +1,8 @@
1
1
  // apx task — per-project TODO list. Backed by /projects/:pid/tasks.
2
2
  //
3
3
  // apx task add "<title>" [--project X] [--body Y] [--tag t] [--due 2026-05-30] [--agent A]
4
- // apx task list [--project X] [--state open|done|dropped|all] [--tag X] [--agent Y] [--due-before ISO] [--limit N]
4
+ // apx task list [--all | --project X] [--state ...] [--status ...] [--tag X] [--agent Y]
5
+ // [--due-before ISO] [--due-after ISO] [--updated-since ISO] [--limit N]
5
6
  // apx task show <id> [--project X]
6
7
  // apx task done <id> [--project X] [--by name]
7
8
  // apx task drop <id> [--project X] [--by name]
@@ -18,7 +19,7 @@ import { resolveProjectId } from "./project.js";
18
19
  // ── Usage strings (also used by index.js help topics) ────────────────────────
19
20
  export const TASK_USAGE = {
20
21
  add: 'apx task add "<title>" [--project X] [--body Y] [--tag t]... [--due 2026-05-30] [--agent A]',
21
- list: "apx task list [--project X] [--state open|done|dropped|all] [--tag X] [--agent Y] [--due-before ISO] [--limit N]",
22
+ list: "apx task list [--all | --project X] [--state open|done|dropped|all] [--status pending|running|in_review|blocked] [--tag X] [--agent Y] [--due-before ISO] [--due-after ISO] [--updated-since ISO] [--limit N]",
22
23
  show: "apx task show <id> [--project X]",
23
24
  done: "apx task done <id> [--project X] [--by name]",
24
25
  drop: "apx task drop <id> [--project X] [--by name]",
@@ -44,14 +45,20 @@ function shortTs(iso) {
44
45
  return String(iso).replace(/T/, " ").replace(/Z$/, "").slice(0, 16);
45
46
  }
46
47
 
47
- function renderTable(rows) {
48
+ function renderTable(rows, { showProject = false } = {}) {
48
49
  if (!rows.length) {
49
50
  console.log("(no tasks)");
50
51
  return;
51
52
  }
52
- const idW = Math.max(...rows.map((r) => r.id.length), 4);
53
+ const idW = Math.max(...rows.map((r) => String(r.id).length), 4);
54
+ const projW = showProject
55
+ ? Math.min(Math.max(...rows.map((r) => String(r.project_name || "").length), 7), 20)
56
+ : 0;
57
+ const proj = (t) => (showProject ? String(t.project_name || "").slice(0, projW).padEnd(projW) + " " : "");
58
+
53
59
  console.log(
54
60
  "ID".padEnd(idW) + " " +
61
+ (showProject ? "PROJECT".padEnd(projW) + " " : "") +
55
62
  "STATE".padEnd(7) + " " +
56
63
  "DUE".padEnd(10) + " " +
57
64
  "TAGS".padEnd(18) + " " +
@@ -61,7 +68,8 @@ function renderTable(rows) {
61
68
  const tags = (t.tags || []).join(",").slice(0, 18).padEnd(18);
62
69
  const title = (t.title || "").slice(0, 60);
63
70
  console.log(
64
- t.id.padEnd(idW) + " " +
71
+ String(t.id).padEnd(idW) + " " +
72
+ proj(t) +
65
73
  (t.state || "open").padEnd(7) + " " +
66
74
  (t.due || "—").padEnd(10) + " " +
67
75
  tags + " " +
@@ -105,17 +113,40 @@ export async function cmdTaskAdd(args) {
105
113
  }
106
114
 
107
115
  // ── list ──────────────────────────────────────────────────────────────────────
116
+ // The list endpoints answer with a { meta, data } envelope. Older callers here
117
+ // treated the response as a bare array, which made `apx task list` print
118
+ // "(no tasks)" no matter what — the rows were sitting in `.data`.
119
+ function unwrap(res) {
120
+ if (Array.isArray(res)) return { rows: res, meta: null };
121
+ return { rows: Array.isArray(res?.data) ? res.data : [], meta: res?.meta || null };
122
+ }
123
+
108
124
  export async function cmdTaskList(args) {
109
- const pid = await resolveProjectId(args?.flags?.project);
110
125
  const params = new URLSearchParams();
111
- if (args.flags?.state) params.set("state", args.flags.state);
112
- if (args.flags?.tag) params.set("tag", args.flags.tag);
113
- if (args.flags?.agent) params.set("agent", args.flags.agent);
114
- if (args.flags?.["due-before"]) params.set("due_before", args.flags["due-before"]);
115
- if (args.flags?.limit) params.set("limit", String(args.flags.limit));
126
+ if (args.flags?.state) params.set("state", args.flags.state);
127
+ if (args.flags?.tag) params.set("tag", args.flags.tag);
128
+ if (args.flags?.agent) params.set("agent", args.flags.agent);
129
+ if (args.flags?.status) params.set("status", args.flags.status);
130
+ if (args.flags?.["due-before"]) params.set("due_before", args.flags["due-before"]);
131
+ if (args.flags?.["due-after"]) params.set("due_after", args.flags["due-after"]);
132
+ if (args.flags?.["updated-since"]) params.set("updated_since", args.flags["updated-since"]);
133
+ if (args.flags?.limit) params.set("limit", String(args.flags.limit));
116
134
  const qs = params.toString();
117
- const rows = await http.get(`/projects/${pid}/tasks${qs ? "?" + qs : ""}`);
118
- renderTable(rows);
135
+
136
+ // --all folds every registered project into one list, each row carrying the
137
+ // project it came from. Without it, behaviour is exactly as before.
138
+ const all = !!args.flags?.all;
139
+ const path = all
140
+ ? `/tasks${qs ? "?" + qs : ""}`
141
+ : `/projects/${await resolveProjectId(args?.flags?.project)}/tasks${qs ? "?" + qs : ""}`;
142
+
143
+ const { rows, meta } = unwrap(await http.get(path));
144
+ renderTable(rows, { showProject: all });
145
+
146
+ // A project whose task log could not be read is reported, never swallowed.
147
+ for (const s of meta?.skipped || []) {
148
+ console.error(`warning: project #${s.id} skipped — ${s.error}`);
149
+ }
119
150
  }
120
151
 
121
152
  // ── show ──────────────────────────────────────────────────────────────────────
@@ -161,6 +161,11 @@ import {
161
161
  cmdProfileDoctor,
162
162
  cmdProfileUninstall,
163
163
  } from "./commands/profile.js";
164
+ import {
165
+ cmdPanelStatus,
166
+ cmdPanelShare,
167
+ cmdPanelUnshare,
168
+ } from "./commands/panel.js";
164
169
  import {
165
170
  cmdOrgShow,
166
171
  cmdOrgAreaAdd,
@@ -1617,6 +1622,19 @@ const HELP_TOPICS = new Map(Object.entries({
1617
1622
  examples: ["apx plugins status telegram"],
1618
1623
  }),
1619
1624
 
1625
+ panel: topic({
1626
+ title: "apx panel",
1627
+ summary:
1628
+ "Reach the admin panel from another device on the same network. Not a tunnel — nothing leaves your LAN, and loopback stays the default.",
1629
+ usage: ["apx panel <status|share|unshare>"],
1630
+ commands: [
1631
+ ["status", "Whether the panel is local only or shared, and on what address."],
1632
+ ["share", "Bind a LAN address and print the URL to open on your phone."],
1633
+ ["unshare", "Go back to local only."],
1634
+ ],
1635
+ options: [["--host <ip>", "share: bind this address instead of the detected one."]],
1636
+ examples: ["apx panel status", "apx panel share", "apx panel unshare"],
1637
+ }),
1620
1638
  profile: topic({
1621
1639
  title: "apx profile",
1622
1640
  summary:
@@ -1666,10 +1684,16 @@ const HELP_TOPICS = new Map(Object.entries({
1666
1684
  ["reopen <id>", "Reopen a done or dropped task."],
1667
1685
  ["patch | edit <id>", "Edit fields on an existing task."],
1668
1686
  ],
1669
- options: [["--project <name|id|path>", "Pin command to a specific project."]],
1687
+ options: [
1688
+ ["--project <name|id|path>", "Pin command to a specific project."],
1689
+ ["--all", "list: fold every registered project into one list, each row labelled."],
1690
+ ["--status <s>", "list: workflow sub-status — pending | running | in_review | blocked."],
1691
+ ["--updated-since <ISO>", "list: only what moved since that moment."],
1692
+ ],
1670
1693
  examples: [
1671
1694
  "apx task add \"Ship release notes\" --tag release --due 2026-06-01",
1672
1695
  "apx task list --state open --tag release",
1696
+ "apx task list --all --status blocked",
1673
1697
  "apx task done t_abc123",
1674
1698
  ],
1675
1699
  }),
@@ -2799,6 +2823,16 @@ async function dispatch(cmd, rest) {
2799
2823
  break;
2800
2824
  }
2801
2825
 
2826
+ case "panel": {
2827
+ const sub = rest[0];
2828
+ const a = parseArgs(rest.slice(1));
2829
+ if (!sub || sub === "status") await cmdPanelStatus(a);
2830
+ else if (sub === "share") await cmdPanelShare(a);
2831
+ else if (sub === "unshare") await cmdPanelUnshare(a);
2832
+ else die(`unknown panel subcommand: ${sub}\nUsage: apx panel <status|share|unshare>`);
2833
+ break;
2834
+ }
2835
+
2802
2836
  case "profile":
2803
2837
  case "profiles": {
2804
2838
  const sub = rest[0];