@forwardimpact/outpost 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +15 -0
- package/bin/fit-outpost.js +3 -0
- package/config/scheduler.json +34 -0
- package/package.json +63 -0
- package/src/agent-runner.js +189 -0
- package/src/index.js +5 -0
- package/src/kb-manager.js +228 -0
- package/src/outpost.js +417 -0
- package/src/scheduler.js +195 -0
- package/src/socket-server.js +336 -0
- package/src/state-manager.js +126 -0
- package/templates/.claude/agents/chief-of-staff.md +58 -0
- package/templates/.claude/agents/concierge.md +46 -0
- package/templates/.claude/agents/head-hunter.md +57 -0
- package/templates/.claude/agents/librarian.md +60 -0
- package/templates/.claude/agents/postman.md +47 -0
- package/templates/.claude/agents/recruiter.md +63 -0
- package/templates/.claude/settings.json +88 -0
- package/templates/.claude/skills/candidate-report/SKILL.md +139 -0
- package/templates/.claude/skills/candidate-report/references/report-template.html +203 -0
- package/templates/.claude/skills/candidate-report/references/report.css +438 -0
- package/templates/.claude/skills/candidate-report/references/rubric.md +76 -0
- package/templates/.claude/skills/candidate-report/scripts/render-pdf.mjs +48 -0
- package/templates/.claude/skills/deck-create/SKILL.md +75 -0
- package/templates/.claude/skills/deck-create/references/slide.css +35 -0
- package/templates/.claude/skills/deck-create/scripts/convert-to-pdf.mjs +47 -0
- package/templates/.claude/skills/deck-summarize/SKILL.md +139 -0
- package/templates/.claude/skills/deck-summarize/references/brief-template.md +85 -0
- package/templates/.claude/skills/deck-summarize/scripts/extract-pptx.mjs +210 -0
- package/templates/.claude/skills/doc-collab/SKILL.md +112 -0
- package/templates/.claude/skills/doc-create/SKILL.md +105 -0
- package/templates/.claude/skills/doc-create/scripts/convert-to-pdf.mjs +50 -0
- package/templates/.claude/skills/draft-emails/SKILL.md +141 -0
- package/templates/.claude/skills/draft-emails/references/template.md +26 -0
- package/templates/.claude/skills/draft-emails/scripts/scan-emails.mjs +66 -0
- package/templates/.claude/skills/draft-emails/scripts/send-email.mjs +139 -0
- package/templates/.claude/skills/extract-entities/SKILL.md +187 -0
- package/templates/.claude/skills/extract-entities/references/TEMPLATES.md +14 -0
- package/templates/.claude/skills/extract-entities/references/conditions.md +55 -0
- package/templates/.claude/skills/extract-entities/references/content.md +104 -0
- package/templates/.claude/skills/extract-entities/references/links.md +48 -0
- package/templates/.claude/skills/extract-entities/references/recruitment.md +57 -0
- package/templates/.claude/skills/extract-entities/references/resolution.md +71 -0
- package/templates/.claude/skills/extract-entities/references/sources.md +70 -0
- package/templates/.claude/skills/extract-entities/references/templates-conditions.md +36 -0
- package/templates/.claude/skills/extract-entities/references/templates-goals-priorities.md +63 -0
- package/templates/.claude/skills/extract-entities/references/templates-people-orgs.md +67 -0
- package/templates/.claude/skills/extract-entities/references/templates-projects-topics.md +68 -0
- package/templates/.claude/skills/extract-entities/scripts/state.mjs +131 -0
- package/templates/.claude/skills/hyprnote-follow/SKILL.md +174 -0
- package/templates/.claude/skills/hyprnote-follow/references/coaching.md +118 -0
- package/templates/.claude/skills/hyprnote-follow/references/debrief.md +59 -0
- package/templates/.claude/skills/hyprnote-follow/references/meeting-types.md +14 -0
- package/templates/.claude/skills/hyprnote-follow/scripts/follow.mjs +243 -0
- package/templates/.claude/skills/hyprnote-process/SKILL.md +147 -0
- package/templates/.claude/skills/hyprnote-process/references/extraction.md +84 -0
- package/templates/.claude/skills/hyprnote-process/references/sessions.md +81 -0
- package/templates/.claude/skills/hyprnote-process/scripts/scan.mjs +246 -0
- package/templates/.claude/skills/hyprnote-trim/SKILL.md +177 -0
- package/templates/.claude/skills/manage-tasks/SKILL.md +152 -0
- package/templates/.claude/skills/manage-tasks/references/format.md +73 -0
- package/templates/.claude/skills/meeting-prep/SKILL.md +180 -0
- package/templates/.claude/skills/organize-files/SKILL.md +146 -0
- package/templates/.claude/skills/organize-files/scripts/organize-by-type.mjs +105 -0
- package/templates/.claude/skills/organize-files/scripts/summarize.mjs +91 -0
- package/templates/.claude/skills/req-assess/SKILL.md +137 -0
- package/templates/.claude/skills/req-assess/references/interview-template.md +62 -0
- package/templates/.claude/skills/req-assess/references/panel-template.md +117 -0
- package/templates/.claude/skills/req-assess/references/rubric.md +65 -0
- package/templates/.claude/skills/req-decide/SKILL.md +163 -0
- package/templates/.claude/skills/req-decide/references/rubric.md +63 -0
- package/templates/.claude/skills/req-decide/references/template.md +118 -0
- package/templates/.claude/skills/req-forget/SKILL.md +154 -0
- package/templates/.claude/skills/req-forget/references/classify.md +23 -0
- package/templates/.claude/skills/req-forget/references/locations.md +59 -0
- package/templates/.claude/skills/req-forget/references/report-template.md +67 -0
- package/templates/.claude/skills/req-scan/SKILL.md +145 -0
- package/templates/.claude/skills/req-scan/references/fallbacks.md +76 -0
- package/templates/.claude/skills/req-scan/references/filters.md +83 -0
- package/templates/.claude/skills/req-scan/references/sources.md +76 -0
- package/templates/.claude/skills/req-scan/references/state.md +54 -0
- package/templates/.claude/skills/req-scan/references/template.md +36 -0
- package/templates/.claude/skills/req-scan/scripts/state.mjs +396 -0
- package/templates/.claude/skills/req-screen/SKILL.md +151 -0
- package/templates/.claude/skills/req-screen/references/rubric.md +94 -0
- package/templates/.claude/skills/req-screen/references/template.md +91 -0
- package/templates/.claude/skills/req-track/SKILL.md +189 -0
- package/templates/.claude/skills/req-track/references/fields.md +114 -0
- package/templates/.claude/skills/req-track/references/signals.md +42 -0
- package/templates/.claude/skills/req-track/references/statuses.md +32 -0
- package/templates/.claude/skills/req-track/references/templates.md +127 -0
- package/templates/.claude/skills/req-workday/SKILL.md +162 -0
- package/templates/.claude/skills/req-workday/references/field-mapping.md +40 -0
- package/templates/.claude/skills/req-workday/references/status-mapping.md +38 -0
- package/templates/.claude/skills/req-workday/references/templates.md +126 -0
- package/templates/.claude/skills/req-workday/references/xlsx-format.md +56 -0
- package/templates/.claude/skills/req-workday/scripts/parse-workday.mjs +313 -0
- package/templates/.claude/skills/send-chat/SKILL.md +170 -0
- package/templates/.claude/skills/sync-apple-calendar/SKILL.md +145 -0
- package/templates/.claude/skills/sync-apple-calendar/references/SCHEMA.md +80 -0
- package/templates/.claude/skills/sync-apple-calendar/scripts/query.mjs +307 -0
- package/templates/.claude/skills/sync-apple-calendar/scripts/sync.mjs +332 -0
- package/templates/.claude/skills/sync-apple-mail/SKILL.md +151 -0
- package/templates/.claude/skills/sync-apple-mail/references/SCHEMA.md +115 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +389 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/sync-helpers.mjs +372 -0
- package/templates/.claude/skills/sync-apple-mail/scripts/sync.mjs +239 -0
- package/templates/.claude/skills/sync-teams/SKILL.md +177 -0
- package/templates/.claude/skills/sync-teams/scripts/idb-reader.mjs +147 -0
- package/templates/.claude/skills/sync-teams/scripts/leveldb-reader.mjs +288 -0
- package/templates/.claude/skills/sync-teams/scripts/sync.mjs +381 -0
- package/templates/.claude/skills/upstream-skill/SKILL.md +130 -0
- package/templates/.claude/skills/upstream-skill/references/examples.md +80 -0
- package/templates/.claude/skills/weekly-update/SKILL.md +141 -0
- package/templates/.claude/skills/weekly-update/references/template.md +55 -0
- package/templates/CLAUDE.md +134 -0
- package/templates/USER.md +3 -0
- package/templates/apm.yml +5 -0
- package/templates/knowledge/Briefings/.gitkeep +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* follow.mjs — Read a live Hyprnote transcript and output new content since
|
|
5
|
+
* the last read. Designed to be called repeatedly during a live session.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node follow.mjs <session-id> # First read — outputs everything
|
|
9
|
+
* node follow.mjs <session-id> --after <word-id> # Outputs words after the given word ID
|
|
10
|
+
* node follow.mjs <session-id> --after <word-id> --summary # Condensed summary mode
|
|
11
|
+
* node follow.mjs <session-id> --meta # Output session metadata only
|
|
12
|
+
* node follow.mjs --detect # Detect the most recently active session
|
|
13
|
+
*
|
|
14
|
+
* Output (JSON):
|
|
15
|
+
* {
|
|
16
|
+
* "session_id": "...",
|
|
17
|
+
* "title": "...",
|
|
18
|
+
* "total_words": 1234,
|
|
19
|
+
* "new_words": 56,
|
|
20
|
+
* "last_word_id": "...",
|
|
21
|
+
* "duration_ms": 123456,
|
|
22
|
+
* "channels": {
|
|
23
|
+
* "0": { "label": "user", "word_count": 600 },
|
|
24
|
+
* "1": { "label": "guest", "word_count": 634 }
|
|
25
|
+
* },
|
|
26
|
+
* "text": [
|
|
27
|
+
* { "channel": 0, "start_ms": 1000, "end_ms": 5000, "text": "Hello, how are you?" },
|
|
28
|
+
* ...
|
|
29
|
+
* ]
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import { homedir } from "node:os";
|
|
36
|
+
|
|
37
|
+
const SESSIONS_DIR = join(
|
|
38
|
+
homedir(),
|
|
39
|
+
"Library/Application Support/hyprnote/sessions",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
function parseArgs() {
|
|
43
|
+
const args = process.argv.slice(2);
|
|
44
|
+
const flags = {};
|
|
45
|
+
let positional = null;
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < args.length; i++) {
|
|
48
|
+
if (args[i] === "--after" && i + 1 < args.length) {
|
|
49
|
+
flags.after = args[++i];
|
|
50
|
+
} else if (args[i] === "--summary") {
|
|
51
|
+
flags.summary = true;
|
|
52
|
+
} else if (args[i] === "--meta") {
|
|
53
|
+
flags.meta = true;
|
|
54
|
+
} else if (args[i] === "--detect") {
|
|
55
|
+
flags.detect = true;
|
|
56
|
+
} else if (!args[i].startsWith("--")) {
|
|
57
|
+
positional = args[i];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { sessionId: positional, ...flags };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function detectActiveSession() {
|
|
65
|
+
const entries = readdirSync(SESSIONS_DIR);
|
|
66
|
+
let best = null;
|
|
67
|
+
let bestMtime = 0;
|
|
68
|
+
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const transcriptPath = join(SESSIONS_DIR, entry, "transcript.json");
|
|
71
|
+
try {
|
|
72
|
+
const stat = statSync(transcriptPath);
|
|
73
|
+
if (stat.mtimeMs > bestMtime) {
|
|
74
|
+
bestMtime = stat.mtimeMs;
|
|
75
|
+
best = entry;
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// No transcript — skip
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!best) {
|
|
83
|
+
console.error("No active Hyprnote session found.");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check if the transcript was modified recently (within last 5 minutes)
|
|
88
|
+
const ageMs = Date.now() - bestMtime;
|
|
89
|
+
const isLive = ageMs < 5 * 60 * 1000;
|
|
90
|
+
|
|
91
|
+
const metaPath = join(SESSIONS_DIR, best, "_meta.json");
|
|
92
|
+
let title = "Unknown";
|
|
93
|
+
try {
|
|
94
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
95
|
+
title = meta.title || "Untitled";
|
|
96
|
+
} catch {
|
|
97
|
+
// No meta
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
session_id: best,
|
|
102
|
+
title,
|
|
103
|
+
is_live: isLive,
|
|
104
|
+
last_modified: new Date(bestMtime).toISOString(),
|
|
105
|
+
age_seconds: Math.round(ageMs / 1000),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readMeta(sessionId) {
|
|
110
|
+
const metaPath = join(SESSIONS_DIR, sessionId, "_meta.json");
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
113
|
+
} catch {
|
|
114
|
+
return { title: "Unknown", created_at: null, participants: [] };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readTranscript(sessionId) {
|
|
119
|
+
const path = join(SESSIONS_DIR, sessionId, "transcript.json");
|
|
120
|
+
try {
|
|
121
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
122
|
+
return data.transcripts?.[0]?.words || [];
|
|
123
|
+
} catch {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function groupIntoSegments(words) {
|
|
129
|
+
if (words.length === 0) return [];
|
|
130
|
+
|
|
131
|
+
const segments = [];
|
|
132
|
+
let current = {
|
|
133
|
+
channel: words[0].channel,
|
|
134
|
+
start_ms: words[0].start_ms,
|
|
135
|
+
end_ms: words[0].end_ms,
|
|
136
|
+
texts: [words[0].text],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
for (let i = 1; i < words.length; i++) {
|
|
140
|
+
const w = words[i];
|
|
141
|
+
const gap = w.start_ms - current.end_ms;
|
|
142
|
+
|
|
143
|
+
// New segment on channel change or >3s gap
|
|
144
|
+
if (w.channel !== current.channel || gap > 3000) {
|
|
145
|
+
segments.push({
|
|
146
|
+
channel: current.channel,
|
|
147
|
+
start_ms: current.start_ms,
|
|
148
|
+
end_ms: current.end_ms,
|
|
149
|
+
text: current.texts.join("").trim(),
|
|
150
|
+
});
|
|
151
|
+
current = {
|
|
152
|
+
channel: w.channel,
|
|
153
|
+
start_ms: w.start_ms,
|
|
154
|
+
end_ms: w.end_ms,
|
|
155
|
+
texts: [w.text],
|
|
156
|
+
};
|
|
157
|
+
} else {
|
|
158
|
+
current.end_ms = w.end_ms;
|
|
159
|
+
current.texts.push(w.text);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Push final segment
|
|
164
|
+
segments.push({
|
|
165
|
+
channel: current.channel,
|
|
166
|
+
start_ms: current.start_ms,
|
|
167
|
+
end_ms: current.end_ms,
|
|
168
|
+
text: current.texts.join("").trim(),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return segments.filter((s) => s.text.length > 0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildChannelStats(allWords) {
|
|
175
|
+
const channels = {};
|
|
176
|
+
for (const w of allWords) {
|
|
177
|
+
if (!channels[w.channel]) {
|
|
178
|
+
channels[w.channel] = {
|
|
179
|
+
label: w.channel === 0 ? "user" : `guest-${w.channel}`,
|
|
180
|
+
word_count: 0,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
channels[w.channel].word_count++;
|
|
184
|
+
}
|
|
185
|
+
return channels;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function filterNewWords(allWords, afterId) {
|
|
189
|
+
if (!afterId) return allWords;
|
|
190
|
+
const idx = allWords.findIndex((w) => w.id === afterId);
|
|
191
|
+
// If word ID not found, return everything (safety fallback)
|
|
192
|
+
return idx >= 0 ? allWords.slice(idx + 1) : allWords;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function main() {
|
|
196
|
+
const opts = parseArgs();
|
|
197
|
+
|
|
198
|
+
// Detect mode
|
|
199
|
+
if (opts.detect) {
|
|
200
|
+
console.log(JSON.stringify(detectActiveSession(), null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!opts.sessionId) {
|
|
205
|
+
console.error(
|
|
206
|
+
"Usage: follow.mjs <session-id> [--after <word-id>] [--meta]",
|
|
207
|
+
);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const meta = readMeta(opts.sessionId);
|
|
212
|
+
|
|
213
|
+
// Meta-only mode
|
|
214
|
+
if (opts.meta) {
|
|
215
|
+
console.log(JSON.stringify(meta, null, 2));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Read transcript
|
|
220
|
+
const allWords = readTranscript(opts.sessionId);
|
|
221
|
+
const words = filterNewWords(allWords, opts.after);
|
|
222
|
+
const lastWord = allWords[allWords.length - 1];
|
|
223
|
+
|
|
224
|
+
console.log(
|
|
225
|
+
JSON.stringify(
|
|
226
|
+
{
|
|
227
|
+
session_id: opts.sessionId,
|
|
228
|
+
title: meta.title,
|
|
229
|
+
created_at: meta.created_at,
|
|
230
|
+
total_words: allWords.length,
|
|
231
|
+
new_words: words.length,
|
|
232
|
+
last_word_id: lastWord?.id || null,
|
|
233
|
+
duration_ms: lastWord?.end_ms || 0,
|
|
234
|
+
channels: buildChannelStats(allWords),
|
|
235
|
+
text: groupIntoSegments(words),
|
|
236
|
+
},
|
|
237
|
+
null,
|
|
238
|
+
2,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
main();
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hyprnote-process
|
|
3
|
+
description: Process Hyprnote meeting sessions (memos, summaries, transcripts) into the knowledge graph. Extracts people, organizations, projects, and topics from AI-generated meeting summaries and user notes, creating or updating Obsidian-compatible notes in knowledge/. Use when the user asks to process meeting notes or after Hyprnote sessions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Process Hyprnote
|
|
7
|
+
|
|
8
|
+
Process meeting sessions from Hyprnote (a local AI meeting-notes app) into the
|
|
9
|
+
knowledge graph. Hyprnote records meetings, transcribes them, and generates AI
|
|
10
|
+
summaries; this skill reads that output and feeds it into `knowledge/` — the
|
|
11
|
+
same way `extract-entities` processes emails and calendar events.
|
|
12
|
+
|
|
13
|
+
## Trigger
|
|
14
|
+
|
|
15
|
+
- The user asks to process meeting notes or Hyprnote sessions.
|
|
16
|
+
- New meetings have been recorded.
|
|
17
|
+
- The user asks to update the knowledge base from recent meetings.
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
- Hyprnote installed; sessions at
|
|
22
|
+
`~/Library/Application Support/hyprnote/sessions/`.
|
|
23
|
+
- User identity in `USER.md`.
|
|
24
|
+
|
|
25
|
+
## Inputs
|
|
26
|
+
|
|
27
|
+
- `~/Library/Application Support/hyprnote/sessions/{uuid}/` — see
|
|
28
|
+
[references/sessions.md](references/sessions.md) for the file shape and skip
|
|
29
|
+
rules.
|
|
30
|
+
- `~/.cache/fit/outpost/state/graph_processed` — processed-file index (TSV,
|
|
31
|
+
shared with `extract-entities`).
|
|
32
|
+
- `USER.md` — user identity for self-exclusion.
|
|
33
|
+
|
|
34
|
+
## Outputs
|
|
35
|
+
|
|
36
|
+
- `knowledge/People/`, `knowledge/Organizations/`, `knowledge/Projects/`,
|
|
37
|
+
`knowledge/Topics/` — created or updated.
|
|
38
|
+
- `knowledge/Goals/`, `knowledge/Priorities/` — **updated only**, never
|
|
39
|
+
auto-created.
|
|
40
|
+
- `~/.cache/fit/outpost/state/graph_processed` — updated.
|
|
41
|
+
|
|
42
|
+
<do_confirm_checklist goal="Verify each session was processed correctly">
|
|
43
|
+
|
|
44
|
+
- [ ] Empty / test / onboarding sessions skipped (per skip rules).
|
|
45
|
+
- [ ] Both `_memo.md` and `_summary.md` read (when present); transcript
|
|
46
|
+
consulted only for disambiguation.
|
|
47
|
+
- [ ] "Would I prep?" test applied to each person; self excluded.
|
|
48
|
+
- [ ] Interview sessions wrote to `knowledge/Candidates/`, not
|
|
49
|
+
`knowledge/People/`.
|
|
50
|
+
- [ ] All links use absolute paths `[[Folder/Name]]`.
|
|
51
|
+
- [ ] Activity entries describe relationship, not communication method.
|
|
52
|
+
- [ ] No new `Goals/` or `Priorities/` auto-created (user-set only); any
|
|
53
|
+
referenced goal had its progress updated.
|
|
54
|
+
- [ ] `graph_processed` updated for every processed file (memo + summary).
|
|
55
|
+
|
|
56
|
+
</do_confirm_checklist>
|
|
57
|
+
|
|
58
|
+
## Procedure
|
|
59
|
+
|
|
60
|
+
### 0. Set up
|
|
61
|
+
|
|
62
|
+
Read `USER.md`. Scan unprocessed sessions:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
node .claude/skills/hyprnote-process/scripts/scan.mjs
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Flags: `--changed` (also detect changed memo/summary hashes), `--json`
|
|
69
|
+
(programmatic output), `--count` (count only), `--limit N` (default 20).
|
|
70
|
+
|
|
71
|
+
A session needs processing when its `_memo.md` is not in `graph_processed`, or
|
|
72
|
+
its hash has changed (`--changed`), or its `_summary.md` exists and is not in
|
|
73
|
+
`graph_processed` (or has changed).
|
|
74
|
+
|
|
75
|
+
Process all unprocessed sessions in one run. **Don't write bespoke scan
|
|
76
|
+
scripts** — this script handles the edge cases (empty memos, missing summaries,
|
|
77
|
+
metadata fallback).
|
|
78
|
+
|
|
79
|
+
### 1. Build the knowledge index
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
ls knowledge/People/ knowledge/Organizations/ knowledge/Projects/ \
|
|
83
|
+
knowledge/Topics/ knowledge/Goals/ knowledge/Priorities/ \
|
|
84
|
+
knowledge/Conditions/ 2>/dev/null
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Read each note's header to build a mental index of known entities (same approach
|
|
88
|
+
as `extract-entities` Step 0).
|
|
89
|
+
|
|
90
|
+
### 2. Read each session
|
|
91
|
+
|
|
92
|
+
For each unprocessed session, read in this order: `_meta.json`, `_memo.md`,
|
|
93
|
+
`_summary.md` (if present), `transcript.json` (only when disambiguation requires
|
|
94
|
+
it). File shapes and skip rules:
|
|
95
|
+
[references/sessions.md](references/sessions.md).
|
|
96
|
+
|
|
97
|
+
### 3. Classify the source
|
|
98
|
+
|
|
99
|
+
Hyprnote sessions are **meetings** and follow the meeting rules from
|
|
100
|
+
`extract-entities`:
|
|
101
|
+
|
|
102
|
+
- **Can create** People, Organization, Project, and Topic notes.
|
|
103
|
+
- **Can update** existing notes — including Goals and Priorities, which are
|
|
104
|
+
user-set and never auto-created.
|
|
105
|
+
- **Can detect** state changes.
|
|
106
|
+
|
|
107
|
+
Apply the "Would I prep for this person?" test from `extract-entities` Step 5
|
|
108
|
+
before creating a person note.
|
|
109
|
+
|
|
110
|
+
### 4. Extract entities and content
|
|
111
|
+
|
|
112
|
+
Combine memo and summary content (prefer summary when both exist). Extraction
|
|
113
|
+
signals — entity types, decisions, commitments, key facts, activity-line format,
|
|
114
|
+
interview-note rules, and linking rules — live in
|
|
115
|
+
[references/extraction.md](references/extraction.md).
|
|
116
|
+
|
|
117
|
+
### 5. Write updates
|
|
118
|
+
|
|
119
|
+
For **new** entities, use the templates in
|
|
120
|
+
`.claude/skills/extract-entities/references/TEMPLATES.md`. For interview
|
|
121
|
+
sessions, use the candidate brief template from `req-track` (under
|
|
122
|
+
`knowledge/Candidates/`).
|
|
123
|
+
|
|
124
|
+
For **existing** entities, apply targeted edits — never rewrite the file:
|
|
125
|
+
|
|
126
|
+
- Add the new activity entry at the **top** of `## Activity`.
|
|
127
|
+
- Update `Last seen` / `Last activity`.
|
|
128
|
+
- Add new key facts (skip duplicates).
|
|
129
|
+
- Update open items (mark completed, add new).
|
|
130
|
+
- Apply state changes.
|
|
131
|
+
|
|
132
|
+
Verify bidirectional links per `extract-entities` Step 10 (Goal ↔ Project, Goal
|
|
133
|
+
↔ Priority, Project ↔ Priority).
|
|
134
|
+
|
|
135
|
+
### 6. Update graph state
|
|
136
|
+
|
|
137
|
+
For each processed session:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
node .claude/skills/extract-entities/scripts/state.mjs update \
|
|
141
|
+
"$HOME/Library/Application Support/hyprnote/sessions/{uuid}/_memo.md"
|
|
142
|
+
|
|
143
|
+
node .claude/skills/extract-entities/scripts/state.mjs update \
|
|
144
|
+
"$HOME/Library/Application Support/hyprnote/sessions/{uuid}/_summary.md"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
(Skip the summary call if `_summary.md` doesn't exist.)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Entity Extraction Signals
|
|
2
|
+
|
|
3
|
+
Reference for `hyprnote-process` Steps 3 and 4. Combine `_memo.md` and
|
|
4
|
+
`_summary.md` (prefer the summary when both exist).
|
|
5
|
+
|
|
6
|
+
## People
|
|
7
|
+
|
|
8
|
+
Look for names in:
|
|
9
|
+
|
|
10
|
+
- Memo text ("chat with Sarah Chen", "interview with David Kim").
|
|
11
|
+
- Summary bullets ("the user will serve as the senior engineer", "Alex from the
|
|
12
|
+
platform team").
|
|
13
|
+
- `_meta.json` participants.
|
|
14
|
+
|
|
15
|
+
For each: resolve against the knowledge index (Step 0); extract role,
|
|
16
|
+
organization, relationship to the user; note what was discussed.
|
|
17
|
+
|
|
18
|
+
## Organizations
|
|
19
|
+
|
|
20
|
+
Explicit mentions ("Acme Corp"), or inferred from people's roles or the
|
|
21
|
+
surrounding context.
|
|
22
|
+
|
|
23
|
+
## Projects
|
|
24
|
+
|
|
25
|
+
Explicit project names ("Customer Portal", "Q2 Migration") or described
|
|
26
|
+
initiatives ("the hiring pipeline", "the product launch").
|
|
27
|
+
|
|
28
|
+
## Topics
|
|
29
|
+
|
|
30
|
+
Recurring themes ("AI coding agents", "interview process", "architecture
|
|
31
|
+
decisions"). Only create a Topic note when the subject spans multiple meetings
|
|
32
|
+
or is strategically important.
|
|
33
|
+
|
|
34
|
+
## Self-exclusion
|
|
35
|
+
|
|
36
|
+
Never create or update a note for the user — match against name, email, or
|
|
37
|
+
`@domain` from `USER.md`.
|
|
38
|
+
|
|
39
|
+
## Interview sessions (special case)
|
|
40
|
+
|
|
41
|
+
If the title or memo indicates "interview with {Name}", the interviewee is a
|
|
42
|
+
**candidate** — create or update their note in `knowledge/Candidates/` (using
|
|
43
|
+
the candidate brief template from `req-track`), **not** in `knowledge/People/`.
|
|
44
|
+
|
|
45
|
+
## Content signals
|
|
46
|
+
|
|
47
|
+
### Decisions
|
|
48
|
+
|
|
49
|
+
"decided", "agreed", "plan to", "established", "will serve as".
|
|
50
|
+
|
|
51
|
+
### Commitments / action items
|
|
52
|
+
|
|
53
|
+
"will share", "plans to", "needs to", "to be created", "will upload". Extract
|
|
54
|
+
owner, action, deadline (if any), status (default `open`).
|
|
55
|
+
|
|
56
|
+
### Key facts
|
|
57
|
+
|
|
58
|
+
Specific numbers (headcount, budget, timeline), preferences ("non-traditional
|
|
59
|
+
backgrounds"), process details (interview stages, evaluation criteria),
|
|
60
|
+
strategic context (market trends, competitive landscape). Skip filler.
|
|
61
|
+
|
|
62
|
+
### Activity summary
|
|
63
|
+
|
|
64
|
+
One line per session per entity:
|
|
65
|
+
|
|
66
|
+
```markdown
|
|
67
|
+
- **2026-02-14** (meeting): Discussed hiring pipeline. 11 internal
|
|
68
|
+
candidates, plan to shortlist to 6-7. [[People/Sarah Chen]] managing
|
|
69
|
+
the team.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Interview notes (for candidates)
|
|
73
|
+
|
|
74
|
+
Add to the candidate's `## Notes` section: impressions, technical assessment,
|
|
75
|
+
strengths and concerns, any interview scoring or decisions.
|
|
76
|
+
|
|
77
|
+
## Linking rules
|
|
78
|
+
|
|
79
|
+
Use absolute paths everywhere: `[[People/Name]]`, `[[Organizations/Name]]`,
|
|
80
|
+
`[[Projects/Name]]`, `[[Goals/Goal Name]]`, `[[Priorities/Priority Name]]`.
|
|
81
|
+
|
|
82
|
+
When meeting content references an existing Goal or Priority, follow the linking
|
|
83
|
+
rules in `extract-entities` Step 7c — update progress and add backlinks, but
|
|
84
|
+
**never** auto-create Goal or Priority notes.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Hyprnote Session Files
|
|
2
|
+
|
|
3
|
+
Reference for `hyprnote-process` Step 1. Each session lives at
|
|
4
|
+
`~/Library/Application Support/hyprnote/sessions/{uuid}/`.
|
|
5
|
+
|
|
6
|
+
## `_meta.json`
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"created_at": "2026-02-16T13:01:59.187Z",
|
|
11
|
+
"id": "7888363f-4cc6-4987-8470-92f386e5bdfc",
|
|
12
|
+
"participants": [],
|
|
13
|
+
"title": "Director-Level Hiring Pipeline",
|
|
14
|
+
"user_id": "00000000-0000-0000-0000-000000000000"
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Use: session date (from `created_at`), title, participants (often empty —
|
|
19
|
+
Hyprnote doesn't reliably populate this).
|
|
20
|
+
|
|
21
|
+
## `_memo.md`
|
|
22
|
+
|
|
23
|
+
YAML frontmatter (`id`, `session_id`) plus the user's markdown notes:
|
|
24
|
+
|
|
25
|
+
```markdown
|
|
26
|
+
---
|
|
27
|
+
id: 213e0f78-a66a-468d-b8e5-bc3fbbe04bf4
|
|
28
|
+
session_id: 213e0f78-a66a-468d-b8e5-bc3fbbe04bf4
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
Chat with Sarah about the product roadmap.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
High-signal — every name and observation is intentional.
|
|
35
|
+
|
|
36
|
+
## `_summary.md` (optional)
|
|
37
|
+
|
|
38
|
+
YAML frontmatter (`id`, `position`, `session_id`, `title`) plus an AI-generated
|
|
39
|
+
summary — typically the richest source:
|
|
40
|
+
|
|
41
|
+
```markdown
|
|
42
|
+
---
|
|
43
|
+
id: 152d9bc9-0cdc-4fb2-9916-cb7670f3a6df
|
|
44
|
+
position: 1
|
|
45
|
+
session_id: 213e0f78-a66a-468d-b8e5-bc3fbbe04bf4
|
|
46
|
+
title: Summary
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
# Product Roadmap Review
|
|
50
|
+
|
|
51
|
+
- Both speakers reviewed the Q2 roadmap priorities...
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## `transcript.json` (disambiguation only)
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"transcripts": [{
|
|
59
|
+
"words": [
|
|
60
|
+
{"channel": 0, "text": "Hello", "start_ms": 0, "end_ms": 500},
|
|
61
|
+
{"channel": 1, "text": "Hi", "start_ms": 600, "end_ms": 900}
|
|
62
|
+
]
|
|
63
|
+
}]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Do not extract entities from the full transcript** — too noisy. Consult it
|
|
68
|
+
only to disambiguate a name from the memo or summary, confirm who said what
|
|
69
|
+
(channel 0 = user, channel 1 = other speaker), or find context around a specific
|
|
70
|
+
topic or decision.
|
|
71
|
+
|
|
72
|
+
## Skip rules
|
|
73
|
+
|
|
74
|
+
A session is skipped when **all** of:
|
|
75
|
+
|
|
76
|
+
- `_memo.md` body is empty or only ` ` / whitespace.
|
|
77
|
+
- No `_summary.md` exists.
|
|
78
|
+
- Title is empty or generic ("Hello", "Welcome to Hyprnote", "Test").
|
|
79
|
+
|
|
80
|
+
Process a session if it has **either** a substantive memo **or** a
|
|
81
|
+
`_summary.md`.
|