@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.
- package/README.md +2 -2
- package/bin/fit-outpost.js +3 -2
- package/package.json +6 -5
- package/src/agent-path.js +10 -6
- package/src/agent-runner.js +28 -24
- package/src/index.js +2 -3
- package/src/kb-manager.js +75 -20
- package/src/kb-validator.js +762 -0
- package/src/outpost.js +108 -39
- package/src/posture.js +3 -3
- package/src/privilege.js +11 -11
- package/src/scheduler.js +14 -12
- package/src/socket-server.js +19 -18
- package/src/spawn-env.js +7 -6
- package/src/state-manager.js +2 -2
- package/templates/.claude/agents/chief-of-staff.md +32 -24
- package/templates/.claude/agents/concierge.md +26 -17
- package/templates/.claude/agents/head-hunter.md +34 -26
- package/templates/.claude/agents/librarian.md +26 -18
- package/templates/.claude/agents/postman.md +26 -18
- package/templates/.claude/agents/recruiter.md +38 -27
- package/templates/.claude/skills/anarlog-follow/SKILL.md +91 -66
- package/templates/.claude/skills/anarlog-follow/references/coaching.md +22 -21
- package/templates/.claude/skills/anarlog-follow/references/sessions.md +52 -0
- package/templates/.claude/skills/anarlog-process/SKILL.md +108 -62
- package/templates/.claude/skills/anarlog-process/references/extraction.md +41 -29
- package/templates/.claude/skills/anarlog-process/references/sessions.md +92 -58
- package/templates/.claude/skills/anarlog-process/scripts/scan.mjs +402 -160
- package/templates/.claude/skills/candidate-report/SKILL.md +41 -36
- package/templates/.claude/skills/candidate-report/references/report-template.html +10 -10
- package/templates/.claude/skills/candidate-report/references/report.css +4 -4
- package/templates/.claude/skills/candidate-report/references/rubric.md +15 -14
- package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +2 -2
- package/templates/.claude/skills/changelog/SKILL.md +122 -67
- package/templates/.claude/skills/deck-create/SKILL.md +90 -32
- package/templates/.claude/skills/deck-create/references/slide.css +4 -4
- package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/deck-review/SKILL.md +63 -58
- package/templates/.claude/skills/deck-review/assets/slide-annotator.js +58 -49
- package/templates/.claude/skills/deck-summarize/SKILL.md +55 -45
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +5 -4
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +8 -7
- package/templates/.claude/skills/doc-collab/SKILL.md +39 -34
- package/templates/.claude/skills/doc-create/SKILL.md +27 -23
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +6 -6
- package/templates/.claude/skills/draft-emails/SKILL.md +63 -53
- package/templates/.claude/skills/draft-emails/references/template.md +2 -2
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +15 -8
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +19 -11
- package/templates/.claude/skills/extract-entities/SKILL.md +76 -75
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +6 -5
- package/templates/.claude/skills/extract-entities/references/conditions.md +20 -18
- package/templates/.claude/skills/extract-entities/references/content.md +10 -9
- package/templates/.claude/skills/extract-entities/references/links.md +37 -13
- package/templates/.claude/skills/extract-entities/references/recruitment.md +30 -24
- package/templates/.claude/skills/extract-entities/references/resolution.md +14 -14
- package/templates/.claude/skills/extract-entities/references/sources.md +9 -9
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +12 -6
- package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +29 -10
- package/templates/.claude/skills/extract-entities/references/templates-priorities.md +10 -4
- package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +19 -7
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +9 -8
- package/templates/.claude/skills/meeting-prep/SKILL.md +42 -36
- package/templates/.claude/skills/organize-files/SKILL.md +27 -22
- package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +5 -4
- package/templates/.claude/skills/organize-files/scripts/summarize.mjs +4 -4
- package/templates/.claude/skills/person-identify/SKILL.md +77 -27
- package/templates/.claude/skills/person-identify/scripts/identify.sh +125 -26
- package/templates/.claude/skills/person-lookup/SKILL.md +34 -30
- package/templates/.claude/skills/person-lookup/scripts/lookup.sh +46 -17
- package/templates/.claude/skills/req-assess/SKILL.md +44 -36
- package/templates/.claude/skills/req-assess/references/interview-template.md +7 -2
- package/templates/.claude/skills/req-assess/references/panel-template.md +7 -2
- package/templates/.claude/skills/req-assess/references/rubric.md +13 -13
- 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 +52 -41
- package/templates/.claude/skills/req-decide/references/rubric.md +11 -10
- package/templates/.claude/skills/req-decide/references/template.md +2 -2
- package/templates/.claude/skills/req-forget/SKILL.md +58 -39
- package/templates/.claude/skills/req-forget/references/classify.md +15 -13
- package/templates/.claude/skills/req-forget/references/locations.md +27 -22
- package/templates/.claude/skills/req-forget/references/report-template.md +24 -16
- package/templates/.claude/skills/req-scan/SKILL.md +27 -25
- package/templates/.claude/skills/req-scan/references/fallbacks.md +9 -9
- package/templates/.claude/skills/req-scan/references/filters.md +13 -13
- package/templates/.claude/skills/req-scan/references/sources.md +7 -7
- package/templates/.claude/skills/req-scan/references/state.md +1 -1
- package/templates/.claude/skills/req-scan/references/template.md +11 -1
- package/templates/.claude/skills/req-scan/scripts/state.mjs +3 -2
- package/templates/.claude/skills/req-screen/SKILL.md +96 -50
- package/templates/.claude/skills/req-screen/references/rubric.md +46 -50
- package/templates/.claude/skills/req-screen/references/scoring.md +79 -0
- package/templates/.claude/skills/req-screen/references/template.md +29 -17
- package/templates/.claude/skills/req-track/SKILL.md +76 -76
- package/templates/.claude/skills/req-track/references/fields.md +31 -27
- package/templates/.claude/skills/req-track/references/overlays.md +42 -0
- package/templates/.claude/skills/req-track/references/signals.md +12 -12
- package/templates/.claude/skills/req-track/references/statuses.md +3 -2
- package/templates/.claude/skills/req-track/references/templates.md +28 -27
- package/templates/.claude/skills/req-workday/SKILL.md +69 -40
- package/templates/.claude/skills/req-workday/references/brief.md +81 -0
- package/templates/.claude/skills/req-workday/references/status-mapping.md +5 -5
- package/templates/.claude/skills/req-workday/references/templates.md +11 -78
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +26 -7
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +113 -35
- package/templates/.claude/skills/send-chat/SKILL.md +58 -54
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +23 -19
- package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +7 -7
- package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +6 -6
- package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +9 -8
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +20 -14
- package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +10 -10
- package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +9 -9
- package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +16 -13
- package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +3 -3
- package/templates/.claude/skills/sync-teams/SKILL.md +46 -43
- package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +22 -21
- package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +4 -4
- package/templates/.claude/skills/sync-teams/scripts/sync.mjs +4 -4
- package/templates/.claude/skills/upstream-instructions/SKILL.md +43 -41
- package/templates/.claude/skills/upstream-instructions/references/examples.md +5 -5
- package/templates/CLAUDE.md +97 -85
- 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 -183
|
@@ -1,56 +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
|
-
*
|
|
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
|
|
11
|
-
* node scripts/scan.mjs --changed
|
|
12
|
-
* node scripts/scan.mjs --json
|
|
13
|
-
* 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).
|
|
14
33
|
*/
|
|
15
34
|
|
|
35
|
+
import { execFileSync } from "node:child_process";
|
|
16
36
|
import { createHash } from "node:crypto";
|
|
17
|
-
import {
|
|
18
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
// ---
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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);
|
|
81
207
|
}
|
|
82
208
|
|
|
83
209
|
/**
|
|
84
|
-
*
|
|
210
|
+
* Read a meeting's processable content via the CLI.
|
|
211
|
+
* Returns { note, summary, combined, hasContent, participants, actionItems }.
|
|
85
212
|
*/
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
const body = content.replace(/---[\s\S]*?---/, "").trim();
|
|
92
|
-
if (!body || body === " ") return null;
|
|
233
|
+
// --- CLI scan ---
|
|
93
234
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
const
|
|
100
|
-
let date = dateMatch ? dateMatch[0] : null;
|
|
241
|
+
for (const m of meetings) {
|
|
242
|
+
const id = m.id;
|
|
101
243
|
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
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
|
+
}
|
|
142
296
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
149
339
|
|
|
150
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
const summaryCheck = hasSummary
|
|
159
|
-
? needsProcessing(summaryPath)
|
|
160
|
-
: { needed: false, reason: null };
|
|
377
|
+
// --- mark ---
|
|
161
378
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
208
|
-
console.log(JSON.stringify(unprocessed.slice(0, limit), null, 2));
|
|
209
|
-
process.exit(0);
|
|
210
|
-
}
|
|
453
|
+
// --- Main ---
|
|
211
454
|
|
|
212
|
-
|
|
213
|
-
console.log(
|
|
214
|
-
`Sessions: ${totalWithMemos} total, ${processedCount} processed, ${unprocessed.length} unprocessed`,
|
|
215
|
-
);
|
|
455
|
+
const cli = resolveCli();
|
|
216
456
|
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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);
|