@forwardimpact/basecamp 2.4.2 → 2.6.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/config/scheduler.json +10 -5
- package/package.json +1 -1
- package/src/basecamp.js +101 -729
- package/template/.claude/agents/chief-of-staff.md +14 -3
- package/template/.claude/agents/head-hunter.md +436 -0
- package/template/.claude/agents/librarian.md +1 -1
- package/template/.claude/settings.json +4 -1
- package/template/.claude/skills/analyze-cv/SKILL.md +39 -7
- package/template/.claude/skills/draft-emails/SKILL.md +29 -9
- package/template/.claude/skills/draft-emails/scripts/scan-emails.mjs +4 -4
- package/template/.claude/skills/draft-emails/scripts/send-email.mjs +41 -6
- package/template/.claude/skills/meeting-prep/SKILL.md +7 -4
- package/template/.claude/skills/process-hyprnote/SKILL.md +17 -8
- package/template/.claude/skills/process-hyprnote/scripts/scan.mjs +246 -0
- package/template/.claude/skills/scan-open-candidates/SKILL.md +476 -0
- package/template/.claude/skills/scan-open-candidates/scripts/state.mjs +396 -0
- package/template/.claude/skills/sync-apple-calendar/SKILL.md +41 -0
- package/template/.claude/skills/sync-apple-calendar/scripts/query.mjs +301 -0
- package/template/.claude/skills/synthesize-deck/SKILL.md +296 -0
- package/template/.claude/skills/synthesize-deck/scripts/extract-pptx.mjs +210 -0
- package/template/.claude/skills/track-candidates/SKILL.md +45 -0
- package/template/.claude/skills/workday-requisition/SKILL.md +86 -53
- package/template/.claude/skills/workday-requisition/scripts/parse-workday.mjs +103 -37
- package/template/CLAUDE.md +13 -3
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Scan for unprocessed emails and output their IDs and subjects.
|
|
4
4
|
*
|
|
5
5
|
* Checks ~/.cache/fit/basecamp/apple_mail/ for email thread markdown files not
|
|
6
|
-
* yet listed in drafts/
|
|
6
|
+
* yet listed in drafts/handled or drafts/ignored. Outputs one tab-separated
|
|
7
7
|
* line per unprocessed thread: email_id<TAB>subject. Used by the draft-emails
|
|
8
8
|
* skill to identify threads that need a reply.
|
|
9
9
|
*/
|
|
@@ -17,7 +17,7 @@ const HELP = `scan-emails — list unprocessed email threads
|
|
|
17
17
|
Usage: node scripts/scan-emails.mjs [-h|--help]
|
|
18
18
|
|
|
19
19
|
Scans ~/.cache/fit/basecamp/apple_mail/ for .md thread files not yet
|
|
20
|
-
recorded in drafts/
|
|
20
|
+
recorded in drafts/handled or drafts/ignored. Outputs one line per
|
|
21
21
|
unprocessed thread as: email_id<TAB>subject`;
|
|
22
22
|
|
|
23
23
|
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
@@ -49,14 +49,14 @@ function extractSubject(filePath) {
|
|
|
49
49
|
function main() {
|
|
50
50
|
if (!existsSync(MAIL_DIR)) return;
|
|
51
51
|
|
|
52
|
-
const
|
|
52
|
+
const handled = loadIdSet("drafts/handled");
|
|
53
53
|
const ignored = loadIdSet("drafts/ignored");
|
|
54
54
|
|
|
55
55
|
for (const name of readdirSync(MAIL_DIR).sort()) {
|
|
56
56
|
if (!name.endsWith(".md")) continue;
|
|
57
57
|
|
|
58
58
|
const emailId = basename(name, ".md");
|
|
59
|
-
if (
|
|
59
|
+
if (handled.has(emailId) || ignored.has(emailId)) continue;
|
|
60
60
|
|
|
61
61
|
const subject = extractSubject(join(MAIL_DIR, name));
|
|
62
62
|
console.log(`${emailId}\t${subject}`);
|
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
* Apple Mail. The script writes a temporary .scpt file, executes it with
|
|
7
7
|
* osascript, and cleans up afterwards. Mail.app must be running.
|
|
8
8
|
*
|
|
9
|
-
* The body should be plain text — no HTML. Do NOT include an email signature
|
|
10
|
-
* Apple Mail appends the user's configured signature automatically.
|
|
9
|
+
* The body should be plain text — no HTML. Do NOT include an email signature
|
|
10
|
+
* or sign-off; Apple Mail appends the user's configured signature automatically.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
appendFileSync,
|
|
16
|
+
mkdtempSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
unlinkSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { basename, join } from "node:path";
|
|
16
21
|
import { tmpdir } from "node:os";
|
|
17
22
|
|
|
18
23
|
const HELP = `send-email — send an email via Apple Mail
|
|
@@ -25,9 +30,10 @@ Options:
|
|
|
25
30
|
--bcc <addrs> Comma-separated BCC recipients
|
|
26
31
|
--subject <subj> Email subject line (required)
|
|
27
32
|
--body <text> Plain-text email body (required)
|
|
33
|
+
--draft <path> Draft file — deleted after send, ID appended to drafts/handled
|
|
28
34
|
-h, --help Show this help message and exit
|
|
29
35
|
|
|
30
|
-
Mail.app must be running. No
|
|
36
|
+
Mail.app must be running. No signature or sign-off needed — Apple Mail appends it.`;
|
|
31
37
|
|
|
32
38
|
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
33
39
|
console.log(HELP);
|
|
@@ -59,7 +65,8 @@ function main() {
|
|
|
59
65
|
cc = "",
|
|
60
66
|
bcc = "",
|
|
61
67
|
subject = "",
|
|
62
|
-
body = ""
|
|
68
|
+
body = "",
|
|
69
|
+
draft = "";
|
|
63
70
|
|
|
64
71
|
for (let i = 0; i < args.length; i++) {
|
|
65
72
|
switch (args[i]) {
|
|
@@ -78,6 +85,9 @@ function main() {
|
|
|
78
85
|
case "--body":
|
|
79
86
|
body = args[++i] ?? "";
|
|
80
87
|
break;
|
|
88
|
+
case "--draft":
|
|
89
|
+
draft = args[++i] ?? "";
|
|
90
|
+
break;
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
|
|
@@ -87,6 +97,13 @@ function main() {
|
|
|
87
97
|
process.exit(1);
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
// Strip leading two-space padding from each line and trim overall whitespace
|
|
101
|
+
body = body
|
|
102
|
+
.split("\n")
|
|
103
|
+
.map((line) => line.replace(/^ {2}/, ""))
|
|
104
|
+
.join("\n")
|
|
105
|
+
.trim();
|
|
106
|
+
|
|
90
107
|
const lines = [
|
|
91
108
|
'tell application "Mail"',
|
|
92
109
|
` set newMessage to make new outgoing message with properties {subject:"${escapeAS(subject)}", content:"${escapeAS(body)}", visible:false}`,
|
|
@@ -106,6 +123,24 @@ function main() {
|
|
|
106
123
|
writeFileSync(tmp, lines);
|
|
107
124
|
execFileSync("osascript", [tmp], { stdio: "inherit" });
|
|
108
125
|
console.log(`Sent: ${subject}`);
|
|
126
|
+
|
|
127
|
+
// Clean up draft and mark thread as handled
|
|
128
|
+
if (draft) {
|
|
129
|
+
try {
|
|
130
|
+
unlinkSync(draft);
|
|
131
|
+
console.log(`Removed draft: ${draft}`);
|
|
132
|
+
} catch {
|
|
133
|
+
// ignore if draft already gone
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Extract email ID from draft filename (e.g. "drafts/12345_draft.md" → "12345")
|
|
137
|
+
const draftBasename = basename(draft, ".md");
|
|
138
|
+
const emailId = draftBasename.replace(/_draft$/, "");
|
|
139
|
+
if (emailId) {
|
|
140
|
+
appendFileSync("drafts/handled", emailId + "\n");
|
|
141
|
+
console.log(`Marked as handled: ${emailId}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
109
144
|
} finally {
|
|
110
145
|
try {
|
|
111
146
|
unlinkSync(tmp);
|
|
@@ -63,16 +63,19 @@ When the user asks to prep for a meeting:
|
|
|
63
63
|
|
|
64
64
|
### Step 1: Identify the Meeting
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Use the calendar query script to find upcoming meetings:
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
# Next 2 hours of meetings as JSON
|
|
70
|
+
node .claude/skills/sync-apple-calendar/scripts/query.mjs --upcoming 2h --json
|
|
71
|
+
|
|
72
|
+
# Today's full schedule
|
|
73
|
+
node .claude/skills/sync-apple-calendar/scripts/query.mjs --today
|
|
71
74
|
```
|
|
72
75
|
|
|
73
76
|
If "prep me for my next meeting":
|
|
74
77
|
|
|
75
|
-
-
|
|
78
|
+
- Query upcoming events with `--upcoming 2h`
|
|
76
79
|
- Find the next meeting with external attendees
|
|
77
80
|
- Confirm with user if unclear
|
|
78
81
|
|
|
@@ -51,27 +51,36 @@ Run this skill:
|
|
|
51
51
|
## Before Starting
|
|
52
52
|
|
|
53
53
|
1. Read `USER.md` to get the user's name, email, and domain.
|
|
54
|
-
2.
|
|
54
|
+
2. **Scan for unprocessed sessions** using the scan script:
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
-
|
|
57
|
+
node .claude/skills/process-hyprnote/scripts/scan.mjs
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
This checks all sessions against the `graph_processed` state file and reports
|
|
61
|
+
which need processing, with titles, dates, and content previews.
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
**Options:**
|
|
64
|
+
|
|
65
|
+
| Flag | Description |
|
|
66
|
+
| ----------- | -------------------------------------------------------- |
|
|
67
|
+
| `--changed` | Also detect sessions whose memo/summary hash has changed |
|
|
68
|
+
| `--json` | Output as JSON (for programmatic use) |
|
|
69
|
+
| `--count` | Just print the count (for quick checks) |
|
|
70
|
+
| `--limit N` | Max sessions to display (default: 20) |
|
|
66
71
|
|
|
67
72
|
A session needs processing if:
|
|
68
73
|
|
|
69
74
|
- Its `_memo.md` path is **not** in `graph_processed`, OR
|
|
70
|
-
- Its `_memo.md` hash has changed (
|
|
75
|
+
- Its `_memo.md` hash has changed (use `--changed` to detect this), OR
|
|
71
76
|
- Its `_summary.md` exists and is not in `graph_processed` or has changed
|
|
72
77
|
|
|
73
78
|
**Process all unprocessed sessions in one run** (typically few sessions).
|
|
74
79
|
|
|
80
|
+
**Do NOT write bespoke scripts to scan for unprocessed sessions.** Use this
|
|
81
|
+
script — it handles all edge cases (empty memos, missing summaries, metadata
|
|
82
|
+
fallback).
|
|
83
|
+
|
|
75
84
|
## Step 0: Build Knowledge Index
|
|
76
85
|
|
|
77
86
|
Scan existing notes to avoid duplicates and resolve entities:
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scan for unprocessed Hyprnote sessions.
|
|
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.
|
|
8
|
+
*
|
|
9
|
+
* 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
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
|
|
21
|
+
const HOME = homedir();
|
|
22
|
+
const SESSIONS_DIR = join(
|
|
23
|
+
HOME,
|
|
24
|
+
"Library/Application Support/hyprnote/sessions",
|
|
25
|
+
);
|
|
26
|
+
const STATE_FILE = join(HOME, ".cache/fit/basecamp/state/graph_processed");
|
|
27
|
+
|
|
28
|
+
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
29
|
+
console.log(`scan — find unprocessed Hyprnote sessions
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
node scripts/scan.mjs [options]
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--changed Also detect sessions whose memo/summary hash has changed
|
|
36
|
+
--json Output as JSON array
|
|
37
|
+
--count Just print the unprocessed count (for scripting)
|
|
38
|
+
--limit N Max sessions to display (default: 20)
|
|
39
|
+
-h, --help Show this help message
|
|
40
|
+
|
|
41
|
+
Sessions dir: ~/Library/Application Support/hyprnote/sessions/
|
|
42
|
+
State file: ~/.cache/fit/basecamp/state/graph_processed`);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const args = process.argv.slice(2);
|
|
47
|
+
const detectChanged = args.includes("--changed");
|
|
48
|
+
const jsonOutput = args.includes("--json");
|
|
49
|
+
const countOnly = args.includes("--count");
|
|
50
|
+
const limitIdx = args.indexOf("--limit");
|
|
51
|
+
const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) || 20 : 20;
|
|
52
|
+
|
|
53
|
+
// --- Load state ---
|
|
54
|
+
|
|
55
|
+
const state = new Map();
|
|
56
|
+
if (existsSync(STATE_FILE)) {
|
|
57
|
+
const text = readFileSync(STATE_FILE, "utf8");
|
|
58
|
+
for (const line of text.split("\n")) {
|
|
59
|
+
if (!line) continue;
|
|
60
|
+
const idx = line.indexOf("\t");
|
|
61
|
+
if (idx === -1) continue;
|
|
62
|
+
state.set(line.slice(0, idx), line.slice(idx + 1));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Compute SHA-256 hash of file contents.
|
|
68
|
+
*/
|
|
69
|
+
function fileHash(filePath) {
|
|
70
|
+
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if a file needs processing (new or changed).
|
|
75
|
+
*/
|
|
76
|
+
function needsProcessing(filePath) {
|
|
77
|
+
const storedHash = state.get(filePath);
|
|
78
|
+
if (!storedHash) return { needed: true, reason: "new" };
|
|
79
|
+
if (detectChanged) {
|
|
80
|
+
const currentHash = fileHash(filePath);
|
|
81
|
+
if (currentHash !== storedHash) return { needed: true, reason: "changed" };
|
|
82
|
+
}
|
|
83
|
+
return { needed: false, reason: null };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract title and date from a memo file.
|
|
88
|
+
*/
|
|
89
|
+
function parseMemo(memoPath) {
|
|
90
|
+
try {
|
|
91
|
+
const content = readFileSync(memoPath, "utf8");
|
|
92
|
+
|
|
93
|
+
// Skip empty/whitespace-only memos
|
|
94
|
+
const body = content.replace(/---[\s\S]*?---/, "").trim();
|
|
95
|
+
if (!body || body === " ") return null;
|
|
96
|
+
|
|
97
|
+
// Extract title from first H1
|
|
98
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
99
|
+
const title = titleMatch ? titleMatch[1].trim() : null;
|
|
100
|
+
|
|
101
|
+
// Extract date from content or fall back to file mtime
|
|
102
|
+
const dateMatch = content.match(/\d{4}-\d{2}-\d{2}/);
|
|
103
|
+
let date = dateMatch ? dateMatch[0] : null;
|
|
104
|
+
|
|
105
|
+
if (!date) {
|
|
106
|
+
const stat = statSync(memoPath);
|
|
107
|
+
date = stat.mtime.toISOString().slice(0, 10);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { title, date, preview: body.slice(0, 150).replace(/\n/g, " ") };
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Read _meta.json for session metadata.
|
|
118
|
+
*/
|
|
119
|
+
function readMeta(sessionDir) {
|
|
120
|
+
const metaPath = join(sessionDir, "_meta.json");
|
|
121
|
+
if (!existsSync(metaPath)) return null;
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(readFileSync(metaPath, "utf8"));
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- Scan sessions ---
|
|
130
|
+
|
|
131
|
+
if (!existsSync(SESSIONS_DIR)) {
|
|
132
|
+
console.error(`Hyprnote sessions directory not found: ${SESSIONS_DIR}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sessionIds = readdirSync(SESSIONS_DIR);
|
|
137
|
+
const unprocessed = [];
|
|
138
|
+
let totalWithMemos = 0;
|
|
139
|
+
let processedCount = 0;
|
|
140
|
+
|
|
141
|
+
for (const uuid of sessionIds) {
|
|
142
|
+
const sessionPath = join(SESSIONS_DIR, uuid);
|
|
143
|
+
const stat = statSync(sessionPath, { throwIfNoEntry: false });
|
|
144
|
+
if (!stat || !stat.isDirectory()) continue;
|
|
145
|
+
|
|
146
|
+
const memoPath = join(sessionPath, "_memo.md");
|
|
147
|
+
const summaryPath = join(sessionPath, "_summary.md");
|
|
148
|
+
const hasMemo = existsSync(memoPath);
|
|
149
|
+
const hasSummary = existsSync(summaryPath);
|
|
150
|
+
|
|
151
|
+
if (!hasMemo && !hasSummary) continue;
|
|
152
|
+
|
|
153
|
+
totalWithMemos++;
|
|
154
|
+
|
|
155
|
+
// Check memo
|
|
156
|
+
const memoCheck = hasMemo
|
|
157
|
+
? needsProcessing(memoPath)
|
|
158
|
+
: { needed: false, reason: null };
|
|
159
|
+
|
|
160
|
+
// Check summary
|
|
161
|
+
const summaryCheck = hasSummary
|
|
162
|
+
? needsProcessing(summaryPath)
|
|
163
|
+
: { needed: false, reason: null };
|
|
164
|
+
|
|
165
|
+
if (!memoCheck.needed && !summaryCheck.needed) {
|
|
166
|
+
processedCount++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Parse memo for display info
|
|
171
|
+
const memo = hasMemo ? parseMemo(memoPath) : null;
|
|
172
|
+
if (hasMemo && !memo) {
|
|
173
|
+
// Empty memo, no summary → skip
|
|
174
|
+
if (!hasSummary) continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Read meta for title fallback
|
|
178
|
+
const meta = readMeta(sessionPath);
|
|
179
|
+
|
|
180
|
+
const title = memo?.title || meta?.title || uuid.slice(0, 8);
|
|
181
|
+
const date =
|
|
182
|
+
memo?.date ||
|
|
183
|
+
(meta?.created_at ? meta.created_at.slice(0, 10) : null) ||
|
|
184
|
+
statSync(sessionPath).mtime.toISOString().slice(0, 10);
|
|
185
|
+
|
|
186
|
+
unprocessed.push({
|
|
187
|
+
uuid,
|
|
188
|
+
title,
|
|
189
|
+
date,
|
|
190
|
+
hasMemo,
|
|
191
|
+
hasSummary,
|
|
192
|
+
memoReason: memoCheck.reason,
|
|
193
|
+
summaryReason: summaryCheck.reason,
|
|
194
|
+
preview: memo?.preview || "(summary only)",
|
|
195
|
+
memoPath: hasMemo ? memoPath : null,
|
|
196
|
+
summaryPath: hasSummary ? summaryPath : null,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Sort by date descending (newest first)
|
|
201
|
+
unprocessed.sort((a, b) => b.date.localeCompare(a.date));
|
|
202
|
+
|
|
203
|
+
// --- Output ---
|
|
204
|
+
|
|
205
|
+
if (countOnly) {
|
|
206
|
+
console.log(unprocessed.length);
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (jsonOutput) {
|
|
211
|
+
console.log(JSON.stringify(unprocessed.slice(0, limit), null, 2));
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Formatted output
|
|
216
|
+
console.log(
|
|
217
|
+
`Sessions: ${totalWithMemos} total, ${processedCount} processed, ${unprocessed.length} unprocessed`,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (unprocessed.length === 0) {
|
|
221
|
+
console.log("\nAll sessions are up to date.");
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log("");
|
|
226
|
+
const display = unprocessed.slice(0, limit);
|
|
227
|
+
for (const s of display) {
|
|
228
|
+
const flags = [];
|
|
229
|
+
if (s.memoReason) flags.push(`memo:${s.memoReason}`);
|
|
230
|
+
if (s.summaryReason) flags.push(`summary:${s.summaryReason}`);
|
|
231
|
+
const sources = [];
|
|
232
|
+
if (s.hasMemo) sources.push("memo");
|
|
233
|
+
if (s.hasSummary) sources.push("summary");
|
|
234
|
+
|
|
235
|
+
console.log(
|
|
236
|
+
`${s.date} | ${s.title} | ${sources.join("+")} | ${flags.join(", ")}`,
|
|
237
|
+
);
|
|
238
|
+
console.log(` ${s.uuid}`);
|
|
239
|
+
if (s.preview && s.preview !== "(summary only)") {
|
|
240
|
+
console.log(` ${s.preview.slice(0, 100)}…`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (unprocessed.length > limit) {
|
|
245
|
+
console.log(`\n... and ${unprocessed.length - limit} more`);
|
|
246
|
+
}
|