@forwardimpact/outpost 3.12.1 → 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.
- package/package.json +3 -2
- package/src/kb-manager.js +58 -5
- package/src/kb-validator.js +762 -0
- package/src/outpost.js +73 -8
- package/templates/.claude/agents/chief-of-staff.md +14 -6
- package/templates/.claude/agents/concierge.md +9 -2
- package/templates/.claude/agents/head-hunter.md +9 -2
- package/templates/.claude/agents/librarian.md +11 -4
- package/templates/.claude/agents/postman.md +9 -2
- package/templates/.claude/agents/recruiter.md +9 -2
- package/templates/.claude/skills/anarlog-follow/SKILL.md +53 -29
- package/templates/.claude/skills/anarlog-follow/references/sessions.md +52 -0
- package/templates/.claude/skills/anarlog-process/SKILL.md +88 -45
- package/templates/.claude/skills/anarlog-process/references/extraction.md +19 -11
- package/templates/.claude/skills/anarlog-process/references/sessions.md +92 -58
- package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +404 -163
- package/templates/.claude/skills/candidate-report/SKILL.md +12 -9
- package/templates/.claude/skills/changelog/SKILL.md +106 -56
- package/templates/.claude/skills/deck-create/SKILL.md +60 -2
- package/templates/.claude/skills/deck-review/SKILL.md +4 -1
- package/templates/.claude/skills/deck-summarize/SKILL.md +6 -2
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +1 -1
- package/templates/.claude/skills/doc-collab/SKILL.md +16 -11
- package/templates/.claude/skills/doc-create/SKILL.md +6 -3
- package/templates/.claude/skills/draft-emails/SKILL.md +31 -25
- package/templates/.claude/skills/draft-emails/references/template.md +1 -1
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +14 -8
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +9 -3
- package/templates/.claude/skills/extract-entities/SKILL.md +17 -16
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +2 -1
- package/templates/.claude/skills/extract-entities/references/conditions.md +6 -5
- package/templates/.claude/skills/extract-entities/references/links.md +34 -10
- package/templates/.claude/skills/extract-entities/references/recruitment.md +15 -10
- package/templates/.claude/skills/extract-entities/references/resolution.md +1 -1
- package/templates/.claude/skills/extract-entities/references/sources.md +1 -1
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +10 -4
- package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +29 -10
- package/templates/.claude/skills/extract-entities/references/templates-priorities.md +8 -2
- package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +19 -7
- package/templates/.claude/skills/meeting-prep/SKILL.md +23 -18
- package/templates/.claude/skills/organize-files/SKILL.md +3 -0
- package/templates/.claude/skills/person-identify/SKILL.md +58 -10
- package/templates/.claude/skills/person-identify/scripts/identify.sh +110 -11
- package/templates/.claude/skills/person-lookup/SKILL.md +8 -3
- package/templates/.claude/skills/person-lookup/scripts/lookup.sh +27 -1
- package/templates/.claude/skills/req-assess/SKILL.md +19 -11
- package/templates/.claude/skills/req-assess/references/interview-template.md +6 -1
- package/templates/.claude/skills/req-assess/references/panel-template.md +5 -1
- package/templates/.claude/skills/req-bundle/SKILL.md +164 -0
- package/templates/.claude/skills/req-bundle/references/matching.md +70 -0
- package/templates/.claude/skills/req-bundle/references/pdf-structure.md +74 -0
- package/templates/.claude/skills/req-bundle/scripts/split-bundle.mjs +377 -0
- package/templates/.claude/skills/req-decide/SKILL.md +26 -18
- package/templates/.claude/skills/req-decide/references/template.md +2 -2
- package/templates/.claude/skills/req-forget/SKILL.md +39 -22
- package/templates/.claude/skills/req-forget/references/classify.md +12 -10
- package/templates/.claude/skills/req-forget/references/locations.md +27 -22
- package/templates/.claude/skills/req-forget/references/report-template.md +23 -15
- package/templates/.claude/skills/req-scan/SKILL.md +6 -3
- package/templates/.claude/skills/req-scan/references/template.md +11 -1
- package/templates/.claude/skills/req-screen/SKILL.md +78 -34
- package/templates/.claude/skills/req-screen/references/rubric.md +38 -42
- package/templates/.claude/skills/req-screen/references/scoring.md +79 -0
- package/templates/.claude/skills/req-screen/references/template.md +26 -14
- package/templates/.claude/skills/req-track/SKILL.md +44 -44
- package/templates/.claude/skills/req-track/references/fields.md +18 -15
- package/templates/.claude/skills/req-track/references/overlays.md +42 -0
- package/templates/.claude/skills/req-track/references/signals.md +2 -2
- package/templates/.claude/skills/req-track/references/templates.md +28 -27
- package/templates/.claude/skills/req-workday/SKILL.md +49 -20
- package/templates/.claude/skills/req-workday/references/brief.md +81 -0
- package/templates/.claude/skills/req-workday/references/templates.md +6 -73
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +19 -0
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +96 -20
- package/templates/.claude/skills/send-chat/SKILL.md +11 -11
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +3 -0
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +3 -0
- package/templates/.claude/skills/sync-teams/SKILL.md +3 -0
- package/templates/.claude/skills/upstream-instructions/SKILL.md +8 -5
- package/templates/.claude/skills/upstream-instructions/references/examples.md +3 -3
- package/templates/CLAUDE.md +96 -87
- package/templates/MIGRATION.md +359 -0
- package/templates/registry.yaml +25 -0
- package/templates/.claude/skills/anarlog-follow/scripts/follow.mjs +0 -243
- package/templates/.claude/skills/anarlog-trim/SKILL.md +0 -186
|
@@ -1,57 +1,160 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Scan for unprocessed Anarlog
|
|
3
|
+
* Scan for unprocessed Anarlog meetings.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
9
19
|
*
|
|
10
20
|
* Usage:
|
|
11
|
-
* node scripts/scan.mjs
|
|
12
|
-
* node scripts/scan.mjs --changed
|
|
13
|
-
* node scripts/scan.mjs --json
|
|
14
|
-
* node scripts/scan.mjs --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).
|
|
15
33
|
*/
|
|
16
34
|
|
|
35
|
+
import { execFileSync } from "node:child_process";
|
|
17
36
|
import { createHash } from "node:crypto";
|
|
18
|
-
import {
|
|
19
|
-
|
|
37
|
+
import {
|
|
38
|
+
existsSync,
|
|
39
|
+
mkdirSync,
|
|
40
|
+
readFileSync,
|
|
41
|
+
readdirSync,
|
|
42
|
+
statSync,
|
|
43
|
+
writeFileSync,
|
|
44
|
+
} from "node:fs";
|
|
20
45
|
import { homedir } from "node:os";
|
|
46
|
+
import { dirname, join } from "node:path";
|
|
21
47
|
|
|
22
48
|
const HOME = homedir();
|
|
23
49
|
const SESSIONS_DIR = join(HOME, "Library/Application Support/anarlog/sessions");
|
|
24
|
-
const STATE_FILE =
|
|
50
|
+
const STATE_FILE =
|
|
51
|
+
process.env.OUTPOST_STATE_FILE ||
|
|
52
|
+
join(HOME, ".cache/fit/outpost/state/graph_processed");
|
|
53
|
+
|
|
54
|
+
// --- Args ---
|
|
25
55
|
|
|
26
56
|
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
27
|
-
console.log(`scan — find unprocessed Anarlog
|
|
57
|
+
console.log(`scan — find unprocessed Anarlog meetings
|
|
28
58
|
|
|
29
59
|
Usage:
|
|
30
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
|
|
31
63
|
|
|
32
64
|
Options:
|
|
33
|
-
--changed Also detect
|
|
34
|
-
--json Output as
|
|
35
|
-
--count Just print the unprocessed count (for
|
|
36
|
-
--limit N Max
|
|
65
|
+
--changed Also detect meetings whose note/summary content has changed
|
|
66
|
+
--json Output as JSON array
|
|
67
|
+
--count Just print the unprocessed count (for scripting)
|
|
68
|
+
--limit N Max meetings to display (default: 20)
|
|
69
|
+
--legacy Force the flat-file scan even if the CLI is available
|
|
37
70
|
-h, --help Show this help message
|
|
38
71
|
|
|
39
|
-
|
|
40
|
-
|
|
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}`);
|
|
41
75
|
process.exit(0);
|
|
42
76
|
}
|
|
43
77
|
|
|
44
78
|
const args = process.argv.slice(2);
|
|
79
|
+
const subcommand = args[0] && !args[0].startsWith("-") ? args[0] : null;
|
|
45
80
|
const detectChanged = args.includes("--changed");
|
|
46
81
|
const jsonOutput = args.includes("--json");
|
|
47
82
|
const countOnly = args.includes("--count");
|
|
83
|
+
const forceLegacy = args.includes("--legacy");
|
|
48
84
|
const limitIdx = args.indexOf("--limit");
|
|
49
85
|
const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) || 20 : 20;
|
|
50
86
|
|
|
51
|
-
// ---
|
|
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
|
+
}
|
|
52
151
|
|
|
53
|
-
|
|
54
|
-
|
|
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;
|
|
55
158
|
const text = readFileSync(STATE_FILE, "utf8");
|
|
56
159
|
for (const line of text.split("\n")) {
|
|
57
160
|
if (!line) continue;
|
|
@@ -59,186 +162,324 @@ if (existsSync(STATE_FILE)) {
|
|
|
59
162
|
if (idx === -1) continue;
|
|
60
163
|
state.set(line.slice(0, idx), line.slice(idx + 1));
|
|
61
164
|
}
|
|
165
|
+
return state;
|
|
62
166
|
}
|
|
63
167
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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);
|
|
69
176
|
}
|
|
70
177
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(/ /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);
|
|
82
207
|
}
|
|
83
208
|
|
|
84
209
|
/**
|
|
85
|
-
*
|
|
210
|
+
* Read a meeting's processable content via the CLI.
|
|
211
|
+
* Returns { note, summary, combined, hasContent, participants, actionItems }.
|
|
86
212
|
*/
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
90
232
|
|
|
91
|
-
|
|
92
|
-
const body = content.replace(/---[\s\S]*?---/, "").trim();
|
|
93
|
-
if (!body || body === " ") return null;
|
|
233
|
+
// --- CLI scan ---
|
|
94
234
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 = [];
|
|
98
240
|
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
let date = dateMatch ? dateMatch[0] : null;
|
|
241
|
+
for (const m of meetings) {
|
|
242
|
+
const id = m.id;
|
|
102
243
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
date = stat.mtime.toISOString().slice(0, 10);
|
|
106
|
-
}
|
|
244
|
+
// Legacy sessions were handled under the old flat-file regime — frozen.
|
|
245
|
+
if (hasLegacyKey(state, id)) continue;
|
|
107
246
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
}
|
|
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;
|
|
113
250
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
});
|
|
124
279
|
}
|
|
125
|
-
}
|
|
126
280
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (!existsSync(SESSIONS_DIR)) {
|
|
130
|
-
console.error(`Anarlog sessions directory not found: ${SESSIONS_DIR}`);
|
|
131
|
-
process.exit(1);
|
|
281
|
+
unprocessed.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
|
282
|
+
return { unprocessed, total: meetings.length, mode: "cli" };
|
|
132
283
|
}
|
|
133
284
|
|
|
134
|
-
|
|
135
|
-
const unprocessed = [];
|
|
136
|
-
let totalWithMemos = 0;
|
|
137
|
-
let processedCount = 0;
|
|
285
|
+
// --- Legacy flat-file scan (fallback when the CLI is unavailable) ---
|
|
138
286
|
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
if (
|
|
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
|
+
}
|
|
143
296
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
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
|
+
}
|
|
148
305
|
|
|
149
|
-
|
|
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 === " ") {
|
|
334
|
+
if (!hasSummary) continue; // empty memo, no summary → skip
|
|
335
|
+
} else {
|
|
336
|
+
memoText = content;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
150
339
|
|
|
151
|
-
|
|
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
|
+
}
|
|
152
372
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
: { needed: false, reason: null };
|
|
373
|
+
unprocessed.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
|
374
|
+
return { unprocessed, total, mode: "legacy" };
|
|
375
|
+
}
|
|
157
376
|
|
|
158
|
-
|
|
159
|
-
const summaryCheck = hasSummary
|
|
160
|
-
? needsProcessing(summaryPath)
|
|
161
|
-
: { needed: false, reason: null };
|
|
377
|
+
// --- mark ---
|
|
162
378
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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);
|
|
166
386
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (hasMemo && !memo) {
|
|
171
|
-
// Empty memo, no summary → skip
|
|
172
|
-
if (!hasSummary) continue;
|
|
387
|
+
if (ids.length === 0) {
|
|
388
|
+
console.error("Usage: scan.mjs mark <session-id> [<session-id>…]");
|
|
389
|
+
process.exit(1);
|
|
173
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
|
+
}
|
|
174
412
|
|
|
175
|
-
|
|
176
|
-
const meta = readMeta(sessionPath);
|
|
177
|
-
|
|
178
|
-
const title = memo?.title || meta?.title || uuid.slice(0, 8);
|
|
179
|
-
const date =
|
|
180
|
-
memo?.date ||
|
|
181
|
-
(meta?.created_at ? meta.created_at.slice(0, 10) : null) ||
|
|
182
|
-
statSync(sessionPath).mtime.toISOString().slice(0, 10);
|
|
413
|
+
// --- Output ---
|
|
183
414
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
title,
|
|
187
|
-
date,
|
|
188
|
-
hasMemo,
|
|
189
|
-
hasSummary,
|
|
190
|
-
memoReason: memoCheck.reason,
|
|
191
|
-
summaryReason: summaryCheck.reason,
|
|
192
|
-
preview: memo?.preview || "(summary only)",
|
|
193
|
-
memoPath: hasMemo ? memoPath : null,
|
|
194
|
-
summaryPath: hasSummary ? summaryPath : null,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
415
|
+
function output(result) {
|
|
416
|
+
const { unprocessed, total, mode } = result;
|
|
197
417
|
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}
|
|
200
426
|
|
|
201
|
-
|
|
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
|
+
}
|
|
202
434
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
}
|
|
206
451
|
}
|
|
207
452
|
|
|
208
|
-
|
|
209
|
-
console.log(JSON.stringify(unprocessed.slice(0, limit), null, 2));
|
|
210
|
-
process.exit(0);
|
|
211
|
-
}
|
|
453
|
+
// --- Main ---
|
|
212
454
|
|
|
213
|
-
|
|
214
|
-
console.log(
|
|
215
|
-
`Sessions: ${totalWithMemos} total, ${processedCount} processed, ${unprocessed.length} unprocessed`,
|
|
216
|
-
);
|
|
455
|
+
const cli = resolveCli();
|
|
217
456
|
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
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);
|
|
221
464
|
}
|
|
222
465
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (s.memoReason) flags.push(`memo:${s.memoReason}`);
|
|
228
|
-
if (s.summaryReason) flags.push(`summary:${s.summaryReason}`);
|
|
229
|
-
const sources = [];
|
|
230
|
-
if (s.hasMemo) sources.push("memo");
|
|
231
|
-
if (s.hasSummary) sources.push("summary");
|
|
232
|
-
|
|
233
|
-
console.log(
|
|
234
|
-
`${s.date} | ${s.title} | ${sources.join("+")} | ${flags.join(", ")}`,
|
|
466
|
+
if (subcommand === "mark") {
|
|
467
|
+
doMark(
|
|
468
|
+
cli,
|
|
469
|
+
args.slice(1).filter((a) => !a.startsWith("-")),
|
|
235
470
|
);
|
|
236
|
-
|
|
237
|
-
if (s.preview && s.preview !== "(summary only)") {
|
|
238
|
-
console.log(` ${s.preview.slice(0, 100)}…`);
|
|
239
|
-
}
|
|
471
|
+
process.exit(0);
|
|
240
472
|
}
|
|
241
473
|
|
|
242
|
-
|
|
243
|
-
|
|
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();
|
|
244
484
|
}
|
|
485
|
+
output(result);
|