@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,246 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
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/outpost/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/outpost/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
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hyprnote-trim
|
|
3
|
+
description: Trim a Hyprnote transcript to its logical end. Recordings are often left running after a meeting finishes — this skill finds the natural conclusion (goodbyes, sign-offs) and cuts the transcript there. Use when the user asks to trim, cut, or clean up a Hyprnote transcript.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Trim Transcript
|
|
7
|
+
|
|
8
|
+
Find the logical end of a Hyprnote meeting transcript and trim everything after
|
|
9
|
+
it. Meetings recorded with Hyprnote often have trailing noise — the mic stays on
|
|
10
|
+
after goodbyes, capturing ambient sound, unrelated chatter, or silence. This
|
|
11
|
+
skill identifies the natural conclusion and edits the transcript in place.
|
|
12
|
+
|
|
13
|
+
## Trigger
|
|
14
|
+
|
|
15
|
+
Run this skill:
|
|
16
|
+
|
|
17
|
+
- When the user asks to trim, cut, or clean up a Hyprnote transcript
|
|
18
|
+
- When given a specific session ID to trim
|
|
19
|
+
- When another skill (e.g., hyprnote-process) flags a transcript as having
|
|
20
|
+
excessive trailing content
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
- Hyprnote installed with session data at
|
|
25
|
+
`~/Library/Application Support/hyprnote/sessions/`
|
|
26
|
+
|
|
27
|
+
## Inputs
|
|
28
|
+
|
|
29
|
+
- **Session ID** — a UUID identifying the Hyprnote session to trim
|
|
30
|
+
- `~/Library/Application Support/hyprnote/sessions/{uuid}/transcript.json` — the
|
|
31
|
+
word-level transcript
|
|
32
|
+
|
|
33
|
+
## Outputs
|
|
34
|
+
|
|
35
|
+
- `~/Library/Application Support/hyprnote/sessions/{uuid}/transcript.json` —
|
|
36
|
+
edited in place with words after the logical end removed
|
|
37
|
+
- `~/Library/Application Support/hyprnote/sessions/{uuid}/audio.mp3` — deleted.
|
|
38
|
+
Trimming indicates the recording captured audio beyond the consented meeting,
|
|
39
|
+
so the full audio must be removed to respect participant privacy.
|
|
40
|
+
- Printed summary: original duration, trim point, new duration, words removed
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Steps
|
|
45
|
+
|
|
46
|
+
### Step 0 — Validate the session
|
|
47
|
+
|
|
48
|
+
1. Confirm the session directory exists:
|
|
49
|
+
```
|
|
50
|
+
~/Library/Application Support/hyprnote/sessions/{uuid}/
|
|
51
|
+
```
|
|
52
|
+
2. Confirm `transcript.json` exists and has at least one transcript with words.
|
|
53
|
+
3. Read `_meta.json` to get the session title for context.
|
|
54
|
+
|
|
55
|
+
### Step 1 — Reconstruct readable text
|
|
56
|
+
|
|
57
|
+
Convert the word-level transcript into readable text with timestamps. Group
|
|
58
|
+
words into lines by speaker channel and approximate sentence boundaries. The
|
|
59
|
+
goal is a human-readable view you can analyze for the logical end.
|
|
60
|
+
|
|
61
|
+
Use this approach:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
#!/usr/bin/env bun
|
|
65
|
+
import json
|
|
66
|
+
|
|
67
|
+
data = json.load(open('transcript.json'))
|
|
68
|
+
words = data['transcripts'][0]['words']
|
|
69
|
+
|
|
70
|
+
# Reconstruct text grouped by ~30-second windows with channel labels
|
|
71
|
+
current_min = -1
|
|
72
|
+
for i, w in enumerate(words):
|
|
73
|
+
minute_mark = int(w['start_ms'] / 30000) # 30-second buckets
|
|
74
|
+
if minute_mark != current_min:
|
|
75
|
+
current_min = minute_mark
|
|
76
|
+
mins = w['start_ms'] / 60000
|
|
77
|
+
print(f'\n[{mins:.1f}m ch{w["channel"]}]', end='')
|
|
78
|
+
print(w['text'], end='')
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Step 2 — Identify the logical end
|
|
82
|
+
|
|
83
|
+
Read through the reconstructed text and find the **first point where the meeting
|
|
84
|
+
has clearly concluded**. Look for these signals, roughly in order of strength:
|
|
85
|
+
|
|
86
|
+
**Strong ending signals (any one is sufficient):**
|
|
87
|
+
|
|
88
|
+
- Explicit farewells: "bye", "bye bye", "goodbye", "take care", "have a good
|
|
89
|
+
day/evening/weekend", "cheers"
|
|
90
|
+
- Final thank-yous followed by no substantive content: "thank you so much",
|
|
91
|
+
"thanks a lot", "thanks everyone"
|
|
92
|
+
- Meeting close phrases: "that's all", "we're done", "let's wrap up", "I'll let
|
|
93
|
+
you go"
|
|
94
|
+
|
|
95
|
+
**Supporting signals (strengthen the case but not sufficient alone):**
|
|
96
|
+
|
|
97
|
+
- Long silence gaps (>30 seconds) after a farewell exchange
|
|
98
|
+
- Channel drops — only one speaker remains after goodbyes
|
|
99
|
+
- Shift to clearly unrelated content (ambient noise transcribed as fragments)
|
|
100
|
+
- Filler-only content: repeated "um", "uh", fragments with no meaning
|
|
101
|
+
|
|
102
|
+
**The trim point** is the end of the last meaningful farewell exchange. Include
|
|
103
|
+
the final "bye" / "thank you" / "take care" from both parties if present, then
|
|
104
|
+
cut everything after.
|
|
105
|
+
|
|
106
|
+
### Step 3 — Confirm with the user
|
|
107
|
+
|
|
108
|
+
Before modifying the file, show the user:
|
|
109
|
+
|
|
110
|
+
1. The **session title** and **original duration**
|
|
111
|
+
2. The **last ~20 words before the proposed trim point** (as readable text)
|
|
112
|
+
3. The **first ~20 words after the proposed trim point** (what will be removed)
|
|
113
|
+
4. The **new duration** and **number of words being removed**
|
|
114
|
+
|
|
115
|
+
Wait for the user to approve before proceeding.
|
|
116
|
+
|
|
117
|
+
### Step 4 — Trim the transcript
|
|
118
|
+
|
|
119
|
+
Once approved:
|
|
120
|
+
|
|
121
|
+
1. Read the current `transcript.json` (fresh read, not cached).
|
|
122
|
+
2. Slice the words array at the identified index.
|
|
123
|
+
3. Write the modified JSON back to `transcript.json`.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import json
|
|
127
|
+
|
|
128
|
+
path = f'~/Library/Application Support/hyprnote/sessions/{uuid}/transcript.json'
|
|
129
|
+
data = json.load(open(path))
|
|
130
|
+
data['transcripts'][0]['words'] = data['transcripts'][0]['words'][:trim_index]
|
|
131
|
+
json.dump(data, open(path, 'w'), indent=2)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
4. Print a summary:
|
|
135
|
+
```
|
|
136
|
+
Trimmed: {title}
|
|
137
|
+
Before: {original_words} words, {original_duration}
|
|
138
|
+
After: {new_words} words, {new_duration}
|
|
139
|
+
Removed: {removed_words} words ({removed_duration} of trailing content)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Step 5 — Delete the audio recording
|
|
143
|
+
|
|
144
|
+
The fact that a transcript needs trimming means the recording captured audio
|
|
145
|
+
beyond the consented meeting — ambient conversation, unrelated chatter, or other
|
|
146
|
+
people who did not consent to being recorded. The full audio file must be
|
|
147
|
+
deleted to respect participant privacy.
|
|
148
|
+
|
|
149
|
+
1. Delete the audio file:
|
|
150
|
+
```bash
|
|
151
|
+
rm "~/Library/Application Support/hyprnote/sessions/{uuid}/audio.mp3"
|
|
152
|
+
```
|
|
153
|
+
2. Confirm deletion and inform the user:
|
|
154
|
+
```
|
|
155
|
+
Audio deleted: audio.mp3 removed (recording contained unconsented content beyond the meeting)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This step is **not optional** and does **not require separate user
|
|
159
|
+
confirmation** — the user already approved the trim, which implicitly
|
|
160
|
+
acknowledges the recording went beyond the meeting boundary.
|
|
161
|
+
|
|
162
|
+
### Step 6 — Verify
|
|
163
|
+
|
|
164
|
+
Read back the last 10 words of the trimmed transcript to confirm the file was
|
|
165
|
+
written correctly and ends at the expected point. Confirm `audio.mp3` no longer
|
|
166
|
+
exists in the session directory.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Quality checklist
|
|
171
|
+
|
|
172
|
+
- [ ] Session ID validated and transcript exists
|
|
173
|
+
- [ ] Logical end identified based on farewell/closing signals
|
|
174
|
+
- [ ] Trim point shown to user and approved before modification
|
|
175
|
+
- [ ] Transcript file written correctly with valid JSON
|
|
176
|
+
- [ ] Audio recording deleted (contains unconsented content beyond meeting)
|
|
177
|
+
- [ ] Post-trim verification confirms expected ending and audio removed
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: manage-tasks
|
|
3
|
+
description: Create, update, list, and close tasks on per-person task boards in knowledge/Tasks/. Manages task lifecycle, extracts action items from other skills, and keeps boards current. Use when the user asks to add, update, or review tasks, or when chained from extract-entities or hyprnote-process.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Manage Tasks
|
|
7
|
+
|
|
8
|
+
Manage per-person task boards in `knowledge/Tasks/`. Each person has a single
|
|
9
|
+
living document tracking open, in-progress, blocked, and recently completed
|
|
10
|
+
tasks. Task boards are the **canonical source** for task tracking — other notes
|
|
11
|
+
link to them rather than duplicating.
|
|
12
|
+
|
|
13
|
+
## Trigger
|
|
14
|
+
|
|
15
|
+
- The user asks to add, update, close, or list tasks.
|
|
16
|
+
- Chained from `extract-entities` or `hyprnote-process` with extracted action
|
|
17
|
+
items.
|
|
18
|
+
- The user asks to see someone's task board or workload.
|
|
19
|
+
- Scheduled housekeeping (prune done items, flag overdue).
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- `knowledge/Tasks/` directory exists.
|
|
24
|
+
- User identity configured in `USER.md`.
|
|
25
|
+
|
|
26
|
+
## Inputs
|
|
27
|
+
|
|
28
|
+
User-initiated: person name and task details from the request.
|
|
29
|
+
|
|
30
|
+
Chained from other skills, action items in this shape:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
TASKS:
|
|
34
|
+
- Owner: {Full Name}
|
|
35
|
+
Task: {Description — starts with a verb}
|
|
36
|
+
Priority: high|medium|low
|
|
37
|
+
Due: YYYY-MM-DD (if known)
|
|
38
|
+
Source: meeting|email
|
|
39
|
+
Source date: YYYY-MM-DD
|
|
40
|
+
Project: {Project Name} (if applicable)
|
|
41
|
+
Context: {brief context}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Outputs
|
|
45
|
+
|
|
46
|
+
- `knowledge/Tasks/{Person Name}.md` — created or updated.
|
|
47
|
+
|
|
48
|
+
<do_confirm_checklist goal="Verify task board changes are clean and consistent">
|
|
49
|
+
|
|
50
|
+
- [ ] Task title is actionable and starts with a verb.
|
|
51
|
+
- [ ] Priority is set; medium-default omitted.
|
|
52
|
+
- [ ] Due date present only when there's a real deadline.
|
|
53
|
+
- [ ] Project link `[[Projects/Name]]` set when applicable.
|
|
54
|
+
- [ ] No duplicate tasks on the board.
|
|
55
|
+
- [ ] All backlinks use absolute paths `[[Folder/Name]]`.
|
|
56
|
+
- [ ] Context line is concise (1–2 lines max).
|
|
57
|
+
- [ ] New boards include all four sections.
|
|
58
|
+
- [ ] Housekeeping pruned `## Recently Done` to the last 14 days.
|
|
59
|
+
|
|
60
|
+
</do_confirm_checklist>
|
|
61
|
+
|
|
62
|
+
Board layout, entry format, conventions, and useful queries:
|
|
63
|
+
[references/format.md](references/format.md).
|
|
64
|
+
|
|
65
|
+
## Procedure
|
|
66
|
+
|
|
67
|
+
### 1. Resolve the person
|
|
68
|
+
|
|
69
|
+
Read `USER.md`, then resolve the target name to its canonical form:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
ls knowledge/Tasks/
|
|
73
|
+
rg "{name}" knowledge/People/
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If no board exists yet, create one (Step 4).
|
|
77
|
+
|
|
78
|
+
### 2. Read the current board
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cat "knowledge/Tasks/{Person Name}.md"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Parse the existing entries to: avoid duplicates, understand current workload,
|
|
85
|
+
and find the right insertion point.
|
|
86
|
+
|
|
87
|
+
### 3. Perform the operation
|
|
88
|
+
|
|
89
|
+
Pick **add**, **update**, **close**, **list**, or **housekeeping** based on the
|
|
90
|
+
request.
|
|
91
|
+
|
|
92
|
+
#### Add
|
|
93
|
+
|
|
94
|
+
1. Check for duplicates. If a near-duplicate exists, update it instead.
|
|
95
|
+
2. Pick the section per the routing rules in
|
|
96
|
+
[references/format.md](references/format.md#section-routing).
|
|
97
|
+
3. Append the entry at the **bottom** of that section using the format from
|
|
98
|
+
`references/format.md`.
|
|
99
|
+
4. Verify any referenced Project note exists.
|
|
100
|
+
|
|
101
|
+
#### Update
|
|
102
|
+
|
|
103
|
+
Find the task by its bold title (fuzzy match OK). Apply changes:
|
|
104
|
+
|
|
105
|
+
- **Status change:** move the entire entry between sections.
|
|
106
|
+
- **Priority change:** update the `| {priority} |` segment.
|
|
107
|
+
- **Due date change:** update or add `| due YYYY-MM-DD |`.
|
|
108
|
+
- **Add context:** append to the indented line.
|
|
109
|
+
|
|
110
|
+
Use Edit for targeted modifications.
|
|
111
|
+
|
|
112
|
+
#### Close
|
|
113
|
+
|
|
114
|
+
Find the task, remove it from its section, and add to the **top** of
|
|
115
|
+
`## Recently Done` using the closed-task format in
|
|
116
|
+
[references/format.md](references/format.md#closed-task-format).
|
|
117
|
+
|
|
118
|
+
#### List
|
|
119
|
+
|
|
120
|
+
Run the queries in [references/format.md](references/format.md#useful-queries)
|
|
121
|
+
and present results grouped by person or project as the user's question
|
|
122
|
+
warrants.
|
|
123
|
+
|
|
124
|
+
#### Housekeeping (scheduled)
|
|
125
|
+
|
|
126
|
+
1. Prune entries older than 14 days from `## Recently Done`.
|
|
127
|
+
2. Flag overdue tasks (due in the past, still in `## Open` or `## In Progress`)
|
|
128
|
+
— report to the user; do **not** auto-modify.
|
|
129
|
+
3. Merge duplicates on the same board.
|
|
130
|
+
4. Spot-check `[[People/]]`, `[[Projects/]]`, `[[Goals/]]` references point to
|
|
131
|
+
existing notes.
|
|
132
|
+
|
|
133
|
+
### 4. Write updates
|
|
134
|
+
|
|
135
|
+
For a new board, create `knowledge/Tasks/{Person Name}.md` with all four
|
|
136
|
+
sections (template in
|
|
137
|
+
[references/format.md](references/format.md#board-structure)).
|
|
138
|
+
|
|
139
|
+
For an existing board, use Edit for targeted changes — never rewrite the whole
|
|
140
|
+
file.
|
|
141
|
+
|
|
142
|
+
### 5. Migrate open items (one-time)
|
|
143
|
+
|
|
144
|
+
When first setting up boards, scan source notes for `## Open items` sections:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
rg -l "## Open items" knowledge/People/ knowledge/Projects/ knowledge/Goals/
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Convert each item into a task entry on the relevant person's board. **Do not**
|
|
151
|
+
remove the original — it stays as the historical record; the board becomes the
|
|
152
|
+
living tracker.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Task Board Format
|
|
2
|
+
|
|
3
|
+
Reference data for `manage-tasks`.
|
|
4
|
+
|
|
5
|
+
## Board structure
|
|
6
|
+
|
|
7
|
+
Each task board is a markdown file with four sections, always present (even if
|
|
8
|
+
empty), in this order:
|
|
9
|
+
|
|
10
|
+
```markdown
|
|
11
|
+
# {Person Name}
|
|
12
|
+
|
|
13
|
+
## In Progress
|
|
14
|
+
|
|
15
|
+
## Open
|
|
16
|
+
|
|
17
|
+
## Blocked
|
|
18
|
+
|
|
19
|
+
## Recently Done
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Task entry
|
|
23
|
+
|
|
24
|
+
```markdown
|
|
25
|
+
- [ ] **{Task title}** | {priority} | due {YYYY-MM-DD} | [[Projects/Name]] → [[Goals/Name]]
|
|
26
|
+
{Context line with source info and backlinks.}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Conventions
|
|
30
|
+
|
|
31
|
+
- **Priorities:** `high` | `medium` | `low`. Omit when medium (the default).
|
|
32
|
+
- **Due dates:** include only when there is a real deadline. Format
|
|
33
|
+
`due YYYY-MM-DD`.
|
|
34
|
+
- **Goal link:** append `→ [[Goals/Name]]` only when the task clearly serves a
|
|
35
|
+
specific Goal — not every task needs one.
|
|
36
|
+
- **Titles are the ID:** keep them unique within a board. No task IDs.
|
|
37
|
+
- **Recently Done:** keep the last 14 days; older entries are pruned during
|
|
38
|
+
housekeeping.
|
|
39
|
+
|
|
40
|
+
## Closed-task format
|
|
41
|
+
|
|
42
|
+
Closed entries drop priority, due date, project link, and context:
|
|
43
|
+
|
|
44
|
+
```markdown
|
|
45
|
+
- [x] **{Task title}** | completed {YYYY-MM-DD}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Section routing
|
|
49
|
+
|
|
50
|
+
- Default add target: `## Open`.
|
|
51
|
+
- Move to `## In Progress` when the user says "I'm working on" / "started".
|
|
52
|
+
- Move to `## Blocked` when the user describes a wait condition; include what's
|
|
53
|
+
needed and from whom in the context line.
|
|
54
|
+
- Move to `## Recently Done` when closed.
|
|
55
|
+
|
|
56
|
+
## Useful queries
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# All open / in-progress tasks
|
|
60
|
+
rg "^- \[ \] \*\*" knowledge/Tasks/
|
|
61
|
+
|
|
62
|
+
# Tasks for a specific project
|
|
63
|
+
rg "Projects/{Name}" knowledge/Tasks/
|
|
64
|
+
|
|
65
|
+
# High priority tasks
|
|
66
|
+
rg "\| high \|" knowledge/Tasks/
|
|
67
|
+
|
|
68
|
+
# Find every due date — compare against today to spot overdue
|
|
69
|
+
rg "due 20[0-9]{2}-[0-9]{2}-[0-9]{2}" knowledge/Tasks/
|
|
70
|
+
|
|
71
|
+
# Blocked tasks with their context
|
|
72
|
+
rg -A1 "^- \[ \]" knowledge/Tasks/ | rg -B1 "Waiting on"
|
|
73
|
+
```
|