@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.
- package/package.json +1 -1
- package/src/core/agent/run-agent.js +30 -5
- package/src/core/agent/tool-summary.js +65 -0
- package/src/core/agent/tools/handlers/list-commitments.js +80 -0
- package/src/core/agent/tools/handlers/list-tasks.js +66 -27
- package/src/core/agent/tools/handlers/record-commitment.js +68 -0
- package/src/core/agent/tools/handlers/send-telegram.js +68 -2
- package/src/core/agent/tools/names.js +6 -0
- package/src/core/agent/tools/registry.js +9 -0
- package/src/core/agent/tools/tool-call-parser.js +70 -1
- package/src/core/channels/telegram/ask-callbacks.js +35 -0
- package/src/core/channels/telegram/dispatch.js +3 -0
- package/src/core/channels/telegram/reply.js +30 -5
- package/src/core/config/paths.js +3 -0
- package/src/core/config/redact.js +22 -0
- package/src/core/daemon/service.js +238 -0
- package/src/core/engines/gemini.js +322 -60
- package/src/core/engines/openai-compatible.js +21 -2
- package/src/core/memory/consolidate.js +225 -0
- package/src/core/nudge/index.js +192 -0
- package/src/core/nudge/policy.js +143 -0
- package/src/core/nudge/store.js +141 -0
- package/src/core/profiles/bundled/secretary/PROFILE.md +8 -9
- package/src/core/profiles/bundled/secretary/config.schema.json +33 -3
- package/src/core/profiles/bundled/secretary/routines/day-close.json +7 -3
- package/src/core/profiles/bundled/secretary/routines/day-open.json +7 -3
- package/src/core/profiles/bundled/secretary/routines/watch.json +13 -0
- package/src/core/routines/runner.js +102 -3
- package/src/core/routines/signals.js +270 -0
- package/src/core/stores/commitments.js +331 -0
- package/src/core/stores/messages.js +4 -0
- package/src/core/stores/routines.js +17 -3
- package/src/core/util/thinking.js +51 -0
- package/src/host/daemon/api/commitments.js +135 -0
- package/src/host/daemon/api/nudges.js +112 -0
- package/src/host/daemon/api/routines.js +24 -0
- package/src/host/daemon/api/self-memory.js +50 -0
- package/src/host/daemon/api/telegram.js +42 -4
- package/src/host/daemon/api/voice.js +3 -1
- package/src/host/daemon/api.js +6 -0
- package/src/host/daemon/callback-reconciler.js +16 -0
- package/src/host/daemon/plugins/desktop/index.js +7 -1
- package/src/host/daemon/plugins/telegram/index.js +7 -2
- package/src/host/daemon/wakeup.js +17 -3
- package/src/interfaces/cli/commands/commitment.js +154 -0
- package/src/interfaces/cli/commands/daemon.js +57 -0
- package/src/interfaces/cli/commands/memory.js +73 -0
- package/src/interfaces/cli/commands/nudge.js +130 -0
- package/src/interfaces/cli/help/index.js +2 -2
- package/src/interfaces/cli/routes/commitment.js +19 -0
- package/src/interfaces/cli/routes/daemon.js +7 -1
- package/src/interfaces/cli/routes/index.js +4 -0
- package/src/interfaces/cli/routes/memory.js +10 -2
- package/src/interfaces/cli/routes/nudge.js +17 -0
- package/src/interfaces/web/dist/assets/index-CvEoGtTf.js +849 -0
- package/src/interfaces/web/dist/assets/index-CvEoGtTf.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-DzBBXFaO.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +11 -10
- package/src/interfaces/web/src/components/Section.tsx +18 -3
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +13 -0
- package/src/interfaces/web/src/components/cron/CronPicker.tsx +196 -0
- package/src/interfaces/web/src/components/inbox/InboxList.tsx +145 -0
- package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +34 -4
- package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +16 -4
- package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +12 -2
- package/src/interfaces/web/src/components/routines/shared.ts +14 -5
- package/src/interfaces/web/src/components/settings/NudgePanel.tsx +183 -0
- package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +36 -12
- package/src/interfaces/web/src/components/ui/filter-chips.tsx +47 -0
- package/src/interfaces/web/src/components/ui.tsx +1 -0
- package/src/interfaces/web/src/constants/index.ts +1 -0
- package/src/interfaces/web/src/hooks/useChat.ts +5 -1
- package/src/interfaces/web/src/hooks/useNudges.ts +38 -0
- package/src/interfaces/web/src/i18n/en.ts +127 -0
- package/src/interfaces/web/src/i18n/es.ts +127 -0
- package/src/interfaces/web/src/lib/api/commitments.ts +57 -0
- package/src/interfaces/web/src/lib/api/notebook.ts +23 -0
- package/src/interfaces/web/src/lib/api/nudges.ts +53 -0
- package/src/interfaces/web/src/lib/cron.ts +196 -0
- package/src/interfaces/web/src/lib/when.ts +32 -0
- package/src/interfaces/web/src/screens/InboxScreen.tsx +107 -77
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +5 -2
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +17 -3
- package/src/interfaces/web/src/screens/base/CommitmentsTab.tsx +239 -0
- package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +102 -19
- package/src/interfaces/web/src/screens/base/LogsTab.tsx +15 -0
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +21 -3
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +13 -11
- package/src/interfaces/web/src/types/daemon.ts +10 -1
- package/src/interfaces/web/dist/assets/index-CBR_-QyA.js +0 -824
- package/src/interfaces/web/dist/assets/index-CBR_-QyA.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-D_EJEA1n.css +0 -1
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
// will TypeScript-error on unknown keys.
|
|
4
4
|
|
|
5
5
|
export const es = {
|
|
6
|
+
cron: {
|
|
7
|
+
daily: "todos los días a las {at}",
|
|
8
|
+
weekdays: "de lunes a viernes a las {at}",
|
|
9
|
+
weekends: "fines de semana a las {at}",
|
|
10
|
+
on_days: "{days} a las {at}",
|
|
11
|
+
monthly: "el día {day} de cada mes a las {at}",
|
|
12
|
+
every_hour: "cada hora, al minuto {minute}",
|
|
13
|
+
every_n_hours: "cada {n} horas, al minuto {minute}",
|
|
14
|
+
every_n_minutes: "cada {n} minutos",
|
|
15
|
+
every_minute: "cada minuto",
|
|
16
|
+
every_label: "cada N horas en vez de una hora fija",
|
|
17
|
+
use_cron: "editar como cron",
|
|
18
|
+
use_picker: "usar el selector",
|
|
19
|
+
preset_every_day: "todos los días",
|
|
20
|
+
preset_weekdays: "días de semana",
|
|
21
|
+
preset_weekends: "fin de semana",
|
|
22
|
+
day_short: { sun: "Dom", mon: "Lun", tue: "Mar", wed: "Mié", thu: "Jue", fri: "Vie", sat: "Sáb" },
|
|
23
|
+
},
|
|
24
|
+
when: {
|
|
25
|
+
now: "ahora",
|
|
26
|
+
in: "en {amount}",
|
|
27
|
+
ago: "hace {amount}",
|
|
28
|
+
minutes: "{n} min",
|
|
29
|
+
hours: "{n} h",
|
|
30
|
+
days: "{n} d",
|
|
31
|
+
},
|
|
6
32
|
common: {
|
|
7
33
|
loading: "Cargando…",
|
|
8
34
|
saving: "Guardando…",
|
|
@@ -144,6 +170,11 @@ export const es = {
|
|
|
144
170
|
no_folders: "Sin carpetas.",
|
|
145
171
|
},
|
|
146
172
|
inbox: {
|
|
173
|
+
search: "Buscar",
|
|
174
|
+
no_match: "No coincide nada",
|
|
175
|
+
hide_quiet: "Ocultar agentes callados",
|
|
176
|
+
open_in_project: "Abrir en el proyecto",
|
|
177
|
+
super_agent_scope: "Todos los proyectos",
|
|
147
178
|
title: "Bandeja de agentes",
|
|
148
179
|
subtitle:
|
|
149
180
|
"Cada agente como una conversación, lo más reciente primero.",
|
|
@@ -196,6 +227,8 @@ export const es = {
|
|
|
196
227
|
devices_revoke: "Revocar",
|
|
197
228
|
account_section: "Cuenta",
|
|
198
229
|
agents_section: "Agentes & modelos",
|
|
230
|
+
super_agent_section: "Super-agente",
|
|
231
|
+
knowledge_section: "Modelos y conocimiento",
|
|
199
232
|
channels_section: "Canales & dispositivos",
|
|
200
233
|
modules_section: "Módulos",
|
|
201
234
|
advanced_section: "Avanzado",
|
|
@@ -207,6 +240,7 @@ export const es = {
|
|
|
207
240
|
engines: "Engines & modelos",
|
|
208
241
|
telegram: "Telegram",
|
|
209
242
|
devices: "Dispositivos",
|
|
243
|
+
nudge: "Interrupciones",
|
|
210
244
|
advanced: "Avanzado",
|
|
211
245
|
},
|
|
212
246
|
|
|
@@ -223,6 +257,9 @@ export const es = {
|
|
|
223
257
|
activate: "Activar",
|
|
224
258
|
replace_active: "Reemplazar el activo",
|
|
225
259
|
deactivate: "Desactivar",
|
|
260
|
+
on: "Activo",
|
|
261
|
+
off: "Inactivo",
|
|
262
|
+
replaces_active: "Activarlo reemplaza al perfil activo",
|
|
226
263
|
deactivate_title: "¿Desactivar el perfil?",
|
|
227
264
|
deactivate_confirm:
|
|
228
265
|
"APX vuelve a vanilla. Las rutinas del perfil se deshabilitan pero no se borran, y tu configuración, tareas y memoria quedan intactas: volver a activarlo restaura todo.",
|
|
@@ -246,6 +283,34 @@ export const es = {
|
|
|
246
283
|
doctor_vanilla: "Sin perfil activo. APX se comporta como siempre.",
|
|
247
284
|
},
|
|
248
285
|
|
|
286
|
+
nudge: {
|
|
287
|
+
title: "Presupuesto de interrupciones",
|
|
288
|
+
subtitle:
|
|
289
|
+
"Cada cuánto puede escribirte APX sin que se lo pidas. Las respuestas a tus propios mensajes nunca se retienen.",
|
|
290
|
+
off_hint:
|
|
291
|
+
"El presupuesto está apagado: sale todo mensaje no solicitado. Igual queda registrado acá abajo.",
|
|
292
|
+
on_hint: "{{sent}} enviados hoy, quedan {{left}}.",
|
|
293
|
+
source: "Definido por",
|
|
294
|
+
enabled: "Aplicar el presupuesto",
|
|
295
|
+
daily_max: "Mensajes por día",
|
|
296
|
+
daily_max_hint: "0 significa sin techo.",
|
|
297
|
+
quiet_hours: "Horas de silencio",
|
|
298
|
+
quiet_hours_hint: "HH:MM-HH:MM. Cruza la medianoche sin problema. Vacío = ninguna.",
|
|
299
|
+
cooldown: "Espacio mínimo (minutos)",
|
|
300
|
+
project_cooldown: "Espacio mínimo por proyecto (minutos)",
|
|
301
|
+
kind_cooldown: "Espacio mínimo por tipo (minutos)",
|
|
302
|
+
critical_bypass: "Lo crítico puede saltarse el presupuesto",
|
|
303
|
+
critical_bypass_hint: "Los saltos quedan siempre registrados y marcados.",
|
|
304
|
+
log_title: "Lo que mandó",
|
|
305
|
+
log_subtitle: "Sólo mensajes no solicitados, del más nuevo al más viejo. Lo que marques alimenta lo que manda después.",
|
|
306
|
+
log_empty: "Todavía no mandó nada sin que se lo pidieras.",
|
|
307
|
+
useful: "Me sirvió",
|
|
308
|
+
noise: "No me servía",
|
|
309
|
+
bypass: "se salteó el presupuesto",
|
|
310
|
+
saved: "Presupuesto actualizado",
|
|
311
|
+
unrated: "sin calificar",
|
|
312
|
+
},
|
|
313
|
+
|
|
249
314
|
identity: {
|
|
250
315
|
title: "Identidad",
|
|
251
316
|
subtitle: "Datos del usuario. Configuración del agente va en Super-agente.",
|
|
@@ -339,6 +404,7 @@ export const es = {
|
|
|
339
404
|
agents: "Agents",
|
|
340
405
|
routines: "Rutinas",
|
|
341
406
|
tasks: "Tasks",
|
|
407
|
+
commitments: "Compromisos",
|
|
342
408
|
mcps: "MCPs",
|
|
343
409
|
artifacts: "Artifacts",
|
|
344
410
|
vars: "Variables",
|
|
@@ -443,6 +509,16 @@ export const es = {
|
|
|
443
509
|
},
|
|
444
510
|
|
|
445
511
|
global_tasks: {
|
|
512
|
+
|
|
513
|
+
add: "Agregar",
|
|
514
|
+
|
|
515
|
+
add_title: "Nueva tarea",
|
|
516
|
+
|
|
517
|
+
field_title: "Tarea",
|
|
518
|
+
|
|
519
|
+
field_project: "Proyecto",
|
|
520
|
+
|
|
521
|
+
field_due: "Vence",
|
|
446
522
|
any_status: "cualquier estado",
|
|
447
523
|
title: "Tasks (todos los proyectos)",
|
|
448
524
|
subtitle: "Tareas agregadas de todos los proyectos registrados.",
|
|
@@ -451,6 +527,44 @@ export const es = {
|
|
|
451
527
|
go_project: "Ir al proyecto",
|
|
452
528
|
},
|
|
453
529
|
|
|
530
|
+
commitments: {
|
|
531
|
+
|
|
532
|
+
add: "Agregar",
|
|
533
|
+
|
|
534
|
+
add_title: "Registrar una promesa",
|
|
535
|
+
|
|
536
|
+
add_hint: "Algo que le dijiste a una persona concreta que ibas a hacer. Las tareas están al lado.",
|
|
537
|
+
|
|
538
|
+
field_who: "Quién está esperando",
|
|
539
|
+
|
|
540
|
+
field_who_hint: "El nombre como lo dirías vos — texto libre, no un contacto.",
|
|
541
|
+
|
|
542
|
+
field_what: "Qué prometiste",
|
|
543
|
+
|
|
544
|
+
field_what_ph: "mandar el presupuesto corregido",
|
|
545
|
+
|
|
546
|
+
field_due: "Para cuándo",
|
|
547
|
+
|
|
548
|
+
field_due_hint: "Opcional, pero es la parte que lo hace útil.",
|
|
549
|
+
|
|
550
|
+
field_project: "Proyecto",
|
|
551
|
+
title: "Compromisos",
|
|
552
|
+
subtitle: "Qué prometiste, a quién y para cuándo. Aparte de las tareas: acá hay alguien esperando.",
|
|
553
|
+
empty: "Nada prometido.",
|
|
554
|
+
overdue: "vencía",
|
|
555
|
+
overdue_only: "Pasados de fecha",
|
|
556
|
+
no_date: "sin fecha acordada",
|
|
557
|
+
moved: "movido",
|
|
558
|
+
mark_kept: "Cumplido",
|
|
559
|
+
mark_missed: "Incumplido",
|
|
560
|
+
state: {
|
|
561
|
+
open: "abiertos",
|
|
562
|
+
kept: "cumplidos",
|
|
563
|
+
missed: "incumplidos",
|
|
564
|
+
all: "todos",
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
|
|
454
568
|
routines: {
|
|
455
569
|
title: "Heartbeats / Routines",
|
|
456
570
|
subtitle: "Cron, every:Nm, once:ISO. Cada rutina dispara un agente o un shell.",
|
|
@@ -782,6 +896,12 @@ export const es = {
|
|
|
782
896
|
},
|
|
783
897
|
|
|
784
898
|
memories: {
|
|
899
|
+
|
|
900
|
+
super_agent_group: "Super-agente",
|
|
901
|
+
|
|
902
|
+
notebook_item: "Libreta de {persona}",
|
|
903
|
+
|
|
904
|
+
tokens: "~{n} tok",
|
|
785
905
|
sidebar_title: "Memorias",
|
|
786
906
|
general_group: "General",
|
|
787
907
|
general_item: "Memoria del proyecto",
|
|
@@ -873,6 +993,8 @@ export const es = {
|
|
|
873
993
|
no_activity_ch: "Sin actividad en el canal \"{ch}\".",
|
|
874
994
|
error: "No pude leer los mensajes: {msg}",
|
|
875
995
|
show_more: "ver más",
|
|
996
|
+
event_routine_created: "rutina creada",
|
|
997
|
+
event_routine_updated: "rutina actualizada",
|
|
876
998
|
show_less: "ver menos",
|
|
877
999
|
daemon_errors: "Errores del daemon (~/.apx/logs/errors.jsonl)",
|
|
878
1000
|
no_errors: "Sin errores registrados. 🎉",
|
|
@@ -1564,6 +1686,9 @@ export const es = {
|
|
|
1564
1686
|
kind_routine: "rutina",
|
|
1565
1687
|
kind_hierarchy: "jerarquía",
|
|
1566
1688
|
nodes_drag_hint: "{n} nodos · arrastrá para reordenar",
|
|
1689
|
+
kind_watch: "Vigía",
|
|
1690
|
+
kind_watch_desc: "Barre buscando cosas que valga la pena notar — vencidos, promesas por caer, proyectos en silencio. No cuesta nada cuando no hay nada que reportar.",
|
|
1691
|
+
action_watch: "Vigila señales, y sólo juzga cuando encuentra alguna",
|
|
1567
1692
|
kind_exec_agent: "Agente del proyecto",
|
|
1568
1693
|
kind_exec_agent_desc: "Corre un agente del proyecto con un prompt. Vos elegís cuál.",
|
|
1569
1694
|
kind_super_agent: "Super-agente",
|
|
@@ -1583,6 +1708,7 @@ export const es = {
|
|
|
1583
1708
|
preset_every_10m: "cada 10 min",
|
|
1584
1709
|
preset_hourly: "cada hora",
|
|
1585
1710
|
preset_daily_9am: "diario 9am",
|
|
1711
|
+
sched_manual: "solo cuando lo corrés vos",
|
|
1586
1712
|
preset_weekdays_9am: "días hábiles 9am",
|
|
1587
1713
|
preset_manual: "Manual",
|
|
1588
1714
|
var_pre_output_prompt: "Salida de texto de los pre-commands. Se reemplaza dentro del prompt/texto antes de enviarlo. Útil para inyectar datos frescos (clima, una API) en la instrucción.",
|
|
@@ -1873,6 +1999,7 @@ export const es = {
|
|
|
1873
1999
|
shared_ui: {
|
|
1874
2000
|
skill_inspector_title: "Skill Inspector ({embedder}) eligió estas skills para este turno",
|
|
1875
2001
|
tools_count: "{n} tools",
|
|
2002
|
+
tools_failed: "{n} fallaron",
|
|
1876
2003
|
tool_read_file: "Leer archivo",
|
|
1877
2004
|
tool_write_file: "Escribir archivo",
|
|
1878
2005
|
tool_edit_file: "Editar archivo",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { http, unwrapPage } from "../http";
|
|
2
|
+
|
|
3
|
+
export type CommitmentState = "open" | "kept" | "missed";
|
|
4
|
+
|
|
5
|
+
export type CommitmentEntry = {
|
|
6
|
+
id: string;
|
|
7
|
+
created_at: string;
|
|
8
|
+
updated_at: string;
|
|
9
|
+
state: CommitmentState;
|
|
10
|
+
counterparty: string;
|
|
11
|
+
body: string;
|
|
12
|
+
promised_at: string;
|
|
13
|
+
due: string | null;
|
|
14
|
+
origin_channel: string | null;
|
|
15
|
+
origin_message_ref: string | null;
|
|
16
|
+
note?: string | null;
|
|
17
|
+
renegotiated_count?: number;
|
|
18
|
+
history: { due: string | null; moved_at: string; note: string | null }[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type GlobalCommitmentEntry = CommitmentEntry & {
|
|
22
|
+
project_id: string | number;
|
|
23
|
+
project_name: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ListOpts = {
|
|
27
|
+
state?: CommitmentState | "all";
|
|
28
|
+
counterparty?: string;
|
|
29
|
+
overdue?: boolean;
|
|
30
|
+
limit: number;
|
|
31
|
+
offset: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function qs(o: ListOpts): string {
|
|
35
|
+
const p = new URLSearchParams();
|
|
36
|
+
if (o.state) p.set("state", o.state);
|
|
37
|
+
if (o.counterparty) p.set("counterparty", o.counterparty);
|
|
38
|
+
if (o.overdue) p.set("overdue", "1");
|
|
39
|
+
p.set("limit", String(o.limit));
|
|
40
|
+
p.set("offset", String(o.offset));
|
|
41
|
+
return p.toString();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const Commitments = {
|
|
45
|
+
globalPage: (o: ListOpts) =>
|
|
46
|
+
http.get<unknown>(`/api/commitments?${qs(o)}`).then((b) => unwrapPage<GlobalCommitmentEntry>(b)),
|
|
47
|
+
listPage: (pid: string, o: ListOpts) =>
|
|
48
|
+
http.get<unknown>(`/api/projects/${pid}/commitments?${qs(o)}`).then((b) => unwrapPage<CommitmentEntry>(b)),
|
|
49
|
+
add: (pid: string, body: { counterparty: string; body: string; due?: string | null }) =>
|
|
50
|
+
http.post<CommitmentEntry>(`/api/projects/${pid}/commitments`, body),
|
|
51
|
+
kept: (pid: string, id: string, note?: string) =>
|
|
52
|
+
http.post<CommitmentEntry>(`/api/projects/${pid}/commitments/${id}/kept`, { note }),
|
|
53
|
+
missed: (pid: string, id: string, note?: string) =>
|
|
54
|
+
http.post<CommitmentEntry>(`/api/projects/${pid}/commitments/${id}/missed`, { note }),
|
|
55
|
+
renegotiate: (pid: string, id: string, due: string, note?: string) =>
|
|
56
|
+
http.post<CommitmentEntry>(`/api/projects/${pid}/commitments/${id}/renegotiate`, { due, note }),
|
|
57
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { http } from "../http";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The super-agent's own notebook (~/.apx/memory.md).
|
|
5
|
+
*
|
|
6
|
+
* Distinct from project memory (.apc/memory.md) and from each agent's memory.
|
|
7
|
+
* This is the one that ships in EVERY prompt on every channel, which is why it
|
|
8
|
+
* reports its own token cost.
|
|
9
|
+
*/
|
|
10
|
+
export interface NotebookInfo {
|
|
11
|
+
body: string;
|
|
12
|
+
path: string;
|
|
13
|
+
chars: number;
|
|
14
|
+
approx_tokens: number;
|
|
15
|
+
entries: number;
|
|
16
|
+
/** How many entries were written by memory consolidation rather than by hand. */
|
|
17
|
+
consolidated: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Notebook = {
|
|
21
|
+
get: () => http.get<NotebookInfo>("/api/notebook"),
|
|
22
|
+
put: (body: string) => http.put<{ ok: true } & Omit<NotebookInfo, "body">>("/api/notebook", { body }),
|
|
23
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { http } from "../http";
|
|
2
|
+
|
|
3
|
+
export type NudgePolicy = {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
daily_max: number;
|
|
6
|
+
quiet_hours: string;
|
|
7
|
+
cooldown_minutes: number;
|
|
8
|
+
project_cooldown_minutes: number;
|
|
9
|
+
kind_cooldown_minutes: number;
|
|
10
|
+
critical_bypasses_budget: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type NudgeEntry = {
|
|
14
|
+
id: string;
|
|
15
|
+
at: string;
|
|
16
|
+
kind: string;
|
|
17
|
+
project_id: string | null;
|
|
18
|
+
severity: string;
|
|
19
|
+
channel: string;
|
|
20
|
+
chat_id: string | null;
|
|
21
|
+
preview: string;
|
|
22
|
+
bypassed_budget: boolean;
|
|
23
|
+
feedback: { useful: boolean; note: string; at: string } | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type NudgeStats = {
|
|
27
|
+
total: number;
|
|
28
|
+
today: number;
|
|
29
|
+
rated: number;
|
|
30
|
+
by_kind: { kind: string; sent: number; useful: number; noise: number }[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const NudgesApi = {
|
|
34
|
+
list: (limit = 50) =>
|
|
35
|
+
http.get<{ data: NudgeEntry[]; meta: { total: number; stats: NudgeStats } }>(
|
|
36
|
+
`/api/nudges?limit=${limit}`,
|
|
37
|
+
),
|
|
38
|
+
policy: () =>
|
|
39
|
+
http.get<{
|
|
40
|
+
policy: NudgePolicy;
|
|
41
|
+
/** Which layers contributed: defaults → profile → user. */
|
|
42
|
+
source: string[];
|
|
43
|
+
defaults: NudgePolicy;
|
|
44
|
+
user_overrides: Partial<NudgePolicy>;
|
|
45
|
+
}>("/api/nudges/policy"),
|
|
46
|
+
setPolicy: (values: Partial<NudgePolicy>) =>
|
|
47
|
+
http.put<{ ok: true; policy: NudgePolicy; source: string[] }>("/api/nudges/policy", values),
|
|
48
|
+
feedback: (id: string, useful: boolean, note = "") =>
|
|
49
|
+
http.post<{ ok: true; entry: NudgeEntry }>(
|
|
50
|
+
`/api/nudges/${encodeURIComponent(id)}/feedback`,
|
|
51
|
+
{ useful, note },
|
|
52
|
+
),
|
|
53
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Five-field cron ⇄ human, both directions.
|
|
3
|
+
*
|
|
4
|
+
* `30 8 * * 1-5` is not a thing anyone should have to read to answer "when
|
|
5
|
+
* does my morning message arrive". Two pure functions, one file, so the
|
|
6
|
+
* summary shown in a status line and the picker used in a form can never
|
|
7
|
+
* disagree about what an expression means.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately NOT a general cron library. It covers what APX writes and what
|
|
10
|
+
* the picker produces — fixed time, day-of-week sets, day-of-month, and
|
|
11
|
+
* step values — and says plainly when it does not understand something rather
|
|
12
|
+
* than guessing. `describeCron` returning null is the signal for a surface to
|
|
13
|
+
* fall back to showing the raw expression.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface CronParts {
|
|
17
|
+
minute: string;
|
|
18
|
+
hour: string;
|
|
19
|
+
dom: string;
|
|
20
|
+
month: string;
|
|
21
|
+
dow: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** What a picker edits. */
|
|
25
|
+
export interface CronSelection {
|
|
26
|
+
/** "HH:MM" in the machine's local time. */
|
|
27
|
+
time: string;
|
|
28
|
+
/** 0=Sunday … 6=Saturday. Empty = every day. */
|
|
29
|
+
days: number[];
|
|
30
|
+
/** Run every N hours instead of at a fixed time. 0 = off. */
|
|
31
|
+
everyHours: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const DAY_KEYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"] as const;
|
|
35
|
+
|
|
36
|
+
export function parseCron(expr: string): CronParts | null {
|
|
37
|
+
const f = String(expr || "").trim().split(/\s+/);
|
|
38
|
+
if (f.length !== 5) return null;
|
|
39
|
+
return { minute: f[0], hour: f[1], dom: f[2], month: f[3], dow: f[4] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Expand `1-5`, `1,3,5`, `*` and step values like `*/2` into concrete numbers.
|
|
43
|
+
// (Kept as a line comment: a `*/` inside a block comment closes it.)
|
|
44
|
+
function expand(field: string, min: number, max: number): number[] | null {
|
|
45
|
+
const out = new Set<number>();
|
|
46
|
+
for (const part of String(field).split(",")) {
|
|
47
|
+
const step = part.match(/^(.+)\/(\d+)$/);
|
|
48
|
+
const body = step ? step[1] : part;
|
|
49
|
+
const by = step ? Number(step[2]) : 1;
|
|
50
|
+
if (!Number.isFinite(by) || by < 1) return null;
|
|
51
|
+
|
|
52
|
+
let lo: number;
|
|
53
|
+
let hi: number;
|
|
54
|
+
if (body === "*") { lo = min; hi = max; }
|
|
55
|
+
else {
|
|
56
|
+
const range = body.match(/^(\d+)-(\d+)$/);
|
|
57
|
+
if (range) { lo = Number(range[1]); hi = Number(range[2]); }
|
|
58
|
+
else if (/^\d+$/.test(body)) { lo = Number(body); hi = lo; }
|
|
59
|
+
else return null;
|
|
60
|
+
}
|
|
61
|
+
if (lo < min || hi > max || lo > hi) return null;
|
|
62
|
+
for (let v = lo; v <= hi; v += by) out.add(v);
|
|
63
|
+
}
|
|
64
|
+
return [...out].sort((a, b) => a - b);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Day numbers a cron dow field selects. Cron accepts 7 for Sunday. */
|
|
68
|
+
export function cronDays(dow: string): number[] | null {
|
|
69
|
+
if (dow === "*" || dow === "?") return [];
|
|
70
|
+
const nums = expand(dow.replace(/\b7\b/g, "0"), 0, 6);
|
|
71
|
+
return nums && nums.length ? nums : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A sentence. Returns null when the expression is beyond what this understands
|
|
76
|
+
* — the caller shows the raw cron rather than a confident wrong reading.
|
|
77
|
+
*/
|
|
78
|
+
export function describeCron(
|
|
79
|
+
expr: string,
|
|
80
|
+
t: (key: string, vars?: Record<string, string | number>) => string,
|
|
81
|
+
): string | null {
|
|
82
|
+
const p = parseCron(expr);
|
|
83
|
+
if (!p) return null;
|
|
84
|
+
if (p.month !== "*") return null; // "in March" is not a case APX writes
|
|
85
|
+
if (p.dom !== "*" && p.dow !== "*") return null; // both set is ambiguous in cron itself
|
|
86
|
+
|
|
87
|
+
// Every N hours: `0 */2 * * *`.
|
|
88
|
+
const stepHour = p.hour.match(/^\*\/(\d+)$/);
|
|
89
|
+
if (stepHour && /^\d+$/.test(p.minute)) {
|
|
90
|
+
const n = Number(stepHour[1]);
|
|
91
|
+
return n === 1
|
|
92
|
+
? t("cron.every_hour", { minute: pad(Number(p.minute)) })
|
|
93
|
+
: t("cron.every_n_hours", { n, minute: pad(Number(p.minute)) });
|
|
94
|
+
}
|
|
95
|
+
// Every N minutes: `*/15 * * * *`.
|
|
96
|
+
const stepMin = p.minute.match(/^\*\/(\d+)$/);
|
|
97
|
+
if (stepMin && p.hour === "*") return t("cron.every_n_minutes", { n: Number(stepMin[1]) });
|
|
98
|
+
if (p.minute === "*" && p.hour === "*") return t("cron.every_minute");
|
|
99
|
+
|
|
100
|
+
const hours = expand(p.hour, 0, 23);
|
|
101
|
+
const minutes = expand(p.minute, 0, 59);
|
|
102
|
+
if (!hours || !minutes || minutes.length !== 1) return null;
|
|
103
|
+
|
|
104
|
+
const at = hours.length === 1
|
|
105
|
+
? `${pad(hours[0])}:${pad(minutes[0])}`
|
|
106
|
+
: hours.map((h) => `${pad(h)}:${pad(minutes[0])}`).join(", ");
|
|
107
|
+
|
|
108
|
+
if (p.dom !== "*") {
|
|
109
|
+
const doms = expand(p.dom, 1, 31);
|
|
110
|
+
if (!doms || doms.length !== 1) return null;
|
|
111
|
+
return t("cron.monthly", { day: doms[0], at });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const days = cronDays(p.dow);
|
|
115
|
+
if (days === null) return null;
|
|
116
|
+
if (!days.length) return t("cron.daily", { at });
|
|
117
|
+
|
|
118
|
+
const weekdays = [1, 2, 3, 4, 5];
|
|
119
|
+
const isWeekdays = days.length === 5 && weekdays.every((d) => days.includes(d));
|
|
120
|
+
if (isWeekdays) return t("cron.weekdays", { at });
|
|
121
|
+
if (days.length === 2 && days.includes(0) && days.includes(6)) return t("cron.weekends", { at });
|
|
122
|
+
|
|
123
|
+
const names = days.map((d) => t(`cron.day_short.${DAY_KEYS[d]}`)).join(", ");
|
|
124
|
+
return t("cron.on_days", { days: names, at });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Cron → what a picker should show. */
|
|
128
|
+
export function cronToSelection(expr: string): CronSelection {
|
|
129
|
+
const p = parseCron(expr);
|
|
130
|
+
const fallback: CronSelection = { time: "08:00", days: [], everyHours: 0 };
|
|
131
|
+
if (!p) return fallback;
|
|
132
|
+
|
|
133
|
+
const stepHour = p.hour.match(/^\*\/(\d+)$/);
|
|
134
|
+
if (stepHour) {
|
|
135
|
+
return {
|
|
136
|
+
time: `00:${pad(Number(/^\d+$/.test(p.minute) ? p.minute : 0))}`,
|
|
137
|
+
days: [],
|
|
138
|
+
everyHours: Number(stepHour[1]),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const hours = expand(p.hour, 0, 23);
|
|
142
|
+
const minutes = expand(p.minute, 0, 59);
|
|
143
|
+
const days = cronDays(p.dow);
|
|
144
|
+
return {
|
|
145
|
+
time: hours?.length && minutes?.length ? `${pad(hours[0])}:${pad(minutes[0])}` : fallback.time,
|
|
146
|
+
days: days ?? [],
|
|
147
|
+
everyHours: 0,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** What a picker produces. Always a valid five-field expression. */
|
|
152
|
+
export function selectionToCron(sel: CronSelection): string {
|
|
153
|
+
if (sel.everyHours > 0) {
|
|
154
|
+
const [, mm] = splitTime(sel.time);
|
|
155
|
+
return `${mm} */${clamp(sel.everyHours, 1, 23)} * * *`;
|
|
156
|
+
}
|
|
157
|
+
const [hh, mm] = splitTime(sel.time);
|
|
158
|
+
// Sorted, and 0 kept as 0 — cron accepts both 0 and 7 for Sunday, and
|
|
159
|
+
// writing one form consistently keeps round-tripping stable.
|
|
160
|
+
const dow = sel.days.length && sel.days.length < 7
|
|
161
|
+
? [...sel.days].sort((a, b) => a - b).join(",")
|
|
162
|
+
: "*";
|
|
163
|
+
return `${mm} ${hh} * * ${dow}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Is this something the pickers can represent without losing information? */
|
|
167
|
+
export function isPickerFriendly(expr: string): boolean {
|
|
168
|
+
const p = parseCron(expr);
|
|
169
|
+
if (!p) return false;
|
|
170
|
+
if (p.month !== "*" || p.dom !== "*") return false;
|
|
171
|
+
return selectionToCron(cronToSelection(expr)) === normalizeCron(expr);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** `30 08 * * 1-5` → `30 8 * * 1,2,3,4,5`, so comparisons are meaningful. */
|
|
175
|
+
export function normalizeCron(expr: string): string {
|
|
176
|
+
const p = parseCron(expr);
|
|
177
|
+
if (!p) return String(expr || "").trim();
|
|
178
|
+
const days = cronDays(p.dow);
|
|
179
|
+
const dow = days && days.length && days.length < 7 ? days.join(",") : "*";
|
|
180
|
+
const one = (f: string) => (/^\d+$/.test(f) ? String(Number(f)) : f);
|
|
181
|
+
return `${one(p.minute)} ${one(p.hour)} ${p.dom} ${p.month} ${dow}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function splitTime(time: string): [string, string] {
|
|
185
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(String(time || "").trim());
|
|
186
|
+
if (!m) return ["8", "0"];
|
|
187
|
+
return [String(clamp(Number(m[1]), 0, 23)), String(clamp(Number(m[2]), 0, 59))];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function pad(n: number): string {
|
|
191
|
+
return String(n).padStart(2, "0");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function clamp(n: number, lo: number, hi: number): number {
|
|
195
|
+
return Math.min(hi, Math.max(lo, Number.isFinite(n) ? n : lo));
|
|
196
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "in 8 hours" / "2 minutes ago".
|
|
3
|
+
*
|
|
4
|
+
* A status row showing two absolute timestamps makes the reader do the
|
|
5
|
+
* arithmetic that the row exists to save them. The absolute value stays
|
|
6
|
+
* available as a tooltip for when the exact minute is what matters.
|
|
7
|
+
*/
|
|
8
|
+
type T = (key: string, vars?: Record<string, string | number>) => string;
|
|
9
|
+
|
|
10
|
+
const MINUTE = 60_000;
|
|
11
|
+
const HOUR = 60 * MINUTE;
|
|
12
|
+
const DAY = 24 * HOUR;
|
|
13
|
+
|
|
14
|
+
export function relativeWhen(iso: string, t: T, now: number = Date.now()): string {
|
|
15
|
+
const ms = Date.parse(iso);
|
|
16
|
+
if (!Number.isFinite(ms)) return "—";
|
|
17
|
+
const diff = ms - now;
|
|
18
|
+
const ahead = diff >= 0;
|
|
19
|
+
const abs = Math.abs(diff);
|
|
20
|
+
|
|
21
|
+
// Under a minute reads as "now" in both directions: "in 34 seconds" is
|
|
22
|
+
// precision nobody acts on.
|
|
23
|
+
if (abs < MINUTE) return t("when.now");
|
|
24
|
+
|
|
25
|
+
const [n, unit] =
|
|
26
|
+
abs < HOUR ? [Math.round(abs / MINUTE), "minutes"] :
|
|
27
|
+
abs < DAY ? [Math.round(abs / HOUR), "hours"] :
|
|
28
|
+
[Math.round(abs / DAY), "days"];
|
|
29
|
+
|
|
30
|
+
const amount = t(`when.${unit}`, { n });
|
|
31
|
+
return ahead ? t("when.in", { amount }) : t("when.ago", { amount });
|
|
32
|
+
}
|