@agentprojectcontext/apx 1.78.0 → 1.79.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 (93) hide show
  1. package/package.json +1 -1
  2. package/src/core/agent/run-agent.js +30 -5
  3. package/src/core/agent/tool-summary.js +65 -0
  4. package/src/core/agent/tools/handlers/list-commitments.js +80 -0
  5. package/src/core/agent/tools/handlers/list-tasks.js +66 -27
  6. package/src/core/agent/tools/handlers/record-commitment.js +68 -0
  7. package/src/core/agent/tools/handlers/send-telegram.js +68 -2
  8. package/src/core/agent/tools/names.js +6 -0
  9. package/src/core/agent/tools/registry.js +9 -0
  10. package/src/core/agent/tools/tool-call-parser.js +70 -1
  11. package/src/core/channels/telegram/ask-callbacks.js +35 -0
  12. package/src/core/channels/telegram/dispatch.js +3 -0
  13. package/src/core/channels/telegram/reply.js +30 -5
  14. package/src/core/config/paths.js +3 -0
  15. package/src/core/config/redact.js +22 -0
  16. package/src/core/daemon/service.js +238 -0
  17. package/src/core/engines/gemini.js +322 -60
  18. package/src/core/engines/openai-compatible.js +21 -2
  19. package/src/core/memory/consolidate.js +225 -0
  20. package/src/core/nudge/index.js +192 -0
  21. package/src/core/nudge/policy.js +143 -0
  22. package/src/core/nudge/store.js +141 -0
  23. package/src/core/profiles/bundled/secretary/PROFILE.md +8 -9
  24. package/src/core/profiles/bundled/secretary/config.schema.json +33 -3
  25. package/src/core/profiles/bundled/secretary/routines/day-close.json +7 -3
  26. package/src/core/profiles/bundled/secretary/routines/day-open.json +7 -3
  27. package/src/core/profiles/bundled/secretary/routines/watch.json +13 -0
  28. package/src/core/routines/runner.js +102 -3
  29. package/src/core/routines/signals.js +270 -0
  30. package/src/core/stores/commitments.js +331 -0
  31. package/src/core/stores/messages.js +4 -0
  32. package/src/core/stores/routines.js +17 -3
  33. package/src/core/util/thinking.js +51 -0
  34. package/src/host/daemon/api/commitments.js +135 -0
  35. package/src/host/daemon/api/nudges.js +112 -0
  36. package/src/host/daemon/api/routines.js +24 -0
  37. package/src/host/daemon/api/self-memory.js +50 -0
  38. package/src/host/daemon/api/telegram.js +42 -4
  39. package/src/host/daemon/api/voice.js +3 -1
  40. package/src/host/daemon/api.js +6 -0
  41. package/src/host/daemon/callback-reconciler.js +16 -0
  42. package/src/host/daemon/plugins/desktop/index.js +7 -1
  43. package/src/host/daemon/plugins/telegram/index.js +7 -2
  44. package/src/host/daemon/wakeup.js +17 -3
  45. package/src/interfaces/cli/commands/commitment.js +154 -0
  46. package/src/interfaces/cli/commands/daemon.js +57 -0
  47. package/src/interfaces/cli/commands/memory.js +73 -0
  48. package/src/interfaces/cli/commands/nudge.js +130 -0
  49. package/src/interfaces/cli/help/index.js +2 -2
  50. package/src/interfaces/cli/routes/commitment.js +19 -0
  51. package/src/interfaces/cli/routes/daemon.js +7 -1
  52. package/src/interfaces/cli/routes/index.js +4 -0
  53. package/src/interfaces/cli/routes/memory.js +10 -2
  54. package/src/interfaces/cli/routes/nudge.js +17 -0
  55. package/src/interfaces/web/dist/assets/index-CvEoGtTf.js +849 -0
  56. package/src/interfaces/web/dist/assets/index-CvEoGtTf.js.map +1 -0
  57. package/src/interfaces/web/dist/assets/index-DzBBXFaO.css +1 -0
  58. package/src/interfaces/web/dist/index.html +2 -2
  59. package/src/interfaces/web/package-lock.json +11 -10
  60. package/src/interfaces/web/src/components/Section.tsx +18 -3
  61. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +13 -0
  62. package/src/interfaces/web/src/components/cron/CronPicker.tsx +196 -0
  63. package/src/interfaces/web/src/components/inbox/InboxList.tsx +145 -0
  64. package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +34 -4
  65. package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +16 -4
  66. package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +12 -2
  67. package/src/interfaces/web/src/components/routines/shared.ts +14 -5
  68. package/src/interfaces/web/src/components/settings/NudgePanel.tsx +183 -0
  69. package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +36 -12
  70. package/src/interfaces/web/src/components/ui/filter-chips.tsx +47 -0
  71. package/src/interfaces/web/src/components/ui.tsx +1 -0
  72. package/src/interfaces/web/src/constants/index.ts +1 -0
  73. package/src/interfaces/web/src/hooks/useChat.ts +5 -1
  74. package/src/interfaces/web/src/hooks/useNudges.ts +38 -0
  75. package/src/interfaces/web/src/i18n/en.ts +127 -0
  76. package/src/interfaces/web/src/i18n/es.ts +127 -0
  77. package/src/interfaces/web/src/lib/api/commitments.ts +57 -0
  78. package/src/interfaces/web/src/lib/api/notebook.ts +23 -0
  79. package/src/interfaces/web/src/lib/api/nudges.ts +53 -0
  80. package/src/interfaces/web/src/lib/cron.ts +196 -0
  81. package/src/interfaces/web/src/lib/when.ts +32 -0
  82. package/src/interfaces/web/src/screens/InboxScreen.tsx +107 -77
  83. package/src/interfaces/web/src/screens/ProjectScreen.tsx +5 -2
  84. package/src/interfaces/web/src/screens/SettingsScreen.tsx +17 -3
  85. package/src/interfaces/web/src/screens/base/CommitmentsTab.tsx +239 -0
  86. package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +102 -19
  87. package/src/interfaces/web/src/screens/base/LogsTab.tsx +15 -0
  88. package/src/interfaces/web/src/screens/project/ChatTab.tsx +21 -3
  89. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +13 -11
  90. package/src/interfaces/web/src/types/daemon.ts +10 -1
  91. package/src/interfaces/web/dist/assets/index-CBR_-QyA.js +0 -824
  92. package/src/interfaces/web/dist/assets/index-CBR_-QyA.js.map +0 -1
  93. package/src/interfaces/web/dist/assets/index-D_EJEA1n.css +0 -1
@@ -0,0 +1,154 @@
1
+ // apx commitment — what you promised, to whom, by when.
2
+ //
3
+ // apx commitment add "<what>" --to "<person>" [--due 2026-05-30] [--project X]
4
+ // [--channel telegram] [--ref <message id>]
5
+ // apx commitment list [--all | --project X] [--to X] [--state open|kept|missed|all]
6
+ // [--overdue] [--due-before ISO] [--limit N]
7
+ // apx commitment show <id> [--project X]
8
+ // apx commitment kept <id> [--project X] [--note "..."]
9
+ // apx commitment missed <id> [--project X] [--note "..."]
10
+ // apx commitment renegotiate <id> --due <ISO> [--project X] [--note "..."]
11
+ //
12
+ // A task is something to do; a commitment is something you promised a person.
13
+ // Kept separate on purpose — see core/stores/commitments.js.
14
+ import { http } from "../http.js";
15
+ import { resolveProjectId } from "./project.js";
16
+
17
+ export const COMMITMENT_USAGE = {
18
+ add: 'apx commitment add "<what>" --to "<person>" [--due ISO] [--project X] [--channel C] [--ref R]',
19
+ list: "apx commitment list [--all | --project X] [--to X] [--state open|kept|missed|all] [--overdue] [--due-before ISO] [--limit N]",
20
+ show: "apx commitment show <id> [--project X]",
21
+ kept: 'apx commitment kept <id> [--project X] [--note "..."]',
22
+ missed: 'apx commitment missed <id> [--project X] [--note "..."]',
23
+ renegotiate: 'apx commitment renegotiate <id> --due <ISO> [--project X] [--note "..."]',
24
+ };
25
+
26
+ function fail(sub, msg) {
27
+ console.error(`apx commitment ${sub}: ${msg}`);
28
+ console.error(`Usage: ${COMMITMENT_USAGE[sub]}`);
29
+ process.exit(1);
30
+ }
31
+
32
+ function shortDate(iso) {
33
+ if (!iso) return "";
34
+ return String(iso).replace(/T/, " ").replace(/Z$/, "").slice(0, 16);
35
+ }
36
+
37
+ /** Is this promise past its date and still open? */
38
+ function isOverdue(c) {
39
+ return c.state === "open" && c.due && c.due < new Date().toISOString();
40
+ }
41
+
42
+ function renderTable(rows, { showProject = false } = {}) {
43
+ if (!rows.length) {
44
+ console.log("(no commitments)");
45
+ return;
46
+ }
47
+ const idW = Math.max(...rows.map((r) => String(r.id).length), 2);
48
+ const whoW = Math.min(Math.max(...rows.map((r) => String(r.counterparty || "").length), 3), 20);
49
+ const projW = showProject
50
+ ? Math.min(Math.max(...rows.map((r) => String(r.project_name || "").length), 7), 18)
51
+ : 0;
52
+
53
+ for (const c of rows) {
54
+ const proj = showProject
55
+ ? String(c.project_name || "").slice(0, projW).padEnd(projW) + " "
56
+ : "";
57
+ // The flag carries the whole point of the type: a broken promise should be
58
+ // impossible to skim past.
59
+ const flag =
60
+ c.state === "kept" ? "✓" :
61
+ c.state === "missed" ? "✗" :
62
+ isOverdue(c) ? "!" : " ";
63
+ const moved = c.renegotiated_count ? ` (moved ×${c.renegotiated_count})` : "";
64
+ console.log(
65
+ `${flag} ${String(c.id).padEnd(idW)} ${proj}` +
66
+ `${String(c.counterparty || "").slice(0, whoW).padEnd(whoW)} ` +
67
+ `${(c.due ? shortDate(c.due).slice(0, 10) : "—").padEnd(10)} ` +
68
+ `${String(c.body || "").slice(0, 48)}${moved}`
69
+ );
70
+ }
71
+ const overdue = rows.filter(isOverdue).length;
72
+ if (overdue) console.log(`\n${overdue} past their date.`);
73
+ }
74
+
75
+ export async function cmdCommitmentAdd(args) {
76
+ const body = args?._?.[0];
77
+ const to = args?.flags?.to;
78
+ if (!body) fail("add", "what you promised is required");
79
+ if (!to) fail("add", "--to <person> is required — without a counterparty this is a task");
80
+
81
+ const pid = await resolveProjectId(args?.flags?.project);
82
+ const created = await http.post(`/api/projects/${pid}/commitments`, {
83
+ counterparty: to,
84
+ body,
85
+ due: args?.flags?.due || null,
86
+ origin_channel: args?.flags?.channel || "cli",
87
+ origin_message_ref: args?.flags?.ref || null,
88
+ });
89
+ console.log(`${created.id} → ${created.counterparty}${created.due ? ` by ${shortDate(created.due).slice(0, 10)}` : ""}`);
90
+ console.log(` ${created.body}`);
91
+ }
92
+
93
+ export async function cmdCommitmentList(args) {
94
+ const f = args?.flags || {};
95
+ const q = new URLSearchParams();
96
+ if (f.state) q.set("state", f.state);
97
+ if (f.to) q.set("counterparty", f.to);
98
+ if (f.overdue) q.set("overdue", "1");
99
+ if (f["due-before"]) q.set("due_before", f["due-before"]);
100
+ if (f.limit) q.set("limit", f.limit);
101
+
102
+ if (f.all) {
103
+ const { data } = await http.get(`/api/commitments?${q.toString()}`);
104
+ renderTable(data || [], { showProject: true });
105
+ return;
106
+ }
107
+ const pid = await resolveProjectId(args?.flags?.project);
108
+ const { data } = await http.get(`/api/projects/${pid}/commitments?${q.toString()}`);
109
+ renderTable(data || []);
110
+ }
111
+
112
+ export async function cmdCommitmentShow(args) {
113
+ const id = args?._?.[0];
114
+ if (!id) fail("show", "id required");
115
+ const pid = await resolveProjectId(args?.flags?.project);
116
+ const c = await http.get(`/api/projects/${pid}/commitments/${encodeURIComponent(id)}`);
117
+ console.log(`${c.id} [${c.state}${isOverdue(c) ? " · OVERDUE" : ""}]`);
118
+ console.log(` To: ${c.counterparty}`);
119
+ console.log(` What: ${c.body}`);
120
+ console.log(` Promised: ${shortDate(c.promised_at)}${c.origin_channel ? ` on ${c.origin_channel}` : ""}`);
121
+ console.log(` Due: ${c.due ? shortDate(c.due) : "(no date)"}`);
122
+ if (c.note) console.log(` Note: ${c.note}`);
123
+ // The history is the relationship record — print it, it is the reason the
124
+ // renegotiate event exists at all.
125
+ for (const h of c.history || []) {
126
+ console.log(` Moved: ${shortDate(h.due)?.slice(0, 10) || "?"} → (on ${shortDate(h.moved_at)})${h.note ? ` — ${h.note}` : ""}`);
127
+ }
128
+ }
129
+
130
+ async function close(args, sub) {
131
+ const id = args?._?.[0];
132
+ if (!id) fail(sub, "id required");
133
+ const pid = await resolveProjectId(args?.flags?.project);
134
+ const c = await http.post(`/api/projects/${pid}/commitments/${encodeURIComponent(id)}/${sub}`, {
135
+ note: args?.flags?.note || null,
136
+ });
137
+ console.log(`${c.id} → ${c.state}`);
138
+ }
139
+
140
+ export const cmdCommitmentKept = (args) => close(args, "kept");
141
+ export const cmdCommitmentMissed = (args) => close(args, "missed");
142
+
143
+ export async function cmdCommitmentRenegotiate(args) {
144
+ const id = args?._?.[0];
145
+ const due = args?.flags?.due;
146
+ if (!id) fail("renegotiate", "id required");
147
+ if (!due) fail("renegotiate", "--due <ISO> is required — a promise with no new date is a promise that vanished");
148
+ const pid = await resolveProjectId(args?.flags?.project);
149
+ const c = await http.post(`/api/projects/${pid}/commitments/${encodeURIComponent(id)}/renegotiate`, {
150
+ due,
151
+ note: args?.flags?.note || null,
152
+ });
153
+ console.log(`${c.id} → new date ${shortDate(c.due).slice(0, 10)} (moved ×${c.renegotiated_count})`);
154
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import { PID_PATH, LOG_PATH } from "#core/config/paths.js";
3
3
  import { ensureDaemon, http } from "../http.js";
4
+ import { installService, uninstallService, serviceStatus } from "#core/daemon/service.js";
4
5
 
5
6
  // Wait until nothing answers on the daemon port (old process fully exited and
6
7
  // released it), so a fresh start can bind it. Resolves true when down.
@@ -244,3 +245,59 @@ export async function cmdDaemonLogs(args) {
244
245
  return new Promise(() => {});
245
246
  }
246
247
  }
248
+
249
+ // ── Service (opt-in supervision) ─────────────────────────────────────────────
250
+
251
+ export async function cmdDaemonInstallService() {
252
+ const before = serviceStatus();
253
+ if (!before.supervised && before.platform !== "win32") {
254
+ console.error(`apx daemon install-service: ${before.note || "not supported here"}`);
255
+ process.exit(1);
256
+ }
257
+
258
+ const r = installService();
259
+ if (!r.ok) {
260
+ console.error(`apx daemon install-service: ${r.error}`);
261
+ process.exit(1);
262
+ }
263
+
264
+ console.log(`\n ${fmt.green("●")} ${fmt.bold("apx daemon")} is now a service`);
265
+ console.log(` ${fmt.gray("·")} ${fmt.cyan("unit")} ${r.path}`);
266
+ if (r.log) console.log(` ${fmt.gray("·")} ${fmt.cyan("log")} ${r.log}`);
267
+ console.log("");
268
+ if (r.supervised) {
269
+ console.log(" If it dies, it comes back. It survives a reboot.");
270
+ } else {
271
+ // Never let the headline promise more than the platform delivers.
272
+ console.log(` ${fmt.yellow("Note")}: ${r.note}`);
273
+ }
274
+ if (r.note && r.supervised) console.log(` ${fmt.yellow("Note")}: ${r.note}`);
275
+ console.log(`\n Undo any time: ${fmt.cyan("apx daemon uninstall-service")}\n`);
276
+ }
277
+
278
+ export async function cmdDaemonUninstallService() {
279
+ const r = uninstallService();
280
+ if (!r.ok) {
281
+ console.error(`apx daemon uninstall-service: ${r.error}`);
282
+ process.exit(1);
283
+ }
284
+ console.log(r.removed
285
+ ? `\n service removed — ${r.path}\n The daemon keeps working; it just is not supervised any more.\n`
286
+ : "\n no service was installed — nothing to remove\n");
287
+ }
288
+
289
+ export async function cmdDaemonServiceStatus() {
290
+ const s = serviceStatus();
291
+ if (!s.installed) {
292
+ console.log(`\n daemon service: ${fmt.dim("not installed")}`);
293
+ console.log(" The daemon starts when a command needs it, and nothing restarts it if it dies.");
294
+ console.log(` Install: ${fmt.cyan("apx daemon install-service")}\n`);
295
+ if (s.note) console.log(` ${fmt.yellow("Note")}: ${s.note}\n`);
296
+ return;
297
+ }
298
+ console.log(`\n daemon service: ${fmt.green("installed")} ${fmt.gray("·")} ${s.path}`);
299
+ console.log(s.supervised
300
+ ? " Supervised: it restarts on its own and survives a reboot."
301
+ : ` ${fmt.yellow("Not supervised")}: ${s.note}`);
302
+ console.log("");
303
+ }
@@ -2,6 +2,10 @@ import fs from "node:fs";
2
2
  import { findApfRoot } from "#core/apc/parser.js";
3
3
  import { agentMemoryPath, readAgentMemory, writeAgentMemory, ensureAgentRuntimeDir } from "#core/agent/memory.js";
4
4
  import { http } from "../http.js";
5
+ import {
6
+ proposeConsolidation, applyConsolidation, revertConsolidation, notebookSize,
7
+ } from "#core/memory/consolidate.js";
8
+ import { readSelfMemory } from "#core/agent/self-memory.js";
5
9
 
6
10
  function requireRoot() {
7
11
  const root = findApfRoot();
@@ -66,3 +70,72 @@ function readStdinSync() {
66
70
  } catch {}
67
71
  return chunks.join("");
68
72
  }
73
+
74
+
75
+ // ── The super-agent's own notebook (~/.apx/memory.md) ────────────────────────
76
+ //
77
+ // Distinct from `apx memory <agent-slug>` above, which is a PROJECT agent's
78
+ // memory. This one ships in every super-agent prompt on every channel, which
79
+ // is why its size is worth showing and its growth worth controlling.
80
+
81
+ export async function cmdMemoryNotebook() {
82
+ const s = notebookSize();
83
+ console.log(`\nnotebook: ${s.entries} entries · ${s.chars} chars · ~${s.approx_tokens} tokens`);
84
+ // The number that matters: this is paid on every turn, of every channel.
85
+ console.log(` ${s.consolidated} of them written by consolidation.`);
86
+ console.log(" This file is injected into every super-agent prompt.\n");
87
+ const body = readSelfMemory();
88
+ if (body.trim()) process.stdout.write(body.endsWith("\n") ? body : body + "\n");
89
+ }
90
+
91
+ /**
92
+ * apx memory consolidate [--apply] [--limit N]
93
+ *
94
+ * Candidates arrive on STDIN, one per line. The DISTILLING is the caller's job
95
+ * — a routine with a model behind it, or a person — and the JUDGEMENT about
96
+ * what survives lives in core, so the same rules apply whoever proposes.
97
+ *
98
+ * Proposes by default. Writing needs --apply, because a background job that
99
+ * silently edits the file the agent believes about itself is not something to
100
+ * switch on quietly.
101
+ */
102
+ export async function cmdMemoryConsolidate(args) {
103
+ const raw = readStdinSync();
104
+ const candidates = raw.split("\n").map((l) => l.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
105
+ if (!candidates.length) {
106
+ console.error("apx memory consolidate: no candidates on stdin (one fact per line)");
107
+ process.exit(1);
108
+ }
109
+
110
+ const limits = args?.flags?.limit ? { max_candidates: Number(args.flags.limit) } : undefined;
111
+ const { kept, rejected } = proposeConsolidation(candidates, limits ? { limits } : {});
112
+
113
+ if (!kept.length) {
114
+ console.log("nothing worth saving.");
115
+ for (const r of rejected) console.log(` skipped: ${r.reason} — ${r.text.slice(0, 60)}`);
116
+ return;
117
+ }
118
+
119
+ if (!args?.flags?.apply) {
120
+ console.log(`would save ${kept.length}:`);
121
+ for (const k of kept) console.log(` + ${k}`);
122
+ if (rejected.length) {
123
+ console.log(`\nskipped ${rejected.length}:`);
124
+ for (const r of rejected) console.log(` - ${r.reason} — ${r.text.slice(0, 60)}`);
125
+ }
126
+ console.log("\nNothing was written. Add --apply to save.");
127
+ return;
128
+ }
129
+
130
+ const { written } = applyConsolidation(kept);
131
+ console.log(`saved ${written.length} to the notebook:`);
132
+ for (const w of written) console.log(` + ${w}`);
133
+ console.log("\nUndo: apx memory revert");
134
+ }
135
+
136
+ export async function cmdMemoryRevert(args) {
137
+ const { removed } = revertConsolidation(args?.flags?.since ? { since: String(args.flags.since) } : {});
138
+ console.log(removed
139
+ ? `removed ${removed} consolidated ${removed === 1 ? "entry" : "entries"} — hand-written notes untouched`
140
+ : "nothing to revert (no consolidated entries)");
141
+ }
@@ -0,0 +1,130 @@
1
+ // apx nudge — the interruption budget: what APX said without being asked,
2
+ // what you thought of it, and how much room is left.
3
+ //
4
+ // apx nudge status
5
+ // apx nudge list [--limit N] [--kind K] [--project P] [--rated | --unrated]
6
+ // apx nudge set [--enabled true|false] [--daily-max N] [--quiet 22:00-07:30]
7
+ // [--cooldown N] [--project-cooldown N] [--kind-cooldown N]
8
+ // apx nudge check --kind K [--severity critical]
9
+ // apx nudge feedback <id> --useful | --noise [--note "..."]
10
+ import { http } from "../http.js";
11
+
12
+ export const NUDGE_USAGE = {
13
+ status: "apx nudge status",
14
+ list: "apx nudge list [--limit N] [--kind K] [--project P] [--rated|--unrated]",
15
+ set: "apx nudge set [--enabled true|false] [--daily-max N] [--quiet HH:MM-HH:MM] [--cooldown N] [--project-cooldown N] [--kind-cooldown N]",
16
+ check: "apx nudge check --kind K [--severity low|normal|high|critical]",
17
+ feedback: 'apx nudge feedback <id> --useful|--noise [--note "..."]',
18
+ };
19
+
20
+ function fail(sub, msg) {
21
+ console.error(`apx nudge ${sub}: ${msg}`);
22
+ console.error(`Usage: ${NUDGE_USAGE[sub]}`);
23
+ process.exit(1);
24
+ }
25
+
26
+ function shortTs(iso) {
27
+ if (!iso) return "";
28
+ return String(iso).replace(/T/, " ").replace(/Z$/, "").slice(0, 16);
29
+ }
30
+
31
+ export async function cmdNudgeStatus() {
32
+ const { policy, source, user_overrides } = await http.get("/api/nudges/policy");
33
+ const { meta } = await http.get("/api/nudges?limit=1");
34
+ const stats = meta?.stats || { total: 0, today: 0, rated: 0, by_kind: [] };
35
+
36
+ if (!policy.enabled) {
37
+ console.log("interruption budget: OFF — every unrequested message goes out.");
38
+ console.log(" Nothing is blocked, but everything is still recorded below.");
39
+ console.log(" Turn it on: apx nudge set --enabled true --daily-max 3");
40
+ } else {
41
+ const left = policy.daily_max > 0 ? Math.max(0, policy.daily_max - stats.today) : "∞";
42
+ console.log(`interruption budget: ON — ${stats.today} sent today, ${left} left`);
43
+ if (policy.daily_max > 0) console.log(` Daily max: ${policy.daily_max}`);
44
+ if (policy.quiet_hours) console.log(` Quiet hours: ${policy.quiet_hours}`);
45
+ if (policy.cooldown_minutes) console.log(` Cooldown: ${policy.cooldown_minutes}m between any two`);
46
+ if (policy.project_cooldown_minutes) console.log(` Per project: ${policy.project_cooldown_minutes}m`);
47
+ if (policy.kind_cooldown_minutes) console.log(` Per kind: ${policy.kind_cooldown_minutes}m`);
48
+ console.log(` Critical: ${policy.critical_bypasses_budget ? "may bypass (logged)" : "no bypass"}`);
49
+ }
50
+ // Provenance matters: a number the user did not choose should say who did.
51
+ console.log(` Set by: ${source.join(" → ")}`);
52
+ if (Object.keys(user_overrides || {}).length) {
53
+ console.log(` Your overrides: ${Object.entries(user_overrides).map(([k, v]) => `${k}=${v}`).join(", ")}`);
54
+ }
55
+
56
+ console.log("");
57
+ console.log(`recorded: ${stats.total} total, ${stats.rated} rated`);
58
+ for (const k of stats.by_kind.slice(0, 8)) {
59
+ const verdict = k.useful || k.noise ? ` (👍 ${k.useful} / 👎 ${k.noise})` : "";
60
+ console.log(` ${k.kind.padEnd(18)} ${String(k.sent).padStart(4)}${verdict}`);
61
+ }
62
+ }
63
+
64
+ export async function cmdNudgeList(args) {
65
+ const q = new URLSearchParams();
66
+ if (args?.flags?.limit) q.set("limit", args.flags.limit);
67
+ if (args?.flags?.kind) q.set("kind", args.flags.kind);
68
+ if (args?.flags?.project) q.set("project_id", args.flags.project);
69
+ if (args?.flags?.rated) q.set("with_feedback", "1");
70
+ if (args?.flags?.unrated) q.set("with_feedback", "0");
71
+
72
+ const { data } = await http.get(`/api/nudges?${q.toString()}`);
73
+ if (!data?.length) {
74
+ console.log("(nothing sent unprompted yet)");
75
+ return;
76
+ }
77
+ const idW = Math.max(...data.map((r) => r.id.length), 2);
78
+ const kindW = Math.max(...data.map((r) => String(r.kind).length), 4);
79
+ for (const row of data) {
80
+ const rating = row.feedback ? (row.feedback.useful ? "👍" : "👎") : " ";
81
+ const flag = row.bypassed_budget ? " ⚠︎bypass" : "";
82
+ console.log(
83
+ `${row.id.padEnd(idW)} ${shortTs(row.at)} ${rating} ` +
84
+ `${String(row.kind).padEnd(kindW)} ${String(row.preview || "").slice(0, 60)}${flag}`
85
+ );
86
+ }
87
+ }
88
+
89
+ export async function cmdNudgeSet(args) {
90
+ const f = args?.flags || {};
91
+ const body = {};
92
+ if (f.enabled !== undefined) body.enabled = String(f.enabled) !== "false";
93
+ if (f["daily-max"] !== undefined) body.daily_max = Number(f["daily-max"]);
94
+ if (f.quiet !== undefined) body.quiet_hours = String(f.quiet);
95
+ if (f.cooldown !== undefined) body.cooldown_minutes = Number(f.cooldown);
96
+ if (f["project-cooldown"] !== undefined) body.project_cooldown_minutes = Number(f["project-cooldown"]);
97
+ if (f["kind-cooldown"] !== undefined) body.kind_cooldown_minutes = Number(f["kind-cooldown"]);
98
+ if (!Object.keys(body).length) fail("set", "nothing to set");
99
+
100
+ const { policy, source } = await http.put("/api/nudges/policy", body);
101
+ console.log(`budget updated (${source.join(" → ")})`);
102
+ console.log(` enabled: ${policy.enabled} · daily max: ${policy.daily_max || "∞"} · quiet: ${policy.quiet_hours || "none"}`);
103
+ console.log(" Applies to the next unrequested message; nothing to restart.");
104
+ }
105
+
106
+ export async function cmdNudgeCheck(args) {
107
+ const kind = args?.flags?.kind;
108
+ if (!kind) fail("check", "--kind required");
109
+ const r = await http.post("/api/nudges/check", {
110
+ kind,
111
+ severity: args?.flags?.severity || "normal",
112
+ project_id: args?.flags?.project || null,
113
+ });
114
+ console.log(r.allowed ? `would send — ${r.reason}` : `would be held — ${r.reason}`);
115
+ if (!r.allowed && r.retry_after_ms) {
116
+ console.log(` Retry in ~${Math.ceil(r.retry_after_ms / 60000)} min.`);
117
+ }
118
+ }
119
+
120
+ export async function cmdNudgeFeedback(args) {
121
+ const id = args?._?.[0];
122
+ if (!id) fail("feedback", "nudge id required");
123
+ const f = args?.flags || {};
124
+ if (!f.useful && !f.noise) fail("feedback", "--useful or --noise required");
125
+ await http.post(`/api/nudges/${encodeURIComponent(id)}/feedback`, {
126
+ useful: !!f.useful,
127
+ note: f.note || "",
128
+ });
129
+ console.log(f.useful ? "noted: useful" : "noted: not useful");
130
+ }
@@ -1138,7 +1138,7 @@ export const HELP_TOPICS = new Map(Object.entries({
1138
1138
  summary: "Create a scheduled routine.",
1139
1139
  usage: ["apx routine add <name> --kind <kind> --schedule <schedule> [--spec '<json>'] [--pre-commands 'cmd1,cmd2'] [--post-commands 'cmd'] [--skip-prompt-on signal|pre_failure|pre_success|always|never] [--project <name|id|path>]"],
1140
1140
  options: [
1141
- ["--kind <kind>", "heartbeat, exec_agent, super_agent, telegram, or shell."],
1141
+ ["--kind <kind>", "heartbeat, exec_agent, super_agent, telegram, shell, or watch."],
1142
1142
  ["--schedule <schedule>", "every:60s, every:5m, every:1h, or once:<iso>."],
1143
1143
  ["--spec '<json>'", "Routine-specific JSON config."],
1144
1144
  ["--pre-commands 'cmd'", "Comma-separated shell commands to run BEFORE the LLM. Use 'artifact:<name>' shorthand."],
@@ -2108,7 +2108,7 @@ export function buildHelp(version) {
2108
2108
  hSec("Routines & Pipeline"),
2109
2109
  hCmd("apx routine list", 36, "list routines + next/last run"),
2110
2110
  hCmd("apx routine add <name>", 36, "--kind K --schedule S [--spec '{...}']"),
2111
- ` ${H.DI}kinds: heartbeat | exec_agent | super_agent | telegram | shell${H.R}`,
2111
+ ` ${H.DI}kinds: heartbeat | exec_agent | super_agent | telegram | shell | watch${H.R}`,
2112
2112
  ` ${H.DI}flags: --permission-mode total|automatico|permiso --allowed-tools a,b${H.R}`,
2113
2113
  ` ${H.DI}pipeline: --pre-commands 'cmd1,cmd2' --post-commands 'cmd'${H.R}`,
2114
2114
  ` ${H.DI} --skip-prompt-on signal|pre_failure|pre_success|always|never${H.R}`,
@@ -0,0 +1,19 @@
1
+ // apx commitment — argument routing.
2
+ import {
3
+ cmdCommitmentAdd, cmdCommitmentList, cmdCommitmentShow,
4
+ cmdCommitmentKept, cmdCommitmentMissed, cmdCommitmentRenegotiate,
5
+ } from "../commands/commitment.js";
6
+
7
+ export const aliases = ["commitments"];
8
+
9
+ export default async function route(rest, { parseArgs, die }) {
10
+ const sub = rest[0];
11
+ const a = parseArgs(rest.slice(1));
12
+ if (!sub || sub === "list" || sub === "ls") await cmdCommitmentList(a);
13
+ else if (sub === "add" || sub === "new") await cmdCommitmentAdd(a);
14
+ else if (sub === "show" || sub === "get") await cmdCommitmentShow(a);
15
+ else if (sub === "kept" || sub === "done") await cmdCommitmentKept(a);
16
+ else if (sub === "missed") await cmdCommitmentMissed(a);
17
+ else if (sub === "renegotiate" || sub === "move") await cmdCommitmentRenegotiate(a);
18
+ else die(`unknown commitment subcommand: ${sub}\nUsage: apx commitment <list|add|show|kept|missed|renegotiate>`);
19
+ }
@@ -4,7 +4,10 @@
4
4
  // owns its own routing and imports only the command functions it calls, so the
5
5
  // CLI no longer loads all 38 command modules to run one of them.
6
6
 
7
- import { cmdDaemonLogs, cmdDaemonReload, cmdDaemonRestart, cmdDaemonStart, cmdDaemonStatus, cmdDaemonStop } from "../commands/daemon.js";
7
+ import {
8
+ cmdDaemonLogs, cmdDaemonReload, cmdDaemonRestart, cmdDaemonStart, cmdDaemonStatus, cmdDaemonStop,
9
+ cmdDaemonInstallService, cmdDaemonUninstallService, cmdDaemonServiceStatus,
10
+ } from "../commands/daemon.js";
8
11
 
9
12
  export default async function route(rest, { parseArgs, die }) {
10
13
  const sub = rest[0];
@@ -15,5 +18,8 @@ export default async function route(rest, { parseArgs, die }) {
15
18
  else if (sub === "reload") await cmdDaemonReload(a);
16
19
  else if (sub === "status") await cmdDaemonStatus(a);
17
20
  else if (sub === "logs") cmdDaemonLogs(a);
21
+ else if (sub === "install-service") await cmdDaemonInstallService(a);
22
+ else if (sub === "uninstall-service") await cmdDaemonUninstallService(a);
23
+ else if (sub === "service-status") await cmdDaemonServiceStatus(a);
18
24
  else die(`unknown daemon subcommand: ${sub || "(none)"}`);
19
25
  }
@@ -44,9 +44,13 @@ export const ROUTES = Object.freeze({
44
44
  "artifacts": () => import("./artifact.js"),
45
45
  "task": () => import("./task.js"),
46
46
  "tasks": () => import("./task.js"),
47
+ "commitment": () => import("./commitment.js"),
48
+ "commitments": () => import("./commitment.js"),
47
49
  "panel": () => import("./panel.js"),
48
50
  "profile": () => import("./profile.js"),
49
51
  "profiles": () => import("./profile.js"),
52
+ "nudge": () => import("./nudge.js"),
53
+ "nudges": () => import("./nudge.js"),
50
54
  "command": () => import("./command.js"),
51
55
  "commands": () => import("./command.js"),
52
56
  "org": () => import("./org.js"),
@@ -4,8 +4,16 @@
4
4
  // owns its own routing and imports only the command functions it calls, so the
5
5
  // CLI no longer loads all 38 command modules to run one of them.
6
6
 
7
- import { cmdMemory } from "../commands/memory.js";
7
+ import { cmdMemory, cmdMemoryNotebook, cmdMemoryConsolidate, cmdMemoryRevert } from "../commands/memory.js";
8
8
 
9
9
  export default async function route(rest, { parseArgs }) {
10
- cmdMemory(parseArgs(rest));
10
+ // `apx memory <agent-slug>` is the original form and stays the default, so
11
+ // these three names are reserved. An agent literally called "notebook" would
12
+ // be shadowed; that is a trade worth making for a readable command.
13
+ const sub = rest[0];
14
+ const a = parseArgs(rest.slice(1));
15
+ if (sub === "notebook") return cmdMemoryNotebook(a);
16
+ if (sub === "consolidate") return cmdMemoryConsolidate(a);
17
+ if (sub === "revert") return cmdMemoryRevert(a);
18
+ return cmdMemory(parseArgs(rest));
11
19
  }
@@ -0,0 +1,17 @@
1
+ // apx nudge — argument routing for the interruption budget.
2
+ import {
3
+ cmdNudgeStatus, cmdNudgeList, cmdNudgeSet, cmdNudgeCheck, cmdNudgeFeedback,
4
+ } from "../commands/nudge.js";
5
+
6
+ export const aliases = ["nudges"];
7
+
8
+ export default async function route(rest, { parseArgs, die }) {
9
+ const sub = rest[0];
10
+ const a = parseArgs(rest.slice(1));
11
+ if (!sub || sub === "status") await cmdNudgeStatus(a);
12
+ else if (sub === "list" || sub === "ls") await cmdNudgeList(a);
13
+ else if (sub === "set" || sub === "config") await cmdNudgeSet(a);
14
+ else if (sub === "check") await cmdNudgeCheck(a);
15
+ else if (sub === "feedback") await cmdNudgeFeedback(a);
16
+ else die(`unknown nudge subcommand: ${sub}\nUsage: apx nudge <status|list|set|check|feedback>`);
17
+ }