@aitne-sh/aitne 0.1.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/LICENSE +21 -0
- package/README.md +464 -0
- package/agent-assets/agent-profiles/_safety.md +26 -0
- package/agent-assets/agent-profiles/conversational.md +33 -0
- package/agent-assets/agent-profiles/docs-qa.md +24 -0
- package/agent-assets/agent-profiles/observer.md +28 -0
- package/agent-assets/agent-profiles/profile-importer.md +63 -0
- package/agent-assets/agent-profiles/proxy.md +28 -0
- package/agent-assets/agent-profiles/routine.md +16 -0
- package/agent-assets/agent-profiles/task.md +18 -0
- package/agent-assets/docs/concepts/agent-day.md +88 -0
- package/agent-assets/docs/concepts/auth-health.md +75 -0
- package/agent-assets/docs/concepts/backends-and-tiers.md +126 -0
- package/agent-assets/docs/concepts/costs-and-quotas.md +103 -0
- package/agent-assets/docs/concepts/delegated-mode.md +223 -0
- package/agent-assets/docs/concepts/memory-model.md +118 -0
- package/agent-assets/docs/concepts/observations.md +80 -0
- package/agent-assets/docs/concepts/process-keys.md +89 -0
- package/agent-assets/docs/concepts/routines.md +108 -0
- package/agent-assets/docs/concepts/safety-and-execution.md +109 -0
- package/agent-assets/docs/concepts/safety-model.md +279 -0
- package/agent-assets/docs/concepts/skills.md +100 -0
- package/agent-assets/docs/features/integrations/calendar.md +92 -0
- package/agent-assets/docs/features/integrations/git.md +95 -0
- package/agent-assets/docs/features/integrations/github.md +170 -0
- package/agent-assets/docs/features/integrations/mail.md +106 -0
- package/agent-assets/docs/features/integrations/notion.md +69 -0
- package/agent-assets/docs/features/integrations/obsidian.md +71 -0
- package/agent-assets/docs/features/lifestyle/git.md +178 -0
- package/agent-assets/docs/features/lifestyle/reading.md +93 -0
- package/agent-assets/docs/features/lifestyle/receipts.md +71 -0
- package/agent-assets/docs/features/lifestyle/travel-bookings.md +44 -0
- package/agent-assets/docs/features/lifestyle/travel-time.md +52 -0
- package/agent-assets/docs/features/memory-files/agent-journal.md +105 -0
- package/agent-assets/docs/features/memory-files/projects.md +56 -0
- package/agent-assets/docs/features/memory-files/roadmap.md +61 -0
- package/agent-assets/docs/features/memory-files/schedule.md +112 -0
- package/agent-assets/docs/features/memory-files/today.md +73 -0
- package/agent-assets/docs/features/memory-files/user-profile.md +81 -0
- package/agent-assets/docs/features/messaging/dashboard-chat.md +93 -0
- package/agent-assets/docs/features/messaging/discord.md +50 -0
- package/agent-assets/docs/features/messaging/overview.md +111 -0
- package/agent-assets/docs/features/messaging/pairing-and-magic-phrase.md +69 -0
- package/agent-assets/docs/features/messaging/slack.md +51 -0
- package/agent-assets/docs/features/messaging/telegram.md +63 -0
- package/agent-assets/docs/features/messaging/whatsapp.md +48 -0
- package/agent-assets/docs/features/operations/activity-and-conversations.md +105 -0
- package/agent-assets/docs/features/operations/approvals.md +58 -0
- package/agent-assets/docs/features/operations/backend-routing.md +62 -0
- package/agent-assets/docs/features/operations/cost-tracking.md +59 -0
- package/agent-assets/docs/features/operations/notifications.md +69 -0
- package/agent-assets/docs/features/operations/quiet-hours.md +106 -0
- package/agent-assets/docs/features/operations/schedule-approaching.md +60 -0
- package/agent-assets/docs/features/routines/custom-routines.md +101 -0
- package/agent-assets/docs/features/routines/evening-review.md +81 -0
- package/agent-assets/docs/features/routines/hourly-check.md +85 -0
- package/agent-assets/docs/features/routines/monthly-review.md +65 -0
- package/agent-assets/docs/features/routines/morning-routine.md +123 -0
- package/agent-assets/docs/features/routines/weekly-review.md +70 -0
- package/agent-assets/docs/getting-started/01-what-is-this.md +192 -0
- package/agent-assets/docs/getting-started/02-first-steps.md +80 -0
- package/agent-assets/docs/getting-started/03-what-can-this-do.md +110 -0
- package/agent-assets/docs/getting-started/04-first-day.md +287 -0
- package/agent-assets/docs/glossary.md +116 -0
- package/agent-assets/docs/guides/add-a-custom-routine.md +71 -0
- package/agent-assets/docs/guides/backup-and-restore.md +54 -0
- package/agent-assets/docs/guides/change-which-model-handles-x.md +47 -0
- package/agent-assets/docs/guides/connect-a-new-mail-account.md +59 -0
- package/agent-assets/docs/guides/import-knowledge-file.md +275 -0
- package/agent-assets/docs/guides/install-and-run.md +72 -0
- package/agent-assets/docs/guides/migrate-machines.md +52 -0
- package/agent-assets/docs/guides/pause-the-agent.md +65 -0
- package/agent-assets/docs/guides/reinstall-cleanly.md +52 -0
- package/agent-assets/docs/guides/setup-wizard.md +107 -0
- package/agent-assets/docs/guides/switch-default-backend.md +60 -0
- package/agent-assets/docs/reference/api.md +51 -0
- package/agent-assets/docs/reference/cli-commands.md +121 -0
- package/agent-assets/docs/reference/config.md +74 -0
- package/agent-assets/docs/reference/disallowed-tools.md +76 -0
- package/agent-assets/docs/reference/keyboard-shortcuts.md +39 -0
- package/agent-assets/docs/reference/process-keys.md +59 -0
- package/agent-assets/docs/reference/skills.md +50 -0
- package/agent-assets/docs/troubleshooting/auth-failed.md +57 -0
- package/agent-assets/docs/troubleshooting/dashboard-shows-degraded.md +55 -0
- package/agent-assets/docs/troubleshooting/fallback-keeps-firing.md +54 -0
- package/agent-assets/docs/troubleshooting/messaging-not-pairing.md +53 -0
- package/agent-assets/docs/troubleshooting/morning-routine-didnt-run.md +75 -0
- package/agent-assets/docs/troubleshooting/observation-not-detected.md +57 -0
- package/agent-assets/docs/troubleshooting/quota-exhausted.md +57 -0
- package/agent-assets/optimizer-skills/drift-analysis/SKILL.md +75 -0
- package/agent-assets/optimizer-skills/knowledge-map/SKILL.md +71 -0
- package/agent-assets/optimizer-skills/skill-curation/SKILL.md +108 -0
- package/agent-assets/project-doc-templates/git-repo.md +21 -0
- package/agent-assets/project-doc-templates/project.md +38 -0
- package/agent-assets/skills/attach/SKILL.md +104 -0
- package/agent-assets/skills/context/SKILL.md +257 -0
- package/agent-assets/skills/context/curation.json +37 -0
- package/agent-assets/skills/context/seeds/file-responsibilities.seed.json +13 -0
- package/agent-assets/skills/context/seeds/frontmatter-requirements.seed.json +40 -0
- package/agent-assets/skills/docs-search/SKILL.md +176 -0
- package/agent-assets/skills/external-services/SKILL.delegated.claude.md +369 -0
- package/agent-assets/skills/external-services/SKILL.delegated.codex.md +349 -0
- package/agent-assets/skills/external-services/SKILL.delegated.gemini.md +347 -0
- package/agent-assets/skills/external-services/SKILL.md +371 -0
- package/agent-assets/skills/mail/SKILL.delegated.claude.md +284 -0
- package/agent-assets/skills/mail/SKILL.delegated.codex.md +261 -0
- package/agent-assets/skills/mail/SKILL.delegated.gemini.md +255 -0
- package/agent-assets/skills/mail/SKILL.md +313 -0
- package/agent-assets/skills/mail/references/errors.md +17 -0
- package/agent-assets/skills/mail/references/providers.md +40 -0
- package/agent-assets/skills/mail/references/query-grammar.md +24 -0
- package/agent-assets/skills/management-policy/SKILL.md +307 -0
- package/agent-assets/skills/management-policy/curation.json +13 -0
- package/agent-assets/skills/management-policy/seeds/policy-file-shape.seed.json +16 -0
- package/agent-assets/skills/management-task-modify/SKILL.md +202 -0
- package/agent-assets/skills/management-task-register/SKILL.md +330 -0
- package/agent-assets/skills/management-task-stop/SKILL.md +166 -0
- package/agent-assets/skills/notify/SKILL.md +196 -0
- package/agent-assets/skills/notion/SKILL.delegated.claude.md +254 -0
- package/agent-assets/skills/notion/SKILL.delegated.codex.md +195 -0
- package/agent-assets/skills/notion/SKILL.delegated.gemini.md +194 -0
- package/agent-assets/skills/notion/SKILL.md +86 -0
- package/agent-assets/skills/observations/SKILL.md +234 -0
- package/agent-assets/skills/observations/curation.json +13 -0
- package/agent-assets/skills/observations/seeds/source-namespacing.seed.json +20 -0
- package/agent-assets/skills/project-doc/SKILL.md +86 -0
- package/agent-assets/skills/project-doc/curation.json +21 -0
- package/agent-assets/skills/project-doc/seeds/project-shape.seed.json +25 -0
- package/agent-assets/skills/project-doc/seeds/slug-grammar.seed.json +20 -0
- package/agent-assets/skills/reading/SKILL.md +198 -0
- package/agent-assets/skills/reading/references/reading-taste.md +197 -0
- package/agent-assets/skills/receipts/SKILL.md +134 -0
- package/agent-assets/skills/roadmap/SKILL.md +276 -0
- package/agent-assets/skills/roadmap/curation.json +13 -0
- package/agent-assets/skills/roadmap/references/horizon-tags.md +40 -0
- package/agent-assets/skills/roadmap/references/preparation-timeline.md +47 -0
- package/agent-assets/skills/roadmap/seeds/entry-types.seed.json +16 -0
- package/agent-assets/skills/schedule/SKILL.md +228 -0
- package/agent-assets/skills/scheduled-managed-task/SKILL.md +392 -0
- package/agent-assets/skills/today/SKILL.md +198 -0
- package/agent-assets/skills/today/curation.json +21 -0
- package/agent-assets/skills/today/seeds/agent-notes-flavors.seed.json +17 -0
- package/agent-assets/skills/today/seeds/section-shape.seed.json +17 -0
- package/agent-assets/skills/travel/SKILL.md +132 -0
- package/agent-assets/skills/travel-time/SKILL.md +149 -0
- package/agent-assets/skills/user-interview/SKILL.md +323 -0
- package/agent-assets/skills/user-interview/references/sweep-and-fallback.md +94 -0
- package/agent-assets/skills/user-profile/SKILL.md +210 -0
- package/agent-assets/skills/user-profile/curation.json +29 -0
- package/agent-assets/skills/user-profile/seeds/learned-context-format.seed.json +14 -0
- package/agent-assets/skills/user-profile/seeds/routing-table.seed.json +53 -0
- package/agent-assets/skills/user-profile/seeds/topic-files.seed.json +27 -0
- package/agent-assets/task-flows/dashboard.docs_qa.md +43 -0
- package/agent-assets/task-flows/default.md +11 -0
- package/agent-assets/task-flows/git.branch.created.md +25 -0
- package/agent-assets/task-flows/git.lifecycle.poll.md +52 -0
- package/agent-assets/task-flows/git.local_ahead.stale.md +34 -0
- package/agent-assets/task-flows/git.merge_to_default.md +30 -0
- package/agent-assets/task-flows/git.project.refresh_architecture.md +100 -0
- package/agent-assets/task-flows/git.project.retemplate.md +73 -0
- package/agent-assets/task-flows/git.push.detected.md +32 -0
- package/agent-assets/task-flows/git.push.force_pushed.md +36 -0
- package/agent-assets/task-flows/git.tag.created.md +24 -0
- package/agent-assets/task-flows/github.assigned.md +43 -0
- package/agent-assets/task-flows/github.pull_request.review_requested.md +57 -0
- package/agent-assets/task-flows/github.security_alert.md +45 -0
- package/agent-assets/task-flows/github.workflow_run.failed.md +57 -0
- package/agent-assets/task-flows/knowledge.import.md +161 -0
- package/agent-assets/task-flows/message.received.dm.md +142 -0
- package/agent-assets/task-flows/message.received.dm_first.md +117 -0
- package/agent-assets/task-flows/message.received.md +14 -0
- package/agent-assets/task-flows/routine.custom.md +38 -0
- package/agent-assets/task-flows/routine.evening_review.md +323 -0
- package/agent-assets/task-flows/routine.hourly_check.delegated.claude.md +405 -0
- package/agent-assets/task-flows/routine.hourly_check.delegated.codex.md +400 -0
- package/agent-assets/task-flows/routine.hourly_check.delegated.gemini.md +404 -0
- package/agent-assets/task-flows/routine.hourly_check.md +184 -0
- package/agent-assets/task-flows/routine.hourly_check.triage.md +93 -0
- package/agent-assets/task-flows/routine.monthly_review.md +250 -0
- package/agent-assets/task-flows/routine.morning_routine.md +300 -0
- package/agent-assets/task-flows/routine.morning_routine_initial.md +184 -0
- package/agent-assets/task-flows/routine.roadmap_refresh.md +275 -0
- package/agent-assets/task-flows/routine.today_refresh.md +172 -0
- package/agent-assets/task-flows/routine.user_profile_sweep.md +242 -0
- package/agent-assets/task-flows/routine.weekly_review.md +247 -0
- package/agent-assets/task-flows/schedule.approaching.md +124 -0
- package/agent-assets/task-flows/scheduled.dm.md +391 -0
- package/agent-assets/task-flows/scheduled.task.md +141 -0
- package/agent-assets/task-flows/setup.initial.md +277 -0
- package/agent-assets/task-flows/setup.update.md +53 -0
- package/agent-assets/templates/README.md +85 -0
- package/agent-assets/templates/_index.md +39 -0
- package/agent-assets/templates/_manifest.json +103 -0
- package/agent-assets/templates/agent/journal.md +10 -0
- package/agent-assets/templates/agent/profile-questions.md +74 -0
- package/agent-assets/templates/context-index.md +42 -0
- package/agent-assets/templates/dossiers/_index.md +22 -0
- package/agent-assets/templates/dossiers/evening.md +23 -0
- package/agent-assets/templates/dossiers/hourly.md +23 -0
- package/agent-assets/templates/dossiers/monthly.md +23 -0
- package/agent-assets/templates/dossiers/morning.md +23 -0
- package/agent-assets/templates/dossiers/roadmap.md +23 -0
- package/agent-assets/templates/dossiers/weekly.md +23 -0
- package/agent-assets/templates/projects/_active.base +14 -0
- package/agent-assets/templates/projects/_index.md +29 -0
- package/agent-assets/templates/roadmap.md +15 -0
- package/agent-assets/templates/routines/_index.md +20 -0
- package/agent-assets/templates/routines/evening.md +22 -0
- package/agent-assets/templates/routines/hourly.md +30 -0
- package/agent-assets/templates/routines/monthly.md +25 -0
- package/agent-assets/templates/routines/morning.md +26 -0
- package/agent-assets/templates/routines/weekly.md +23 -0
- package/agent-assets/templates/rules/_index.md +19 -0
- package/agent-assets/templates/rules/journal-export.md +41 -0
- package/agent-assets/templates/rules/journal-format.md +61 -0
- package/agent-assets/templates/rules/management.md +48 -0
- package/agent-assets/templates/rules/mcp.md +40 -0
- package/agent-assets/templates/rules/policies/_index.md +22 -0
- package/agent-assets/templates/rules/redaction.md +30 -0
- package/agent-assets/templates/today.md +13 -0
- package/agent-assets/templates/user/_index.md +16 -0
- package/agent-assets/templates/user/expertise.md +7 -0
- package/agent-assets/templates/user/goals.md +7 -0
- package/agent-assets/templates/user/people.md +7 -0
- package/agent-assets/templates/user/personal.md +7 -0
- package/agent-assets/templates/user/profile.md +28 -0
- package/agent-assets/templates/user/work.md +7 -0
- package/bin/aitne.mjs +1096 -0
- package/package.json +78 -0
- package/personal-agent.mjs +39 -0
- package/scripts/browser.mjs +99 -0
- package/scripts/check-redaction-coverage.mjs +109 -0
- package/scripts/commands/audit.mjs +309 -0
- package/scripts/commands/doctor.mjs +437 -0
- package/scripts/commands/open.mjs +40 -0
- package/scripts/commands/setup.mjs +21 -0
- package/scripts/commands/uninstall.mjs +114 -0
- package/scripts/commands/update.mjs +96 -0
- package/scripts/commands/version.mjs +62 -0
- package/scripts/commands.md +0 -0
- package/scripts/lib/sqlite-loader.mjs +49 -0
- package/scripts/message-discipline-digest.mjs +535 -0
- package/scripts/poc/google-connector-inheritance/REPORT.md +197 -0
- package/scripts/poc/google-connector-inheritance/claude-sdk-probe.mjs +79 -0
- package/scripts/remint-roadmap-ids.mjs +257 -0
- package/scripts/rm-paths.mjs +22 -0
- package/scripts/run-node.mjs +223 -0
- package/scripts/smoke-obsidian-api.mjs +166 -0
- package/scripts/start.mjs +160 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// MESSAGING-DISCIPLINE-PLAN.md §5.1 — message-discipline post-ship monitor.
|
|
3
|
+
//
|
|
4
|
+
// Reads notification_log + agent_actions for a given day, classifies every
|
|
5
|
+
// user-facing message against opener_forbidden / readback_suspected /
|
|
6
|
+
// clean, and posts a low-priority digest DM via /api/notify so the owner
|
|
7
|
+
// can audit whether the new universal-discipline rules are actually
|
|
8
|
+
// holding in production.
|
|
9
|
+
//
|
|
10
|
+
// Mechanism rationale (§5.1): the prompt-text exit criteria in Phases
|
|
11
|
+
// 0-2 confirm the universal discipline section is *written* correctly.
|
|
12
|
+
// They do not detect agent behavior regressions — that signal previously
|
|
13
|
+
// only arrived via the owner manually flagging a message. This digest
|
|
14
|
+
// closes the loop by surfacing every user-facing emission for the day
|
|
15
|
+
// with regex classification, so a regression is caught the next morning,
|
|
16
|
+
// not weeks later.
|
|
17
|
+
//
|
|
18
|
+
// USAGE
|
|
19
|
+
// -----
|
|
20
|
+
// # Compose + send today's digest (defaults to today)
|
|
21
|
+
// node scripts/message-discipline-digest.mjs --send
|
|
22
|
+
//
|
|
23
|
+
// # Compose + print without sending
|
|
24
|
+
// node scripts/message-discipline-digest.mjs --date 2026-04-26
|
|
25
|
+
//
|
|
26
|
+
// # Seed 7 nightly agent_schedule rows at 22:00 local for the next 7 days
|
|
27
|
+
// node scripts/message-discipline-digest.mjs --seed
|
|
28
|
+
//
|
|
29
|
+
// # Override defaults
|
|
30
|
+
// node scripts/message-discipline-digest.mjs --send --date 2026-04-25 \
|
|
31
|
+
// --base-url http://localhost:8321 --db-path ~/.personal-agent/data/personal_agent.db
|
|
32
|
+
//
|
|
33
|
+
// FLAGS
|
|
34
|
+
// --send POST the digest via /api/notify (priority `low`).
|
|
35
|
+
// Without this flag the digest is printed to stdout.
|
|
36
|
+
// --date YYYY-MM-DD The day to digest. Defaults to today (local).
|
|
37
|
+
// --base-url URL Daemon base URL. Default http://localhost:8321.
|
|
38
|
+
// --db-path PATH SQLite path. Default $PA_DATA_DIR/data/personal_agent.db
|
|
39
|
+
// or ~/.personal-agent/data/personal_agent.db.
|
|
40
|
+
// --seed Insert 7 nightly agent_schedule rows starting
|
|
41
|
+
// tomorrow at --time (default 22:00 local).
|
|
42
|
+
// --days N (with --seed) how many evenings to seed. Default 7.
|
|
43
|
+
// --time HH:MM (with --seed) local time of day. Default 22:00.
|
|
44
|
+
// --start-date YYYY-MM-DD (with --seed) first evening. Default tomorrow.
|
|
45
|
+
//
|
|
46
|
+
// CLASSIFICATION REGEXES (seed list — extend as the owner flags misses):
|
|
47
|
+
// opener_forbidden — leading ceremony phrases the universal section
|
|
48
|
+
// (notify/SKILL.md § Universal user-facing message
|
|
49
|
+
// discipline § No ceremony) bans. Applies to EVERY
|
|
50
|
+
// user-facing surface — the no-ceremony rule has no
|
|
51
|
+
// conversational carve-out.
|
|
52
|
+
// readback_suspected — `Schedule:` / `Tasks:` / `Deadlines:` /
|
|
53
|
+
// `Notes:` lines as leading labels — likely
|
|
54
|
+
// readback of the user's own data, banned by
|
|
55
|
+
// § No table-of-contents readback. Applied ONLY
|
|
56
|
+
// to PROACTIVE surfaces (POST /api/notify,
|
|
57
|
+
// scheduled.dm, scheduled_dm). DM replies under
|
|
58
|
+
// message.received.* get a conversational carve-
|
|
59
|
+
// out per agent-profiles/conversational.md — when
|
|
60
|
+
// the user explicitly asks for their own data,
|
|
61
|
+
// returning it plainly is legal. Running the
|
|
62
|
+
// readback regex over those rows would produce
|
|
63
|
+
// systematic false positives that erode owner
|
|
64
|
+
// attention to the digest.
|
|
65
|
+
//
|
|
66
|
+
// IMPLEMENTATION NOTES
|
|
67
|
+
// - Reads SQLite via the system `sqlite3` CLI (`-json`) so the script
|
|
68
|
+
// has zero npm runtime deps. macOS ships sqlite3 ≥ 3.43, which
|
|
69
|
+
// supports `-json`. On Linux without sqlite3 CLI installed, the
|
|
70
|
+
// script will exit with a clear error.
|
|
71
|
+
// - The plan permits no daemon code change, so the script reads the
|
|
72
|
+
// DB directly (read-only intent — `--seed` is the one write path,
|
|
73
|
+
// and it INSERTs only `agent_schedule` rows that the daemon
|
|
74
|
+
// scheduler already understands).
|
|
75
|
+
// - Posts via global `fetch` (Node ≥ 18). Project pins Node ≥ 22.
|
|
76
|
+
|
|
77
|
+
import { execFileSync } from "node:child_process";
|
|
78
|
+
import { existsSync } from "node:fs";
|
|
79
|
+
import os from "node:os";
|
|
80
|
+
import path from "node:path";
|
|
81
|
+
import process from "node:process";
|
|
82
|
+
|
|
83
|
+
const FORBIDDEN_OPENERS_RE =
|
|
84
|
+
/^(?:Good\s+(?:morning|evening|afternoon)|Evening\s+check-in|Morning\s+briefing|Heads-up|FYI|Quick\s+update|Summary|Done|Sent|OK|Here's|Here\sis)\b/i;
|
|
85
|
+
const READBACK_RE =
|
|
86
|
+
/^(?:Schedule|Tasks|Deadlines|Notes)\s*:/im;
|
|
87
|
+
|
|
88
|
+
function parseArgs(argv) {
|
|
89
|
+
const args = { send: false, seed: false };
|
|
90
|
+
for (let i = 0; i < argv.length; i++) {
|
|
91
|
+
const a = argv[i];
|
|
92
|
+
if (a === "--send") args.send = true;
|
|
93
|
+
else if (a === "--seed") args.seed = true;
|
|
94
|
+
else if (a === "--date") args.date = argv[++i];
|
|
95
|
+
else if (a === "--base-url") args.baseUrl = argv[++i];
|
|
96
|
+
else if (a === "--db-path") args.dbPath = argv[++i];
|
|
97
|
+
else if (a === "--start-date") args.startDate = argv[++i];
|
|
98
|
+
else if (a === "--days") args.days = Number(argv[++i]);
|
|
99
|
+
else if (a === "--time") args.time = argv[++i];
|
|
100
|
+
else if (a === "--help" || a === "-h") {
|
|
101
|
+
printHelp();
|
|
102
|
+
process.exit(0);
|
|
103
|
+
} else {
|
|
104
|
+
console.error(`Unknown flag: ${a}. Use --help for usage.`);
|
|
105
|
+
process.exit(2);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return args;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function printHelp() {
|
|
112
|
+
// Help text is the file header above; print the relevant portion.
|
|
113
|
+
console.log(
|
|
114
|
+
"Usage:\n" +
|
|
115
|
+
" node scripts/message-discipline-digest.mjs [--send] [--date YYYY-MM-DD]\n" +
|
|
116
|
+
" node scripts/message-discipline-digest.mjs --seed [--start-date YYYY-MM-DD]\n" +
|
|
117
|
+
" [--days N] [--time HH:MM]\n" +
|
|
118
|
+
"Run with --help for the full header in the source file.",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveDbPath(override) {
|
|
123
|
+
if (override) return override;
|
|
124
|
+
const dataDir = process.env.PA_DATA_DIR || path.join(os.homedir(), ".personal-agent");
|
|
125
|
+
return path.join(dataDir, "data", "personal_agent.db");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function todayLocal() {
|
|
129
|
+
const d = new Date();
|
|
130
|
+
const yyyy = d.getFullYear();
|
|
131
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
132
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
133
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function addDaysLocal(yyyymmdd, n) {
|
|
137
|
+
const [y, m, d] = yyyymmdd.split("-").map(Number);
|
|
138
|
+
const dt = new Date(y, m - 1, d);
|
|
139
|
+
dt.setDate(dt.getDate() + n);
|
|
140
|
+
const yyyy = dt.getFullYear();
|
|
141
|
+
const mm = String(dt.getMonth() + 1).padStart(2, "0");
|
|
142
|
+
const dd = String(dt.getDate()).padStart(2, "0");
|
|
143
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function localIsoAt(dateStr, hhmm) {
|
|
147
|
+
// Returns an ISO-8601 string with the local timezone offset for
|
|
148
|
+
// `<dateStr>T<hhmm>:00` interpreted in the user's local zone. The daemon
|
|
149
|
+
// accepts any parseable date string (POST /api/schedule does
|
|
150
|
+
// `new Date(time)`); attaching the offset eliminates ambiguity.
|
|
151
|
+
const [y, m, d] = dateStr.split("-").map(Number);
|
|
152
|
+
const [hh, mm] = hhmm.split(":").map(Number);
|
|
153
|
+
const dt = new Date(y, m - 1, d, hh, mm, 0);
|
|
154
|
+
const tzMin = -dt.getTimezoneOffset();
|
|
155
|
+
const sign = tzMin >= 0 ? "+" : "-";
|
|
156
|
+
const absMin = Math.abs(tzMin);
|
|
157
|
+
const tzh = String(Math.floor(absMin / 60)).padStart(2, "0");
|
|
158
|
+
const tzm = String(absMin % 60).padStart(2, "0");
|
|
159
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
160
|
+
return (
|
|
161
|
+
`${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}` +
|
|
162
|
+
`T${pad(dt.getHours())}:${pad(dt.getMinutes())}:00${sign}${tzh}:${tzm}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function querySqlite(dbPath, sql) {
|
|
167
|
+
if (!existsSync(dbPath)) {
|
|
168
|
+
throw new Error(`SQLite DB not found at ${dbPath}`);
|
|
169
|
+
}
|
|
170
|
+
let out;
|
|
171
|
+
try {
|
|
172
|
+
out = execFileSync("sqlite3", ["-json", dbPath, sql], {
|
|
173
|
+
encoding: "utf-8",
|
|
174
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
175
|
+
});
|
|
176
|
+
} catch (err) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`sqlite3 invocation failed (${err.message}). The script needs the system sqlite3 CLI (macOS ships it).`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const trimmed = out.trim();
|
|
182
|
+
if (!trimmed) return [];
|
|
183
|
+
return JSON.parse(trimmed);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Classify a user-facing message against the universal-discipline regexes.
|
|
188
|
+
*
|
|
189
|
+
* The `surface` argument controls which rules apply. Only "proactive"
|
|
190
|
+
* surfaces (the agent decided to message the user without being asked)
|
|
191
|
+
* are subject to the readback regex; "reactive" surfaces (DM replies)
|
|
192
|
+
* are exempt because conversational.md explicitly carves out
|
|
193
|
+
* user-requested data readback as legal — the universal section's
|
|
194
|
+
* "no readback" rule's load-bearing rationale ("the user already has
|
|
195
|
+
* it") doesn't hold when the user just asked for it. The opener regex
|
|
196
|
+
* always applies; the no-ceremony rule has no conversational carve-out.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} text
|
|
199
|
+
* @param {"proactive"|"reactive"} surface
|
|
200
|
+
*/
|
|
201
|
+
function classify(text, surface = "proactive") {
|
|
202
|
+
if (!text) return "clean";
|
|
203
|
+
const stripped = text.trim();
|
|
204
|
+
// Strip a single leading markdown bullet / heading marker — a digest
|
|
205
|
+
// bullet like "- Good morning" still violates the no-ceremony rule.
|
|
206
|
+
const noBullet = stripped.replace(/^(?:[-*]\s+|#+\s+)/, "");
|
|
207
|
+
if (FORBIDDEN_OPENERS_RE.test(noBullet)) return "opener_forbidden";
|
|
208
|
+
if (surface === "proactive" && READBACK_RE.test(noBullet)) {
|
|
209
|
+
return "readback_suspected";
|
|
210
|
+
}
|
|
211
|
+
return "clean";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function truncate(s, n) {
|
|
215
|
+
if (!s) return "";
|
|
216
|
+
return s.length <= n ? s : `${s.slice(0, n - 1)}…`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function loadDayData(dbPath, date) {
|
|
220
|
+
// notification_log carries every user-facing emission. The
|
|
221
|
+
// notification_type column distinguishes the source surface:
|
|
222
|
+
//
|
|
223
|
+
// notification_type='agent' — POST /api/notify (every call,
|
|
224
|
+
// set in packages/daemon/src/index.ts
|
|
225
|
+
// sendNotification + agent.ts:357
|
|
226
|
+
// /notify route)
|
|
227
|
+
// notification_type='scheduled.dm' — LLM-composed Morning briefing
|
|
228
|
+
// (and any future scheduled.dm
|
|
229
|
+
// sub-flow) — final assistant
|
|
230
|
+
// text written by
|
|
231
|
+
// notification-manager.send,
|
|
232
|
+
// which logs notification_type =
|
|
233
|
+
// event.type literally
|
|
234
|
+
// notification_type='scheduled_dm' — pre-composed direct DM
|
|
235
|
+
// (task_type='dm') from
|
|
236
|
+
// scheduler.ts:handleDirectDm,
|
|
237
|
+
// no LLM involved
|
|
238
|
+
// notification_type='message.received.*' — DM reply to an inbound
|
|
239
|
+
// user DM (final-text,
|
|
240
|
+
// notification-manager auto-
|
|
241
|
+
// send via dispatcher)
|
|
242
|
+
// notification_type='schedule.approaching' / 'calendar.*' / etc. —
|
|
243
|
+
// other event-typed auto-sends, currently unused for user-facing
|
|
244
|
+
// text (the schedule.approaching prompt routes through /api/notify,
|
|
245
|
+
// so its rows land under 'agent', not under the event type).
|
|
246
|
+
//
|
|
247
|
+
// The buckets below mirror the §5.1 surfaces — /api/notify, scheduled
|
|
248
|
+
// DMs (LLM and direct), DM replies — so the regex classifier runs over
|
|
249
|
+
// every user-visible row produced that day.
|
|
250
|
+
const notifications = querySqlite(
|
|
251
|
+
dbPath,
|
|
252
|
+
`SELECT id, notification_type, priority, content_summary, status, platform, created_at
|
|
253
|
+
FROM notification_log
|
|
254
|
+
WHERE date(created_at, 'localtime') = ${sqlString(date)}
|
|
255
|
+
ORDER BY created_at ASC`,
|
|
256
|
+
);
|
|
257
|
+
// schedule.approaching event firings: count agent_actions rows with
|
|
258
|
+
// action_type='schedule.approaching'. We deliberately do NOT split into
|
|
259
|
+
// notified/skipped here — there is no clean join from agent_actions to
|
|
260
|
+
// notification_log (notification_log has no event_id / correlationId
|
|
261
|
+
// column, and /api/notify rewrites notification_type to 'agent', so a
|
|
262
|
+
// filter on 'schedule.approaching' would always be 0). Reporting a fake
|
|
263
|
+
// split produced misleading "100% skipped" digests; an honest total
|
|
264
|
+
// plus the global notify-row classifier covers regression detection.
|
|
265
|
+
const approachingActions = querySqlite(
|
|
266
|
+
dbPath,
|
|
267
|
+
`SELECT id, started_at, completed_at, result
|
|
268
|
+
FROM agent_actions
|
|
269
|
+
WHERE action_type = 'schedule.approaching'
|
|
270
|
+
AND date(started_at, 'localtime') = ${sqlString(date)}
|
|
271
|
+
ORDER BY started_at ASC`,
|
|
272
|
+
);
|
|
273
|
+
return { notifications, approachingActions };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function sqlString(s) {
|
|
277
|
+
// Inline literal string — `s` is always a controlled value (date in
|
|
278
|
+
// YYYY-MM-DD form from local arithmetic). Defense-in-depth: reject
|
|
279
|
+
// anything that doesn't match the date pattern.
|
|
280
|
+
if (!/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(s)) {
|
|
281
|
+
throw new Error(`refusing to inline non-date value into SQL: ${s}`);
|
|
282
|
+
}
|
|
283
|
+
return `'${s}'`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildDigest(date, { notifications, approachingActions }) {
|
|
287
|
+
// Bucket rows by surface so the regex classifier runs over every
|
|
288
|
+
// user-visible emission. See loadDayData header comment for the
|
|
289
|
+
// notification_type taxonomy.
|
|
290
|
+
const notifyApi = notifications.filter(
|
|
291
|
+
(n) => n.notification_type === "agent",
|
|
292
|
+
);
|
|
293
|
+
// Both spellings cover the scheduled-DM surface: 'scheduled.dm' is
|
|
294
|
+
// notification-manager logging event.type literally for the LLM-composed
|
|
295
|
+
// path (Morning briefing); 'scheduled_dm' is scheduler.handleDirectDm
|
|
296
|
+
// for pre-composed direct DMs.
|
|
297
|
+
const scheduledDm = notifications.filter(
|
|
298
|
+
(n) =>
|
|
299
|
+
n.notification_type === "scheduled.dm" ||
|
|
300
|
+
n.notification_type === "scheduled_dm",
|
|
301
|
+
);
|
|
302
|
+
const dmReplies = notifications.filter((n) =>
|
|
303
|
+
typeof n.notification_type === "string" &&
|
|
304
|
+
n.notification_type.startsWith("message.received"),
|
|
305
|
+
);
|
|
306
|
+
const otherNotifications = notifications.filter(
|
|
307
|
+
(n) =>
|
|
308
|
+
n.notification_type !== "agent" &&
|
|
309
|
+
n.notification_type !== "scheduled.dm" &&
|
|
310
|
+
n.notification_type !== "scheduled_dm" &&
|
|
311
|
+
!(
|
|
312
|
+
typeof n.notification_type === "string" &&
|
|
313
|
+
n.notification_type.startsWith("message.received")
|
|
314
|
+
),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const totalApproaching = approachingActions.length;
|
|
318
|
+
|
|
319
|
+
const lines = [];
|
|
320
|
+
lines.push(`message-discipline digest — ${date}`);
|
|
321
|
+
lines.push("");
|
|
322
|
+
|
|
323
|
+
lines.push(`POST /api/notify calls: ${notifyApi.length}`);
|
|
324
|
+
for (const n of notifyApi) {
|
|
325
|
+
lines.push(formatNotifyLine(n));
|
|
326
|
+
}
|
|
327
|
+
if (notifyApi.length === 0) lines.push("- (none)");
|
|
328
|
+
lines.push("");
|
|
329
|
+
|
|
330
|
+
lines.push(`scheduled.dm final-text DMs: ${scheduledDm.length}`);
|
|
331
|
+
for (const n of scheduledDm) {
|
|
332
|
+
lines.push(formatNotifyLine(n));
|
|
333
|
+
}
|
|
334
|
+
if (scheduledDm.length === 0) lines.push("- (none)");
|
|
335
|
+
lines.push("");
|
|
336
|
+
|
|
337
|
+
lines.push(`message.received.* DM replies: ${dmReplies.length}`);
|
|
338
|
+
for (const n of dmReplies) {
|
|
339
|
+
lines.push(formatNotifyLine(n));
|
|
340
|
+
}
|
|
341
|
+
if (dmReplies.length === 0) lines.push("- (none)");
|
|
342
|
+
lines.push("");
|
|
343
|
+
|
|
344
|
+
if (otherNotifications.length > 0) {
|
|
345
|
+
lines.push(`other notification_log rows: ${otherNotifications.length}`);
|
|
346
|
+
for (const n of otherNotifications) {
|
|
347
|
+
lines.push(formatNotifyLine(n));
|
|
348
|
+
}
|
|
349
|
+
lines.push("");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Honest reporting: agent_actions has no clean join to notification_log
|
|
353
|
+
// (no shared event_id / correlationId column), and /api/notify writes
|
|
354
|
+
// notification_type='agent', so we cannot tell which approaching events
|
|
355
|
+
// produced a notification without timestamp-window heuristics. Surface
|
|
356
|
+
// the total here; the regex classification above already covers any
|
|
357
|
+
// notify rows the firing session emitted.
|
|
358
|
+
lines.push(`schedule.approaching events: total=${totalApproaching}`);
|
|
359
|
+
lines.push(
|
|
360
|
+
" (notified vs skipped split not derivable — notification_log has no event_id; trigger attribution a/d also pending instrumentation)",
|
|
361
|
+
);
|
|
362
|
+
lines.push("");
|
|
363
|
+
|
|
364
|
+
// Roll-up of flags across every user-visible surface. This is the line
|
|
365
|
+
// the owner glances at first; if it's all-zero for 7 days the digest is
|
|
366
|
+
// retired (§5.1 Action rules). Each row is classified at its own
|
|
367
|
+
// surface — readback_suspected is suppressed for DM replies because
|
|
368
|
+
// conversational.md carves out user-requested data readback as legal.
|
|
369
|
+
const allMessages = [
|
|
370
|
+
...notifyApi,
|
|
371
|
+
...scheduledDm,
|
|
372
|
+
...dmReplies,
|
|
373
|
+
...otherNotifications,
|
|
374
|
+
];
|
|
375
|
+
const flagCounts = { opener_forbidden: 0, readback_suspected: 0, clean: 0 };
|
|
376
|
+
for (const m of allMessages) {
|
|
377
|
+
flagCounts[classify(m.content_summary, surfaceFor(m))]++;
|
|
378
|
+
}
|
|
379
|
+
lines.push(
|
|
380
|
+
`flags: opener_forbidden=${flagCounts.opener_forbidden} readback_suspected=${flagCounts.readback_suspected} clean=${flagCounts.clean}`,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
return lines.join("\n");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Decide whether a notification_log row counts as a "proactive" surface
|
|
388
|
+
* for classifier purposes. DM replies (message.received.*) are reactive;
|
|
389
|
+
* everything else (agent / scheduled.dm / scheduled_dm / etc.) is the
|
|
390
|
+
* agent reaching out unprompted.
|
|
391
|
+
*/
|
|
392
|
+
function surfaceFor(n) {
|
|
393
|
+
if (
|
|
394
|
+
typeof n.notification_type === "string" &&
|
|
395
|
+
n.notification_type.startsWith("message.received")
|
|
396
|
+
) {
|
|
397
|
+
return "reactive";
|
|
398
|
+
}
|
|
399
|
+
return "proactive";
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function formatNotifyLine(n) {
|
|
403
|
+
const cls = classify(n.content_summary, surfaceFor(n));
|
|
404
|
+
const tag = cls === "clean" ? "✓" : `⚠ ${cls}`;
|
|
405
|
+
const summary = truncate((n.content_summary ?? "").replace(/\s+/g, " "), 120);
|
|
406
|
+
const type = n.notification_type ?? "(none)";
|
|
407
|
+
const priority = n.priority ?? "(none)";
|
|
408
|
+
return `- [${priority}] ${type} ${tag} :: ${summary}`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function postDigest(baseUrl, message) {
|
|
412
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/api/notify`;
|
|
413
|
+
const res = await fetch(url, {
|
|
414
|
+
method: "POST",
|
|
415
|
+
headers: { "Content-Type": "application/json" },
|
|
416
|
+
body: JSON.stringify({ message, priority: "low" }),
|
|
417
|
+
});
|
|
418
|
+
const text = await res.text();
|
|
419
|
+
if (!res.ok) {
|
|
420
|
+
throw new Error(`POST ${url} → ${res.status}: ${text}`);
|
|
421
|
+
}
|
|
422
|
+
return text;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function runDigest(args) {
|
|
426
|
+
const date = args.date ?? todayLocal();
|
|
427
|
+
if (!/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(date)) {
|
|
428
|
+
console.error(`Invalid --date: ${date}. Expected YYYY-MM-DD.`);
|
|
429
|
+
process.exit(2);
|
|
430
|
+
}
|
|
431
|
+
const dbPath = resolveDbPath(args.dbPath);
|
|
432
|
+
const baseUrl = args.baseUrl ?? "http://localhost:8321";
|
|
433
|
+
|
|
434
|
+
const data = loadDayData(dbPath, date);
|
|
435
|
+
const digest = buildDigest(date, data);
|
|
436
|
+
|
|
437
|
+
if (args.send) {
|
|
438
|
+
const reply = await postDigest(baseUrl, digest);
|
|
439
|
+
console.log(`Digest sent. Daemon response: ${reply}`);
|
|
440
|
+
} else {
|
|
441
|
+
console.log(digest);
|
|
442
|
+
console.log(
|
|
443
|
+
`\n(Use --send to POST this via ${baseUrl}/api/notify at priority \`low\`.)`,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function runSeed(args) {
|
|
449
|
+
const days = args.days ?? 7;
|
|
450
|
+
if (!Number.isInteger(days) || days < 1 || days > 30) {
|
|
451
|
+
console.error(`Invalid --days: ${days}. Expected 1..30.`);
|
|
452
|
+
process.exit(2);
|
|
453
|
+
}
|
|
454
|
+
const time = args.time ?? "22:00";
|
|
455
|
+
if (!/^[0-2][0-9]:[0-5][0-9]$/.test(time)) {
|
|
456
|
+
console.error(`Invalid --time: ${time}. Expected HH:MM.`);
|
|
457
|
+
process.exit(2);
|
|
458
|
+
}
|
|
459
|
+
const start = args.startDate ?? addDaysLocal(todayLocal(), 1);
|
|
460
|
+
if (!/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(start)) {
|
|
461
|
+
console.error(`Invalid --start-date: ${start}. Expected YYYY-MM-DD.`);
|
|
462
|
+
process.exit(2);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const baseUrl = (args.baseUrl ?? "http://localhost:8321").replace(/\/+$/, "");
|
|
466
|
+
const url = `${baseUrl}/api/schedule`;
|
|
467
|
+
|
|
468
|
+
// Description is plumbed straight into {event_data[task]} of the
|
|
469
|
+
// generated scheduled.task event. Encode the date and the explicit
|
|
470
|
+
// invocation so the firing LLM session has zero judgment to exercise:
|
|
471
|
+
// it sees a deterministic Bash command + a "do not duplicate
|
|
472
|
+
// delivery" instruction and exits.
|
|
473
|
+
const seeded = [];
|
|
474
|
+
for (let i = 0; i < days; i++) {
|
|
475
|
+
const date = addDaysLocal(start, i);
|
|
476
|
+
const iso = localIsoAt(date, time);
|
|
477
|
+
const description =
|
|
478
|
+
`Run message-discipline digest for ${date}: invoke ` +
|
|
479
|
+
`\`node scripts/message-discipline-digest.mjs --date ${date} --send\` ` +
|
|
480
|
+
`via Bash from the repo root. The script itself POSTs the digest via ` +
|
|
481
|
+
`/api/notify at priority \`low\`; you must NOT compose or send a ` +
|
|
482
|
+
`duplicate notification. Log one line to today.md ## Agent Log on ` +
|
|
483
|
+
`completion (e.g. \`HH:MM [digest] message-discipline digest sent\`).`;
|
|
484
|
+
const body = {
|
|
485
|
+
time: iso,
|
|
486
|
+
taskType: "agent_task",
|
|
487
|
+
description,
|
|
488
|
+
};
|
|
489
|
+
const res = await fetch(url, {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: { "Content-Type": "application/json" },
|
|
492
|
+
body: JSON.stringify(body),
|
|
493
|
+
});
|
|
494
|
+
const replyText = await res.text();
|
|
495
|
+
if (!res.ok) {
|
|
496
|
+
console.error(`Failed to seed ${date} @ ${time}: ${res.status} ${replyText}`);
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
seeded.push({ date, iso, response: replyText });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
console.log(
|
|
503
|
+
`Seeded ${seeded.length} message-discipline digest agent_schedule rows:`,
|
|
504
|
+
);
|
|
505
|
+
for (const s of seeded) {
|
|
506
|
+
console.log(` ${s.date} ${s.iso} ${s.response}`);
|
|
507
|
+
}
|
|
508
|
+
console.log(
|
|
509
|
+
"\nThe firing LLM session will need to invoke `node` from Bash. The default\n" +
|
|
510
|
+
"Claude allowed-tools list does NOT include Bash(node *), so the user will\n" +
|
|
511
|
+
"be prompted to approve the command pattern on first fire. Approve once,\n" +
|
|
512
|
+
"or pre-add `Bash(node *)` to the dashboard's allowed-tools override.",
|
|
513
|
+
);
|
|
514
|
+
console.log(
|
|
515
|
+
"\nAlternative without LLM round-trips: schedule the script directly via\n" +
|
|
516
|
+
"launchd / cron with the same daily cadence. Example launchd plist on macOS:\n" +
|
|
517
|
+
" ProgramArguments = (\"/usr/local/bin/node\", \"<repo>/scripts/message-discipline-digest.mjs\", \"--send\")\n" +
|
|
518
|
+
" StartCalendarInterval = { Hour = 22; Minute = 0; }\n" +
|
|
519
|
+
"Run for 7 days, then `launchctl unload` to retire.",
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function main() {
|
|
524
|
+
const args = parseArgs(process.argv.slice(2));
|
|
525
|
+
if (args.seed) {
|
|
526
|
+
await runSeed(args);
|
|
527
|
+
} else {
|
|
528
|
+
await runDigest(args);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
main().catch((err) => {
|
|
533
|
+
console.error(err.stack ?? err.message ?? String(err));
|
|
534
|
+
process.exit(1);
|
|
535
|
+
});
|