@forwardimpact/outpost 3.12.0 → 3.13.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 (129) hide show
  1. package/README.md +2 -2
  2. package/bin/fit-outpost.js +3 -2
  3. package/package.json +6 -5
  4. package/src/agent-path.js +10 -6
  5. package/src/agent-runner.js +28 -24
  6. package/src/index.js +2 -3
  7. package/src/kb-manager.js +75 -20
  8. package/src/kb-validator.js +762 -0
  9. package/src/outpost.js +108 -39
  10. package/src/posture.js +3 -3
  11. package/src/privilege.js +11 -11
  12. package/src/scheduler.js +14 -12
  13. package/src/socket-server.js +19 -18
  14. package/src/spawn-env.js +7 -6
  15. package/src/state-manager.js +2 -2
  16. package/templates/.claude/agents/chief-of-staff.md +32 -24
  17. package/templates/.claude/agents/concierge.md +26 -17
  18. package/templates/.claude/agents/head-hunter.md +34 -26
  19. package/templates/.claude/agents/librarian.md +26 -18
  20. package/templates/.claude/agents/postman.md +26 -18
  21. package/templates/.claude/agents/recruiter.md +38 -27
  22. package/templates/.claude/skills/anarlog-follow/SKILL.md +91 -66
  23. package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
  24. package/templates/.claude/skills/anarlog-follow/references/sessions.md +52 -0
  25. package/templates/.claude/skills/anarlog-process/SKILL.md +108 -62
  26. package/templates/.claude/skills/anarlog-process/references/extraction.md +41 -29
  27. package/templates/.claude/skills/anarlog-process/references/sessions.md +92 -58
  28. package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +402 -160
  29. package/templates/.claude/skills/candidate-report/SKILL.md +41 -36
  30. package/templates/.claude/skills/candidate-report/references/report-template.html +10 -10
  31. package/templates/.claude/skills/candidate-report/references/report.css +4 -4
  32. package/templates/.claude/skills/candidate-report/references/rubric.md +15 -14
  33. package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +2 -2
  34. package/templates/.claude/skills/changelog/SKILL.md +122 -67
  35. package/templates/.claude/skills/deck-create/SKILL.md +90 -32
  36. package/templates/.claude/skills/deck-create/references/slide.css +4 -4
  37. package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +6 -6
  38. package/templates/.claude/skills/deck-review/SKILL.md +63 -58
  39. package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
  40. package/templates/.claude/skills/deck-summarize/SKILL.md +55 -45
  41. package/templates/.claude/skills/deck-summarize/references/brief-template.md +5 -4
  42. package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
  43. package/templates/.claude/skills/doc-collab/SKILL.md +39 -34
  44. package/templates/.claude/skills/doc-create/SKILL.md +27 -23
  45. package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
  46. package/templates/.claude/skills/draft-emails/SKILL.md +63 -53
  47. package/templates/.claude/skills/draft-emails/references/template.md +2 -2
  48. package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +15 -8
  49. package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +19 -11
  50. package/templates/.claude/skills/extract-entities/SKILL.md +76 -75
  51. package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +6 -5
  52. package/templates/.claude/skills/extract-entities/references/conditions.md +20 -18
  53. package/templates/.claude/skills/extract-entities/references/content.md +10 -9
  54. package/templates/.claude/skills/extract-entities/references/links.md +37 -13
  55. package/templates/.claude/skills/extract-entities/references/recruitment.md +30 -24
  56. package/templates/.claude/skills/extract-entities/references/resolution.md +14 -14
  57. package/templates/.claude/skills/extract-entities/references/sources.md +9 -9
  58. package/templates/.claude/skills/extract-entities/references/templates-conditions.md +12 -6
  59. package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +29 -10
  60. package/templates/.claude/skills/extract-entities/references/templates-priorities.md +10 -4
  61. package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +19 -7
  62. package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
  63. package/templates/.claude/skills/meeting-prep/SKILL.md +42 -36
  64. package/templates/.claude/skills/organize-files/SKILL.md +27 -22
  65. package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
  66. package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
  67. package/templates/.claude/skills/person-identify/SKILL.md +77 -27
  68. package/templates/.claude/skills/person-identify/scripts/identify.sh +125 -26
  69. package/templates/.claude/skills/person-lookup/SKILL.md +34 -30
  70. package/templates/.claude/skills/person-lookup/scripts/lookup.sh +46 -17
  71. package/templates/.claude/skills/req-assess/SKILL.md +44 -36
  72. package/templates/.claude/skills/req-assess/references/interview-template.md +7 -2
  73. package/templates/.claude/skills/req-assess/references/panel-template.md +7 -2
  74. package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
  75. package/templates/.claude/skills/req-bundle/SKILL.md +164 -0
  76. package/templates/.claude/skills/req-bundle/references/matching.md +70 -0
  77. package/templates/.claude/skills/req-bundle/references/pdf-structure.md +74 -0
  78. package/templates/.claude/skills/req-bundle/scripts/split-bundle.mjs +377 -0
  79. package/templates/.claude/skills/req-decide/SKILL.md +52 -41
  80. package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
  81. package/templates/.claude/skills/req-decide/references/template.md +2 -2
  82. package/templates/.claude/skills/req-forget/SKILL.md +58 -39
  83. package/templates/.claude/skills/req-forget/references/classify.md +15 -13
  84. package/templates/.claude/skills/req-forget/references/locations.md +27 -22
  85. package/templates/.claude/skills/req-forget/references/report-template.md +24 -16
  86. package/templates/.claude/skills/req-scan/SKILL.md +27 -25
  87. package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
  88. package/templates/.claude/skills/req-scan/references/filters.md +13 -13
  89. package/templates/.claude/skills/req-scan/references/sources.md +7 -7
  90. package/templates/.claude/skills/req-scan/references/state.md +1 -1
  91. package/templates/.claude/skills/req-scan/references/template.md +11 -1
  92. package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
  93. package/templates/.claude/skills/req-screen/SKILL.md +96 -50
  94. package/templates/.claude/skills/req-screen/references/rubric.md +46 -50
  95. package/templates/.claude/skills/req-screen/references/scoring.md +79 -0
  96. package/templates/.claude/skills/req-screen/references/template.md +29 -17
  97. package/templates/.claude/skills/req-track/SKILL.md +76 -76
  98. package/templates/.claude/skills/req-track/references/fields.md +31 -27
  99. package/templates/.claude/skills/req-track/references/overlays.md +42 -0
  100. package/templates/.claude/skills/req-track/references/signals.md +12 -12
  101. package/templates/.claude/skills/req-track/references/statuses.md +3 -2
  102. package/templates/.claude/skills/req-track/references/templates.md +28 -27
  103. package/templates/.claude/skills/req-workday/SKILL.md +69 -40
  104. package/templates/.claude/skills/req-workday/references/brief.md +81 -0
  105. package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
  106. package/templates/.claude/skills/req-workday/references/templates.md +11 -78
  107. package/templates/.claude/skills/req-workday/references/xlsx-format.md +26 -7
  108. package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +113 -35
  109. package/templates/.claude/skills/send-chat/SKILL.md +58 -54
  110. package/templates/.claude/skills/sync-apple-calendar/SKILL.md +23 -19
  111. package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
  112. package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
  113. package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
  114. package/templates/.claude/skills/sync-apple-mail/SKILL.md +20 -14
  115. package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
  116. package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
  117. package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
  118. package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
  119. package/templates/.claude/skills/sync-teams/SKILL.md +46 -43
  120. package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
  121. package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
  122. package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
  123. package/templates/.claude/skills/upstream-instructions/SKILL.md +43 -41
  124. package/templates/.claude/skills/upstream-instructions/references/examples.md +5 -5
  125. package/templates/CLAUDE.md +97 -85
  126. package/templates/MIGRATION.md +359 -0
  127. package/templates/registry.yaml +25 -0
  128. package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +0 -243
  129. package/templates/.claude/skills/anarlog-trim/SKILL.md +0 -183
@@ -1,56 +1,160 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Scan for unprocessed Anarlog sessions.
3
+ * Scan for unprocessed Anarlog meetings.
4
4
  *
5
- * Compares session _memo.md and _summary.md files against the graph_processed
6
- * state file to identify sessions that need processing. Reports unprocessed
7
- * sessions with title, date, and content preview.
5
+ * Anarlog stores meetings in a local SQLite database (app.db). The supported,
6
+ * format-agnostic way to read them is the bundled `anarlog-cli` — the app's
7
+ * own AGENTS.md is explicit: use the CLI, do NOT crawl the filesystem or query
8
+ * SQLite directly. This script enumerates meetings via that CLI, compares them
9
+ * against the shared graph_processed state, and reports meetings that still
10
+ * need processing. Because it reads through the CLI it finds every meeting,
11
+ * including recent ones Anarlog has not exported to flat files yet.
12
+ *
13
+ * Older Anarlog versions exported each session to flat files
14
+ * (_memo.md / _summary.md) under sessions/{uuid}/. Those already-processed
15
+ * sessions are recorded in graph_processed under their file paths; this script
16
+ * treats them as frozen so they are not reprocessed. If the CLI is unavailable
17
+ * it falls back to scanning those flat files directly, so the skill keeps
18
+ * working however the data happens to be available.
8
19
  *
9
20
  * Usage:
10
- * node scripts/scan.mjs List unprocessed sessions
11
- * node scripts/scan.mjs --changed Also detect changed (re-edited) sessions
12
- * node scripts/scan.mjs --json Output as JSON
13
- * node scripts/scan.mjs --count Just print the count
21
+ * node scripts/scan.mjs List unprocessed meetings
22
+ * node scripts/scan.mjs --changed Also detect changed (re-edited) meetings
23
+ * node scripts/scan.mjs --json Output as JSON
24
+ * node scripts/scan.mjs --count Just print the count
25
+ * node scripts/scan.mjs --limit N Max meetings to display (default: 20)
26
+ * node scripts/scan.mjs --legacy Force the flat-file scan (fallback)
27
+ * node scripts/scan.mjs mark <id>... Mark meetings processed (after writing notes)
28
+ * node scripts/scan.mjs cli-path Print the resolved anarlog-cli path
29
+ *
30
+ * Env overrides:
31
+ * ANARLOG_CLI Path to the anarlog-cli binary.
32
+ * OUTPOST_STATE_FILE Path to the graph_processed TSV (for testing).
14
33
  */
15
34
 
35
+ import { execFileSync } from "node:child_process";
16
36
  import { createHash } from "node:crypto";
17
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
18
- import { join } from "node:path";
37
+ import {
38
+ existsSync,
39
+ mkdirSync,
40
+ readFileSync,
41
+ readdirSync,
42
+ statSync,
43
+ writeFileSync,
44
+ } from "node:fs";
19
45
  import { homedir } from "node:os";
46
+ import { dirname, join } from "node:path";
20
47
 
21
48
  const HOME = homedir();
22
49
  const SESSIONS_DIR = join(HOME, "Library/Application Support/anarlog/sessions");
23
- const STATE_FILE = join(HOME, ".cache/fit/outpost/state/graph_processed");
50
+ const STATE_FILE =
51
+ process.env.OUTPOST_STATE_FILE ||
52
+ join(HOME, ".cache/fit/outpost/state/graph_processed");
53
+
54
+ // --- Args ---
24
55
 
25
56
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
26
- console.log(`scan — find unprocessed Anarlog sessions
57
+ console.log(`scan — find unprocessed Anarlog meetings
27
58
 
28
59
  Usage:
29
60
  node scripts/scan.mjs [options]
61
+ node scripts/scan.mjs mark <id> [<id>…] Mark meetings processed
62
+ node scripts/scan.mjs cli-path Print the resolved anarlog-cli path
30
63
 
31
64
  Options:
32
- --changed Also detect sessions whose memo/summary hash has changed
65
+ --changed Also detect meetings whose note/summary content has changed
33
66
  --json Output as JSON array
34
67
  --count Just print the unprocessed count (for scripting)
35
- --limit N Max sessions to display (default: 20)
68
+ --limit N Max meetings to display (default: 20)
69
+ --legacy Force the flat-file scan even if the CLI is available
36
70
  -h, --help Show this help message
37
71
 
38
- Sessions dir: ~/Library/Application Support/anarlog/sessions/
39
- State file: ~/.cache/fit/outpost/state/graph_processed`);
72
+ Reads meetings through anarlog-cli (falls back to flat files under
73
+ ~/Library/Application Support/anarlog/sessions/ if the CLI is unavailable).
74
+ State file: ${STATE_FILE}`);
40
75
  process.exit(0);
41
76
  }
42
77
 
43
78
  const args = process.argv.slice(2);
79
+ const subcommand = args[0] && !args[0].startsWith("-") ? args[0] : null;
44
80
  const detectChanged = args.includes("--changed");
45
81
  const jsonOutput = args.includes("--json");
46
82
  const countOnly = args.includes("--count");
83
+ const forceLegacy = args.includes("--legacy");
47
84
  const limitIdx = args.indexOf("--limit");
48
85
  const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) || 20 : 20;
49
86
 
50
- // --- Load state ---
87
+ // --- CLI discovery ---
88
+
89
+ /** Resolve the anarlog-cli binary: env override, app bundle, then PATH. */
90
+ function resolveCli() {
91
+ const candidates = [];
92
+ if (process.env.ANARLOG_CLI) candidates.push(process.env.ANARLOG_CLI);
93
+ candidates.push("/Applications/Anarlog.app/Contents/MacOS/anarlog-cli");
94
+ for (const name of ["anarlog-cli", "anarlog"]) {
95
+ try {
96
+ const p = execFileSync("/usr/bin/which", [name], {
97
+ encoding: "utf8",
98
+ }).trim();
99
+ if (p) candidates.push(p);
100
+ } catch {
101
+ /* not on PATH */
102
+ }
103
+ }
104
+ for (const c of candidates) {
105
+ if (c && existsSync(c)) return c;
106
+ }
107
+ return null;
108
+ }
109
+
110
+ /** Run an anarlog-cli command and return its parsed `data` payload. */
111
+ function runCli(cli, cliArgs) {
112
+ const out = execFileSync(cli, cliArgs, {
113
+ encoding: "utf8",
114
+ maxBuffer: 128 * 1024 * 1024,
115
+ });
116
+ return JSON.parse(out).data;
117
+ }
118
+
119
+ /** True if the CLI can open the database read-only. */
120
+ function cliReady(cli) {
121
+ try {
122
+ const d = runCli(cli, ["--json", "doctor"]);
123
+ return !!(d && d.ready && d.database && d.database.exists);
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ /** Enumerate every meeting via the CLI, paging through the full list. */
130
+ function listAllMeetings(cli) {
131
+ const all = [];
132
+ const pageSize = 200;
133
+ let offset = 0;
134
+ for (;;) {
135
+ const page = runCli(cli, [
136
+ "--json",
137
+ "meetings",
138
+ "list",
139
+ "--limit",
140
+ String(pageSize),
141
+ "--offset",
142
+ String(offset),
143
+ ]);
144
+ if (!Array.isArray(page) || page.length === 0) break;
145
+ all.push(...page);
146
+ if (page.length < pageSize) break;
147
+ offset += pageSize;
148
+ }
149
+ return all;
150
+ }
51
151
 
52
- const state = new Map();
53
- if (existsSync(STATE_FILE)) {
152
+ // --- State ---
153
+
154
+ /** Load the graph_processed TSV into a Map of {key → hash}. */
155
+ function loadState() {
156
+ const state = new Map();
157
+ if (!existsSync(STATE_FILE)) return state;
54
158
  const text = readFileSync(STATE_FILE, "utf8");
55
159
  for (const line of text.split("\n")) {
56
160
  if (!line) continue;
@@ -58,186 +162,324 @@ if (existsSync(STATE_FILE)) {
58
162
  if (idx === -1) continue;
59
163
  state.set(line.slice(0, idx), line.slice(idx + 1));
60
164
  }
165
+ return state;
61
166
  }
62
167
 
63
- /**
64
- * Compute SHA-256 hash of file contents.
65
- */
66
- function fileHash(filePath) {
67
- return createHash("sha256").update(readFileSync(filePath)).digest("hex");
168
+ /** Write the state Map back to graph_processed (sorted, TSV). */
169
+ function saveState(state) {
170
+ mkdirSync(dirname(STATE_FILE), { recursive: true });
171
+ const entries = [...state.entries()].sort((a, b) => a[0].localeCompare(b[0]));
172
+ const text = entries.length
173
+ ? entries.map(([k, h]) => `${k}\t${h}`).join("\n") + "\n"
174
+ : "";
175
+ writeFileSync(STATE_FILE, text);
68
176
  }
69
177
 
70
- /**
71
- * Check if a file needs processing (new or changed).
72
- */
73
- function needsProcessing(filePath) {
74
- const storedHash = state.get(filePath);
75
- if (!storedHash) return { needed: true, reason: "new" };
76
- if (detectChanged) {
77
- const currentHash = fileHash(filePath);
78
- if (currentHash !== storedHash) return { needed: true, reason: "changed" };
79
- }
80
- return { needed: false, reason: null };
178
+ const sha256 = (s) => createHash("sha256").update(s).digest("hex");
179
+
180
+ // State keys. New meetings use one synthetic key per session; legacy sessions
181
+ // are keyed by the flat-file paths the old pipeline recorded.
182
+ const synthKey = (id) => `anarlog://${id}`;
183
+ const legacyMemoKey = (id) => join(SESSIONS_DIR, id, "_memo.md");
184
+ const legacySummaryKey = (id) => join(SESSIONS_DIR, id, "_summary.md");
185
+ const hasLegacyKey = (state, id) =>
186
+ state.has(legacyMemoKey(id)) || state.has(legacySummaryKey(id));
187
+
188
+ // --- Content helpers ---
189
+
190
+ /** A note body is empty if it is blank, a spacer, or only its title heading. */
191
+ function noteBody(markdown) {
192
+ const body = (markdown || "")
193
+ .replace(/^#\s+.*$/m, "") // drop the leading title heading
194
+ .replace(/&nbsp;/g, "")
195
+ .trim();
196
+ return body;
197
+ }
198
+
199
+ /** Collapse markdown to a one-line preview. */
200
+ function preview(markdown) {
201
+ return (markdown || "")
202
+ .replace(/^#+\s*/gm, "")
203
+ .replace(/[*_>`]/g, "")
204
+ .replace(/\s+/g, " ")
205
+ .trim()
206
+ .slice(0, 150);
81
207
  }
82
208
 
83
209
  /**
84
- * Extract title and date from a memo file.
210
+ * Read a meeting's processable content via the CLI.
211
+ * Returns { note, summary, combined, hasContent, participants, actionItems }.
85
212
  */
86
- function parseMemo(memoPath) {
87
- try {
88
- const content = readFileSync(memoPath, "utf8");
213
+ function meetingContent(cli, id) {
214
+ const d = runCli(cli, ["--json", "meetings", "get", id]) || {};
215
+ const rawNote = (d.note && d.note.markdown) || "";
216
+ const note = noteBody(rawNote) ? rawNote.trim() : "";
217
+ const summary = (d.summaries || [])
218
+ .map((s) => (s.markdown || "").trim())
219
+ .filter(Boolean)
220
+ .join("\n\n");
221
+ return {
222
+ title: d.title || "",
223
+ createdAt: d.created_at || "",
224
+ note,
225
+ summary,
226
+ combined: `${note}\n---\n${summary}`,
227
+ hasContent: Boolean(note || summary),
228
+ participants: (d.participants || []).length,
229
+ actionItems: (d.action_items || []).length,
230
+ };
231
+ }
89
232
 
90
- // Skip empty/whitespace-only memos
91
- const body = content.replace(/---[\s\S]*?---/, "").trim();
92
- if (!body || body === "&nbsp;") return null;
233
+ // --- CLI scan ---
93
234
 
94
- // Extract title from first H1
95
- const titleMatch = content.match(/^#\s+(.+)/m);
96
- const title = titleMatch ? titleMatch[1].trim() : null;
235
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: per-meeting skip/change/legacy triage in one pass
236
+ function scanCli(cli) {
237
+ const state = loadState();
238
+ const meetings = listAllMeetings(cli);
239
+ const unprocessed = [];
97
240
 
98
- // Extract date from content or fall back to file mtime
99
- const dateMatch = content.match(/\d{4}-\d{2}-\d{2}/);
100
- let date = dateMatch ? dateMatch[0] : null;
241
+ for (const m of meetings) {
242
+ const id = m.id;
101
243
 
102
- if (!date) {
103
- const stat = statSync(memoPath);
104
- date = stat.mtime.toISOString().slice(0, 10);
105
- }
244
+ // Legacy sessions were handled under the old flat-file regime — frozen.
245
+ if (hasLegacyKey(state, id)) continue;
106
246
 
107
- return { title, date, preview: body.slice(0, 150).replace(/\n/g, " ") };
108
- } catch {
109
- return null;
110
- }
111
- }
247
+ const stored = state.get(synthKey(id));
248
+ // Already marked and we aren't looking for edits → skip without a fetch.
249
+ if (stored && !detectChanged) continue;
112
250
 
113
- /**
114
- * Read _meta.json for session metadata.
115
- */
116
- function readMeta(sessionDir) {
117
- const metaPath = join(sessionDir, "_meta.json");
118
- if (!existsSync(metaPath)) return null;
119
- try {
120
- return JSON.parse(readFileSync(metaPath, "utf8"));
121
- } catch {
122
- return null;
251
+ let c;
252
+ try {
253
+ c = meetingContent(cli, id);
254
+ } catch {
255
+ continue; // unreadable meeting — skip rather than crash the scan
256
+ }
257
+ if (!c.hasContent) continue; // empty/onboarding/test session → skip
258
+
259
+ const hash = sha256(c.combined);
260
+ let reason;
261
+ if (!stored) reason = "new";
262
+ else if (stored !== hash) reason = "changed";
263
+ else continue; // marked and unchanged
264
+
265
+ const sources = [];
266
+ if (c.note) sources.push("note");
267
+ if (c.summary) sources.push("summary");
268
+
269
+ unprocessed.push({
270
+ id,
271
+ title: c.title || m.title || id.slice(0, 8),
272
+ date: (m.created_at || c.createdAt || "").slice(0, 10),
273
+ sources,
274
+ reason,
275
+ participants: c.participants,
276
+ actionItems: c.actionItems,
277
+ preview: preview(c.summary || c.note),
278
+ });
123
279
  }
124
- }
125
280
 
126
- // --- Scan sessions ---
127
-
128
- if (!existsSync(SESSIONS_DIR)) {
129
- console.error(`Anarlog sessions directory not found: ${SESSIONS_DIR}`);
130
- process.exit(1);
281
+ unprocessed.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
282
+ return { unprocessed, total: meetings.length, mode: "cli" };
131
283
  }
132
284
 
133
- const sessionIds = readdirSync(SESSIONS_DIR);
134
- const unprocessed = [];
135
- let totalWithMemos = 0;
136
- let processedCount = 0;
285
+ // --- Legacy flat-file scan (fallback when the CLI is unavailable) ---
137
286
 
138
- for (const uuid of sessionIds) {
139
- const sessionPath = join(SESSIONS_DIR, uuid);
140
- const stat = statSync(sessionPath, { throwIfNoEntry: false });
141
- if (!stat || !stat.isDirectory()) continue;
287
+ function needsFile(state, filePath) {
288
+ const stored = state.get(filePath);
289
+ if (!stored) return { needed: true, reason: "new" };
290
+ if (detectChanged) {
291
+ const current = sha256(readFileSync(filePath, "utf8"));
292
+ if (current !== stored) return { needed: true, reason: "changed" };
293
+ }
294
+ return { needed: false, reason: null };
295
+ }
142
296
 
143
- const memoPath = join(sessionPath, "_memo.md");
144
- const summaryPath = join(sessionPath, "_summary.md");
145
- const hasMemo = existsSync(memoPath);
146
- const hasSummary = existsSync(summaryPath);
297
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: verbatim legacy flat-file scan kept as the fallback path
298
+ function scanLegacy() {
299
+ const state = loadState();
300
+ if (!existsSync(SESSIONS_DIR)) {
301
+ console.error(`Anarlog sessions directory not found: ${SESSIONS_DIR}`);
302
+ console.error("The anarlog-cli was also not found — nothing to scan.");
303
+ process.exit(1);
304
+ }
147
305
 
148
- if (!hasMemo && !hasSummary) continue;
306
+ const unprocessed = [];
307
+ let total = 0;
308
+
309
+ for (const uuid of readdirSync(SESSIONS_DIR)) {
310
+ const dir = join(SESSIONS_DIR, uuid);
311
+ const st = statSync(dir, { throwIfNoEntry: false });
312
+ if (!st || !st.isDirectory()) continue;
313
+
314
+ const memoPath = join(dir, "_memo.md");
315
+ const summaryPath = join(dir, "_summary.md");
316
+ const hasMemo = existsSync(memoPath);
317
+ const hasSummary = existsSync(summaryPath);
318
+ if (!hasMemo && !hasSummary) continue;
319
+ total++;
320
+
321
+ const memoCheck = hasMemo
322
+ ? needsFile(state, memoPath)
323
+ : { needed: false, reason: null };
324
+ const summaryCheck = hasSummary
325
+ ? needsFile(state, summaryPath)
326
+ : { needed: false, reason: null };
327
+ if (!memoCheck.needed && !summaryCheck.needed) continue;
328
+
329
+ let memoText = "";
330
+ if (hasMemo) {
331
+ const content = readFileSync(memoPath, "utf8");
332
+ const body = content.replace(/---[\s\S]*?---/, "").trim();
333
+ if (!body || body === "&nbsp;") {
334
+ if (!hasSummary) continue; // empty memo, no summary → skip
335
+ } else {
336
+ memoText = content;
337
+ }
338
+ }
149
339
 
150
- totalWithMemos++;
340
+ let title = null;
341
+ let date = null;
342
+ const titleMatch = memoText.match(/^#\s+(.+)/m);
343
+ if (titleMatch) title = titleMatch[1].trim();
344
+ const dateMatch = memoText.match(/\d{4}-\d{2}-\d{2}/);
345
+ if (dateMatch) date = dateMatch[0];
346
+ if ((!title || !date) && existsSync(join(dir, "_meta.json"))) {
347
+ try {
348
+ const meta = JSON.parse(readFileSync(join(dir, "_meta.json"), "utf8"));
349
+ title = title || meta.title;
350
+ date = date || (meta.created_at ? meta.created_at.slice(0, 10) : null);
351
+ } catch {
352
+ /* ignore */
353
+ }
354
+ }
355
+ date = date || statSync(dir).mtime.toISOString().slice(0, 10);
356
+
357
+ const sources = [];
358
+ if (hasMemo && memoText) sources.push("note");
359
+ if (hasSummary) sources.push("summary");
360
+
361
+ unprocessed.push({
362
+ id: uuid,
363
+ title: title || uuid.slice(0, 8),
364
+ date,
365
+ sources,
366
+ reason: memoCheck.reason || summaryCheck.reason,
367
+ preview: preview(memoText.replace(/---[\s\S]*?---/, "")),
368
+ memoPath: hasMemo ? memoPath : null,
369
+ summaryPath: hasSummary ? summaryPath : null,
370
+ });
371
+ }
151
372
 
152
- // Check memo
153
- const memoCheck = hasMemo
154
- ? needsProcessing(memoPath)
155
- : { needed: false, reason: null };
373
+ unprocessed.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
374
+ return { unprocessed, total, mode: "legacy" };
375
+ }
156
376
 
157
- // Check summary
158
- const summaryCheck = hasSummary
159
- ? needsProcessing(summaryPath)
160
- : { needed: false, reason: null };
377
+ // --- mark ---
161
378
 
162
- if (!memoCheck.needed && !summaryCheck.needed) {
163
- processedCount++;
164
- continue;
379
+ function doMark(cli, ids) {
380
+ if (!cli) {
381
+ console.error("mark requires anarlog-cli, which was not found.");
382
+ console.error(
383
+ "For legacy flat-file sessions, use extract-entities/scripts/state.mjs update <path> instead.",
384
+ );
385
+ process.exit(1);
165
386
  }
166
-
167
- // Parse memo for display info
168
- const memo = hasMemo ? parseMemo(memoPath) : null;
169
- if (hasMemo && !memo) {
170
- // Empty memo, no summary → skip
171
- if (!hasSummary) continue;
387
+ if (ids.length === 0) {
388
+ console.error("Usage: scan.mjs mark <session-id> [<session-id>…]");
389
+ process.exit(1);
172
390
  }
391
+ const state = loadState();
392
+ let marked = 0;
393
+ for (const id of ids) {
394
+ let c;
395
+ try {
396
+ c = meetingContent(cli, id);
397
+ } catch (e) {
398
+ console.error(`skip ${id}: ${e.message}`);
399
+ continue;
400
+ }
401
+ if (!c.hasContent) {
402
+ console.error(`skip ${id}: no note or summary content`);
403
+ continue;
404
+ }
405
+ state.set(synthKey(id), sha256(c.combined));
406
+ marked++;
407
+ console.error(`marked ${id} — ${c.title}`);
408
+ }
409
+ saveState(state);
410
+ console.log(`Marked ${marked} meeting(s) processed in ${STATE_FILE}`);
411
+ }
173
412
 
174
- // Read meta for title fallback
175
- const meta = readMeta(sessionPath);
176
-
177
- const title = memo?.title || meta?.title || uuid.slice(0, 8);
178
- const date =
179
- memo?.date ||
180
- (meta?.created_at ? meta.created_at.slice(0, 10) : null) ||
181
- statSync(sessionPath).mtime.toISOString().slice(0, 10);
413
+ // --- Output ---
182
414
 
183
- unprocessed.push({
184
- uuid,
185
- title,
186
- date,
187
- hasMemo,
188
- hasSummary,
189
- memoReason: memoCheck.reason,
190
- summaryReason: summaryCheck.reason,
191
- preview: memo?.preview || "(summary only)",
192
- memoPath: hasMemo ? memoPath : null,
193
- summaryPath: hasSummary ? summaryPath : null,
194
- });
195
- }
415
+ function output(result) {
416
+ const { unprocessed, total, mode } = result;
196
417
 
197
- // Sort by date descending (newest first)
198
- unprocessed.sort((a, b) => b.date.localeCompare(a.date));
418
+ if (countOnly) {
419
+ console.log(unprocessed.length);
420
+ return;
421
+ }
422
+ if (jsonOutput) {
423
+ console.log(JSON.stringify(unprocessed.slice(0, limit), null, 2));
424
+ return;
425
+ }
199
426
 
200
- // --- Output ---
427
+ console.log(
428
+ `Meetings: ${total} total, ${unprocessed.length} unprocessed (${mode} scan)`,
429
+ );
430
+ if (unprocessed.length === 0) {
431
+ console.log("\nAll meetings are up to date.");
432
+ return;
433
+ }
201
434
 
202
- if (countOnly) {
203
- console.log(unprocessed.length);
204
- process.exit(0);
435
+ console.log("");
436
+ for (const s of unprocessed.slice(0, limit)) {
437
+ const extra = [];
438
+ if (s.participants) extra.push(`${s.participants}p`);
439
+ if (s.actionItems) extra.push(`${s.actionItems} action items`);
440
+ console.log(
441
+ `${s.date} | ${s.title} | ${s.sources.join("+")} | ${s.reason}${
442
+ extra.length ? ` | ${extra.join(", ")}` : ""
443
+ }`,
444
+ );
445
+ console.log(` ${s.id}`);
446
+ if (s.preview) console.log(` ${s.preview.slice(0, 100)}…`);
447
+ }
448
+ if (unprocessed.length > limit) {
449
+ console.log(`\n... and ${unprocessed.length - limit} more`);
450
+ }
205
451
  }
206
452
 
207
- if (jsonOutput) {
208
- console.log(JSON.stringify(unprocessed.slice(0, limit), null, 2));
209
- process.exit(0);
210
- }
453
+ // --- Main ---
211
454
 
212
- // Formatted output
213
- console.log(
214
- `Sessions: ${totalWithMemos} total, ${processedCount} processed, ${unprocessed.length} unprocessed`,
215
- );
455
+ const cli = resolveCli();
216
456
 
217
- if (unprocessed.length === 0) {
218
- console.log("\nAll sessions are up to date.");
219
- process.exit(0);
457
+ if (subcommand === "cli-path") {
458
+ if (cli) {
459
+ console.log(cli);
460
+ process.exit(0);
461
+ }
462
+ console.error("anarlog-cli not found");
463
+ process.exit(1);
220
464
  }
221
465
 
222
- console.log("");
223
- const display = unprocessed.slice(0, limit);
224
- for (const s of display) {
225
- const flags = [];
226
- if (s.memoReason) flags.push(`memo:${s.memoReason}`);
227
- if (s.summaryReason) flags.push(`summary:${s.summaryReason}`);
228
- const sources = [];
229
- if (s.hasMemo) sources.push("memo");
230
- if (s.hasSummary) sources.push("summary");
231
-
232
- console.log(
233
- `${s.date} | ${s.title} | ${sources.join("+")} | ${flags.join(", ")}`,
466
+ if (subcommand === "mark") {
467
+ doMark(
468
+ cli,
469
+ args.slice(1).filter((a) => !a.startsWith("-")),
234
470
  );
235
- console.log(` ${s.uuid}`);
236
- if (s.preview && s.preview !== "(summary only)") {
237
- console.log(` ${s.preview.slice(0, 100)}…`);
238
- }
471
+ process.exit(0);
239
472
  }
240
473
 
241
- if (unprocessed.length > limit) {
242
- console.log(`\n... and ${unprocessed.length - limit} more`);
474
+ let result;
475
+ if (cli && !forceLegacy && cliReady(cli)) {
476
+ result = scanCli(cli);
477
+ } else {
478
+ if (!forceLegacy && !cli) {
479
+ console.error(
480
+ "anarlog-cli not found — falling back to legacy flat-file scan.\n",
481
+ );
482
+ }
483
+ result = scanLegacy();
243
484
  }
485
+ output(result);