@forwardimpact/basecamp 1.0.0 → 2.2.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 +1 -1
- package/config/scheduler.json +18 -17
- package/package.json +3 -3
- package/src/basecamp.js +532 -259
- package/template/.claude/agents/chief-of-staff.md +103 -0
- package/template/.claude/agents/concierge.md +75 -0
- package/template/.claude/agents/librarian.md +59 -0
- package/template/.claude/agents/postman.md +73 -0
- package/template/.claude/agents/recruiter.md +222 -0
- package/template/.claude/settings.json +0 -4
- package/template/.claude/skills/analyze-cv/SKILL.md +267 -0
- package/template/.claude/skills/create-presentations/SKILL.md +2 -2
- package/template/.claude/skills/create-presentations/references/slide.css +1 -1
- package/template/.claude/skills/create-presentations/scripts/convert-to-pdf.mjs +47 -0
- package/template/.claude/skills/draft-emails/SKILL.md +85 -123
- package/template/.claude/skills/draft-emails/scripts/scan-emails.mjs +66 -0
- package/template/.claude/skills/draft-emails/scripts/send-email.mjs +118 -0
- package/template/.claude/skills/extract-entities/SKILL.md +2 -2
- package/template/.claude/skills/extract-entities/scripts/state.mjs +130 -0
- package/template/.claude/skills/manage-tasks/SKILL.md +242 -0
- package/template/.claude/skills/organize-files/SKILL.md +3 -3
- package/template/.claude/skills/organize-files/scripts/organize-by-type.mjs +105 -0
- package/template/.claude/skills/organize-files/scripts/summarize.mjs +84 -0
- package/template/.claude/skills/process-hyprnote/SKILL.md +2 -2
- package/template/.claude/skills/send-chat/SKILL.md +170 -0
- package/template/.claude/skills/sync-apple-calendar/SKILL.md +5 -5
- package/template/.claude/skills/sync-apple-calendar/scripts/sync.mjs +325 -0
- package/template/.claude/skills/sync-apple-mail/SKILL.md +6 -6
- package/template/.claude/skills/sync-apple-mail/scripts/parse-emlx.mjs +374 -0
- package/template/.claude/skills/sync-apple-mail/scripts/sync.mjs +629 -0
- package/template/.claude/skills/track-candidates/SKILL.md +375 -0
- package/template/.claude/skills/weekly-update/SKILL.md +250 -0
- package/template/CLAUDE.md +73 -29
- package/template/knowledge/Briefings/.gitkeep +0 -0
- package/template/.claude/skills/create-presentations/scripts/convert-to-pdf.js +0 -32
- package/template/.claude/skills/draft-emails/scripts/scan-emails.sh +0 -34
- package/template/.claude/skills/extract-entities/scripts/state.py +0 -100
- package/template/.claude/skills/organize-files/scripts/organize-by-type.sh +0 -42
- package/template/.claude/skills/organize-files/scripts/summarize.sh +0 -21
- package/template/.claude/skills/sync-apple-calendar/scripts/sync.py +0 -242
- package/template/.claude/skills/sync-apple-mail/scripts/parse-emlx.py +0 -104
- package/template/.claude/skills/sync-apple-mail/scripts/sync.py +0 -455
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scan for unprocessed emails and output their IDs and subjects.
|
|
4
|
+
*
|
|
5
|
+
* Checks ~/.cache/fit/basecamp/apple_mail/ for email thread markdown files not
|
|
6
|
+
* yet listed in drafts/drafted or drafts/ignored. Outputs one tab-separated
|
|
7
|
+
* line per unprocessed thread: email_id<TAB>subject. Used by the draft-emails
|
|
8
|
+
* skill to identify threads that need a reply.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { basename, join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
|
|
15
|
+
const HELP = `scan-emails — list unprocessed email threads
|
|
16
|
+
|
|
17
|
+
Usage: node scripts/scan-emails.mjs [-h|--help]
|
|
18
|
+
|
|
19
|
+
Scans ~/.cache/fit/basecamp/apple_mail/ for .md thread files not yet
|
|
20
|
+
recorded in drafts/drafted or drafts/ignored. Outputs one line per
|
|
21
|
+
unprocessed thread as: email_id<TAB>subject`;
|
|
22
|
+
|
|
23
|
+
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
24
|
+
console.log(HELP);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const HOME = homedir();
|
|
29
|
+
const MAIL_DIR = join(HOME, ".cache/fit/basecamp/apple_mail");
|
|
30
|
+
|
|
31
|
+
/** Load a file of IDs (one per line) into a Set. */
|
|
32
|
+
function loadIdSet(path) {
|
|
33
|
+
const ids = new Set();
|
|
34
|
+
if (!existsSync(path)) return ids;
|
|
35
|
+
for (const line of readFileSync(path, "utf-8").split("\n")) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (trimmed) ids.add(trimmed);
|
|
38
|
+
}
|
|
39
|
+
return ids;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Extract the first H1 heading from a markdown file. */
|
|
43
|
+
function extractSubject(filePath) {
|
|
44
|
+
const text = readFileSync(filePath, "utf-8");
|
|
45
|
+
const match = text.match(/^# (.+)$/m);
|
|
46
|
+
return match ? match[1] : "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function main() {
|
|
50
|
+
if (!existsSync(MAIL_DIR)) return;
|
|
51
|
+
|
|
52
|
+
const drafted = loadIdSet("drafts/drafted");
|
|
53
|
+
const ignored = loadIdSet("drafts/ignored");
|
|
54
|
+
|
|
55
|
+
for (const name of readdirSync(MAIL_DIR).sort()) {
|
|
56
|
+
if (!name.endsWith(".md")) continue;
|
|
57
|
+
|
|
58
|
+
const emailId = basename(name, ".md");
|
|
59
|
+
if (drafted.has(emailId) || ignored.has(emailId)) continue;
|
|
60
|
+
|
|
61
|
+
const subject = extractSubject(join(MAIL_DIR, name));
|
|
62
|
+
console.log(`${emailId}\t${subject}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
main();
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Send an email via Apple Mail using AppleScript.
|
|
4
|
+
*
|
|
5
|
+
* Builds an AppleScript command to create and send an outgoing message through
|
|
6
|
+
* Apple Mail. The script writes a temporary .scpt file, executes it with
|
|
7
|
+
* osascript, and cleans up afterwards. Mail.app must be running.
|
|
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.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { mkdtempSync, writeFileSync, unlinkSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
|
|
18
|
+
const HELP = `send-email — send an email via Apple Mail
|
|
19
|
+
|
|
20
|
+
Usage: node scripts/send-email.mjs --to <addrs> --subject <subj> --body <text> [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--to <addrs> Comma-separated To recipients (required)
|
|
24
|
+
--cc <addrs> Comma-separated CC recipients
|
|
25
|
+
--bcc <addrs> Comma-separated BCC recipients
|
|
26
|
+
--subject <subj> Email subject line (required)
|
|
27
|
+
--body <text> Plain-text email body (required)
|
|
28
|
+
-h, --help Show this help message and exit
|
|
29
|
+
|
|
30
|
+
Mail.app must be running. No email signature needed — Apple Mail appends it.`;
|
|
31
|
+
|
|
32
|
+
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
33
|
+
console.log(HELP);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Escape a string for AppleScript double-quoted context. */
|
|
38
|
+
function escapeAS(s) {
|
|
39
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Build recipient lines for AppleScript. */
|
|
43
|
+
function recipientLines(type, addrs) {
|
|
44
|
+
if (!addrs) return "";
|
|
45
|
+
return addrs
|
|
46
|
+
.split(",")
|
|
47
|
+
.map((a) => a.trim())
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.map(
|
|
50
|
+
(addr) =>
|
|
51
|
+
` make new ${type} at end of ${type}s with properties {address:"${escapeAS(addr)}"}`,
|
|
52
|
+
)
|
|
53
|
+
.join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function main() {
|
|
57
|
+
const args = process.argv.slice(2);
|
|
58
|
+
let to = "",
|
|
59
|
+
cc = "",
|
|
60
|
+
bcc = "",
|
|
61
|
+
subject = "",
|
|
62
|
+
body = "";
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < args.length; i++) {
|
|
65
|
+
switch (args[i]) {
|
|
66
|
+
case "--to":
|
|
67
|
+
to = args[++i] ?? "";
|
|
68
|
+
break;
|
|
69
|
+
case "--cc":
|
|
70
|
+
cc = args[++i] ?? "";
|
|
71
|
+
break;
|
|
72
|
+
case "--bcc":
|
|
73
|
+
bcc = args[++i] ?? "";
|
|
74
|
+
break;
|
|
75
|
+
case "--subject":
|
|
76
|
+
subject = args[++i] ?? "";
|
|
77
|
+
break;
|
|
78
|
+
case "--body":
|
|
79
|
+
body = args[++i] ?? "";
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!to || !subject || !body) {
|
|
85
|
+
console.error("Error: --to, --subject, and --body are required.");
|
|
86
|
+
console.error("Run with --help for usage info.");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lines = [
|
|
91
|
+
'tell application "Mail"',
|
|
92
|
+
` set newMessage to make new outgoing message with properties {subject:"${escapeAS(subject)}", content:"${escapeAS(body)}", visible:false}`,
|
|
93
|
+
" tell newMessage",
|
|
94
|
+
recipientLines("to recipient", to),
|
|
95
|
+
cc ? recipientLines("cc recipient", cc) : "",
|
|
96
|
+
bcc ? recipientLines("bcc recipient", bcc) : "",
|
|
97
|
+
" end tell",
|
|
98
|
+
" send newMessage",
|
|
99
|
+
"end tell",
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join("\n");
|
|
103
|
+
|
|
104
|
+
const tmp = join(mkdtempSync(join(tmpdir(), "send-email-")), "mail.scpt");
|
|
105
|
+
try {
|
|
106
|
+
writeFileSync(tmp, lines);
|
|
107
|
+
execFileSync("osascript", [tmp], { stdio: "inherit" });
|
|
108
|
+
console.log(`Sent: ${subject}`);
|
|
109
|
+
} finally {
|
|
110
|
+
try {
|
|
111
|
+
unlinkSync(tmp);
|
|
112
|
+
} catch {
|
|
113
|
+
// ignore cleanup errors
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
main();
|
|
@@ -68,7 +68,7 @@ Run this skill:
|
|
|
68
68
|
1. Read `USER.md` to get the user's name, email, and domain
|
|
69
69
|
2. Find new/changed files to process:
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
node scripts/state.mjs check
|
|
72
72
|
|
|
73
73
|
This outputs one file path per line for all source files that are new or
|
|
74
74
|
have changed since last processing.
|
|
@@ -434,7 +434,7 @@ Always use absolute links: `[[People/Sarah Chen]]`,
|
|
|
434
434
|
|
|
435
435
|
After processing each file, update the state:
|
|
436
436
|
|
|
437
|
-
|
|
437
|
+
node scripts/state.mjs update "$FILE"
|
|
438
438
|
|
|
439
439
|
## Source Type Rules Summary
|
|
440
440
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Manage graph_processed state for entity extraction.
|
|
4
|
+
*
|
|
5
|
+
* Tracks which source files (synced emails and calendar events) have already
|
|
6
|
+
* been processed by the extract-entities skill. The `check` command lists files
|
|
7
|
+
* that are new or changed since their last recorded hash; `update` marks files
|
|
8
|
+
* as processed by storing their current SHA-256 hash.
|
|
9
|
+
*
|
|
10
|
+
* State is persisted as a TSV file at
|
|
11
|
+
* ~/.cache/fit/basecamp/state/graph_processed (path<TAB>hash).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
15
|
+
console.log(`state — manage graph_processed state for entity extraction
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
node scripts/state.mjs check List new/changed files
|
|
19
|
+
node scripts/state.mjs update <path> [<path>…] Mark files as processed
|
|
20
|
+
node scripts/state.mjs -h|--help Show this help message
|
|
21
|
+
|
|
22
|
+
State file: ~/.cache/fit/basecamp/state/graph_processed (TSV: path<TAB>hash)`);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
27
|
+
import {
|
|
28
|
+
existsSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
readFileSync,
|
|
31
|
+
readdirSync,
|
|
32
|
+
statSync,
|
|
33
|
+
writeFileSync,
|
|
34
|
+
} from "node:fs";
|
|
35
|
+
import { homedir } from "node:os";
|
|
36
|
+
import { join } from "node:path";
|
|
37
|
+
|
|
38
|
+
const HOME = homedir();
|
|
39
|
+
const STATE_FILE = join(HOME, ".cache/fit/basecamp/state/graph_processed");
|
|
40
|
+
const SOURCE_DIRS = [
|
|
41
|
+
join(HOME, ".cache/fit/basecamp/apple_mail"),
|
|
42
|
+
join(HOME, ".cache/fit/basecamp/apple_calendar"),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Compute SHA-256 hash of a file. */
|
|
46
|
+
function fileHash(filePath) {
|
|
47
|
+
const data = readFileSync(filePath);
|
|
48
|
+
return createHash("sha256").update(data).digest("hex");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Load the state file into a Map of {path → hash}. */
|
|
52
|
+
function loadState() {
|
|
53
|
+
const state = new Map();
|
|
54
|
+
if (!existsSync(STATE_FILE)) return state;
|
|
55
|
+
const text = readFileSync(STATE_FILE, "utf-8");
|
|
56
|
+
for (const line of text.split("\n")) {
|
|
57
|
+
if (!line) continue;
|
|
58
|
+
const idx = line.indexOf("\t");
|
|
59
|
+
if (idx === -1) continue;
|
|
60
|
+
state.set(line.slice(0, idx), line.slice(idx + 1));
|
|
61
|
+
}
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Write the full state Map back to the state file. */
|
|
66
|
+
function saveState(state) {
|
|
67
|
+
const dir = join(HOME, ".cache/fit/basecamp/state");
|
|
68
|
+
mkdirSync(dir, { recursive: true });
|
|
69
|
+
const entries = [...state.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
70
|
+
const text = entries.length
|
|
71
|
+
? entries.map(([p, h]) => `${p}\t${h}`).join("\n") + "\n"
|
|
72
|
+
: "";
|
|
73
|
+
writeFileSync(STATE_FILE, text);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Find source files that are new or have changed since last processing. */
|
|
77
|
+
function check() {
|
|
78
|
+
const state = loadState();
|
|
79
|
+
const newFiles = [];
|
|
80
|
+
for (const dir of SOURCE_DIRS) {
|
|
81
|
+
if (!existsSync(dir)) continue;
|
|
82
|
+
for (const name of readdirSync(dir)) {
|
|
83
|
+
const filePath = join(dir, name);
|
|
84
|
+
const stat = statSync(filePath, { throwIfNoEntry: false });
|
|
85
|
+
if (!stat || !stat.isFile()) continue;
|
|
86
|
+
const h = fileHash(filePath);
|
|
87
|
+
if (state.get(filePath) !== h) {
|
|
88
|
+
newFiles.push(filePath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
newFiles.sort();
|
|
93
|
+
for (const f of newFiles) {
|
|
94
|
+
console.log(f);
|
|
95
|
+
}
|
|
96
|
+
return newFiles.length;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Mark files as processed by updating their hashes in state. */
|
|
100
|
+
function update(filePaths) {
|
|
101
|
+
const state = loadState();
|
|
102
|
+
for (const fp of filePaths) {
|
|
103
|
+
if (!existsSync(fp)) {
|
|
104
|
+
console.error(`Warning: File not found: ${fp}`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
state.set(fp, fileHash(fp));
|
|
108
|
+
}
|
|
109
|
+
saveState(state);
|
|
110
|
+
console.log(`Updated ${filePaths.length} file(s) in graph state`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- CLI ---
|
|
114
|
+
|
|
115
|
+
const args = process.argv.slice(2);
|
|
116
|
+
const cmd = args[0];
|
|
117
|
+
|
|
118
|
+
if (cmd === "check") {
|
|
119
|
+
const count = check();
|
|
120
|
+
console.error(`\n${count} file(s) to process`);
|
|
121
|
+
} else if (cmd === "update" && args.length >= 2) {
|
|
122
|
+
update(args.slice(1));
|
|
123
|
+
} else {
|
|
124
|
+
console.error(
|
|
125
|
+
"Usage:\n" +
|
|
126
|
+
" node scripts/state.mjs check\n" +
|
|
127
|
+
" node scripts/state.mjs update <file-path> [<file-path> …]",
|
|
128
|
+
);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
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 process-hyprnote.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Manage Tasks
|
|
7
|
+
|
|
8
|
+
Manage per-person task boards in `knowledge/Tasks/`. Each person has a single
|
|
9
|
+
living document that tracks all their open, in-progress, blocked, and recently
|
|
10
|
+
completed tasks. Task boards are the **canonical source** for task tracking —
|
|
11
|
+
other notes (People, Projects) link to them rather than duplicating.
|
|
12
|
+
|
|
13
|
+
## Trigger
|
|
14
|
+
|
|
15
|
+
Run this skill:
|
|
16
|
+
|
|
17
|
+
- When the user asks to add, update, close, or list tasks
|
|
18
|
+
- When chained from `extract-entities` or `process-hyprnote` with extracted
|
|
19
|
+
action items
|
|
20
|
+
- When the user asks to see someone's task board or workload
|
|
21
|
+
- On a schedule to perform housekeeping (prune done items, flag overdue)
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- `knowledge/Tasks/` directory exists
|
|
26
|
+
- User identity configured in `USER.md`
|
|
27
|
+
|
|
28
|
+
## Inputs
|
|
29
|
+
|
|
30
|
+
### User-initiated
|
|
31
|
+
|
|
32
|
+
- Person name and task details from the user's request
|
|
33
|
+
|
|
34
|
+
### Chained from other skills
|
|
35
|
+
|
|
36
|
+
Action items extracted by `extract-entities` or `process-hyprnote`, passed as
|
|
37
|
+
structured data:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
TASKS:
|
|
41
|
+
- Owner: {Full Name}
|
|
42
|
+
Task: {Description — starts with a verb}
|
|
43
|
+
Priority: high|medium|low
|
|
44
|
+
Due: YYYY-MM-DD (if known)
|
|
45
|
+
Source: meeting|email
|
|
46
|
+
Source date: YYYY-MM-DD
|
|
47
|
+
Project: {Project Name} (if applicable)
|
|
48
|
+
Context: {Brief context about where this came from}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Outputs
|
|
52
|
+
|
|
53
|
+
- `knowledge/Tasks/{Person Name}.md` — created or updated task boards
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Task Board Format
|
|
58
|
+
|
|
59
|
+
Each task board is a markdown file with four sections, always in this order:
|
|
60
|
+
|
|
61
|
+
```markdown
|
|
62
|
+
# {Person Name}
|
|
63
|
+
|
|
64
|
+
## In Progress
|
|
65
|
+
## Open
|
|
66
|
+
## Blocked
|
|
67
|
+
## Recently Done
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
All four sections are always present, even if empty.
|
|
71
|
+
|
|
72
|
+
**Task entry format:**
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
- [ ] **{Task title}** | {priority} | due {YYYY-MM-DD} | [[Projects/Name]]
|
|
76
|
+
{Context line with source info and backlinks.}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Key conventions:**
|
|
80
|
+
|
|
81
|
+
- **Priorities:** `high` | `medium` | `low` — omit if medium (default)
|
|
82
|
+
- **Due dates:** Only include if there's a real deadline. Format:
|
|
83
|
+
`due YYYY-MM-DD`
|
|
84
|
+
- **No task IDs.** Tasks are identified by their bold title. Keep titles unique
|
|
85
|
+
within a person's board.
|
|
86
|
+
- **Recently Done:** Keep the last 14 days. Older items pruned during
|
|
87
|
+
housekeeping.
|
|
88
|
+
|
|
89
|
+
## Before Starting
|
|
90
|
+
|
|
91
|
+
1. Read `USER.md` to get user identity.
|
|
92
|
+
2. Determine the operation: **add**, **update**, **close**, **list**, or
|
|
93
|
+
**housekeeping**.
|
|
94
|
+
|
|
95
|
+
## Step 1: Resolve the Person
|
|
96
|
+
|
|
97
|
+
For any task operation, resolve the person to their canonical name:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
ls knowledge/Tasks/
|
|
101
|
+
rg "{name}" knowledge/People/
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If the person doesn't have a task board yet, create one from the template (Step
|
|
105
|
+
4 covers creation).
|
|
106
|
+
|
|
107
|
+
## Step 2: Read Current Task Board
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cat "knowledge/Tasks/{Person Name}.md"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Parse existing tasks to:
|
|
114
|
+
|
|
115
|
+
- Avoid duplicates (same person, similar description)
|
|
116
|
+
- Understand current workload
|
|
117
|
+
- Find the right insertion point
|
|
118
|
+
|
|
119
|
+
## Step 3: Perform the Operation
|
|
120
|
+
|
|
121
|
+
### Add a Task
|
|
122
|
+
|
|
123
|
+
1. Check for duplicates — same person, similar task title or description. If a
|
|
124
|
+
near-duplicate exists, update it instead of creating a new entry.
|
|
125
|
+
2. Determine the section:
|
|
126
|
+
- Default: `## Open`
|
|
127
|
+
- If user says "I'm working on" / "started" → `## In Progress`
|
|
128
|
+
- If blocked → `## Blocked`
|
|
129
|
+
3. Format the task entry:
|
|
130
|
+
```
|
|
131
|
+
- [ ] **{Task title}** | {priority} | due {YYYY-MM-DD} | [[Projects/Name]]
|
|
132
|
+
{Context line with source info and backlinks.}
|
|
133
|
+
```
|
|
134
|
+
4. Add the entry at the **bottom** of the appropriate section.
|
|
135
|
+
5. If the task references a project, verify the project note exists.
|
|
136
|
+
|
|
137
|
+
### Update a Task
|
|
138
|
+
|
|
139
|
+
1. Find the task by title (fuzzy match OK — bold text between `**`).
|
|
140
|
+
2. Apply changes:
|
|
141
|
+
- **Status change:** Move the entire entry between sections
|
|
142
|
+
- **Priority change:** Update the `| {priority} |` segment
|
|
143
|
+
- **Due date change:** Update or add `| due YYYY-MM-DD |`
|
|
144
|
+
- **Add context:** Append to the indented line
|
|
145
|
+
3. Use the Edit tool for targeted modifications.
|
|
146
|
+
|
|
147
|
+
### Close a Task
|
|
148
|
+
|
|
149
|
+
1. Find the task by title.
|
|
150
|
+
2. Remove it from its current section.
|
|
151
|
+
3. Add to the **top** of `## Recently Done`:
|
|
152
|
+
```
|
|
153
|
+
- [x] **{Task title}** | completed {YYYY-MM-DD}
|
|
154
|
+
```
|
|
155
|
+
(Drop priority, due date, project link, and context — keep it compact.)
|
|
156
|
+
|
|
157
|
+
### List Tasks
|
|
158
|
+
|
|
159
|
+
Query across all task boards:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# All open/in-progress tasks
|
|
163
|
+
rg "^- \[ \] \*\*" knowledge/Tasks/
|
|
164
|
+
|
|
165
|
+
# Tasks for a specific project
|
|
166
|
+
rg "Projects/{Name}" knowledge/Tasks/
|
|
167
|
+
|
|
168
|
+
# High priority tasks
|
|
169
|
+
rg "\| high \|" knowledge/Tasks/
|
|
170
|
+
|
|
171
|
+
# Overdue tasks — find all due dates and compare against today
|
|
172
|
+
rg "due 20[0-9]{2}-[0-9]{2}-[0-9]{2}" knowledge/Tasks/
|
|
173
|
+
|
|
174
|
+
# Blocked tasks
|
|
175
|
+
rg -A1 "^- \[ \]" knowledge/Tasks/ | rg -B1 "Waiting on"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Present results in a clean summary, grouped by person or project as appropriate
|
|
179
|
+
for the user's question.
|
|
180
|
+
|
|
181
|
+
### Housekeeping (Scheduled)
|
|
182
|
+
|
|
183
|
+
Run across all task boards:
|
|
184
|
+
|
|
185
|
+
1. **Prune done items:** Remove completed tasks older than 14 days from
|
|
186
|
+
`## Recently Done`.
|
|
187
|
+
2. **Flag overdue:** Any task with `due {date}` in the past that's still in
|
|
188
|
+
`## Open` or `## In Progress` — check if it needs attention. Do NOT
|
|
189
|
+
auto-modify the task; instead, report overdue items to the user.
|
|
190
|
+
3. **Deduplicate:** If identical tasks appear on the same board, merge them.
|
|
191
|
+
4. **Validate links:** Spot-check that `[[People/]]` and `[[Projects/]]`
|
|
192
|
+
references point to existing notes.
|
|
193
|
+
|
|
194
|
+
## Step 4: Write Updates
|
|
195
|
+
|
|
196
|
+
### New task board
|
|
197
|
+
|
|
198
|
+
Create `knowledge/Tasks/{Person Name}.md` with all four sections:
|
|
199
|
+
|
|
200
|
+
```markdown
|
|
201
|
+
# {Person Name}
|
|
202
|
+
|
|
203
|
+
## In Progress
|
|
204
|
+
|
|
205
|
+
## Open
|
|
206
|
+
|
|
207
|
+
## Blocked
|
|
208
|
+
|
|
209
|
+
## Recently Done
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Existing task board
|
|
213
|
+
|
|
214
|
+
Use the Edit tool to make targeted changes — add, move, or modify individual
|
|
215
|
+
task entries. Do NOT rewrite the entire file.
|
|
216
|
+
|
|
217
|
+
## Step 5: Migrate Open Items (One-time)
|
|
218
|
+
|
|
219
|
+
When first setting up task boards, or when the user asks, migrate existing
|
|
220
|
+
`## Open items` from People and Project notes:
|
|
221
|
+
|
|
222
|
+
1. Scan notes for `## Open items` sections with content:
|
|
223
|
+
```bash
|
|
224
|
+
rg -l "## Open items" knowledge/People/ knowledge/Projects/
|
|
225
|
+
```
|
|
226
|
+
2. For each note with open items, read the items and convert them to task board
|
|
227
|
+
entries.
|
|
228
|
+
3. Add each item to the appropriate person's task board.
|
|
229
|
+
4. **Do NOT remove** the original open items from source notes — they serve as
|
|
230
|
+
the historical record. The task board becomes the living tracker.
|
|
231
|
+
|
|
232
|
+
## Quality Checklist
|
|
233
|
+
|
|
234
|
+
- [ ] Task title is clear and actionable (starts with a verb)
|
|
235
|
+
- [ ] Priority set appropriately (omit if medium)
|
|
236
|
+
- [ ] Due date included only if there's a real deadline
|
|
237
|
+
- [ ] Project linked with `[[Projects/Name]]` if applicable
|
|
238
|
+
- [ ] No duplicate tasks on the board
|
|
239
|
+
- [ ] Recently Done pruned to last 14 days (housekeeping)
|
|
240
|
+
- [ ] All backlinks use absolute paths `[[Folder/Name]]`
|
|
241
|
+
- [ ] Context line is concise (1-2 lines max)
|
|
242
|
+
- [ ] New task board has all four sections present
|
|
@@ -61,7 +61,7 @@ Run when the user asks to find, organize, clean up, or tidy files on their Mac.
|
|
|
61
61
|
|
|
62
62
|
Get an overview of both directories:
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
node scripts/summarize.mjs
|
|
65
65
|
|
|
66
66
|
## Finding Files
|
|
67
67
|
|
|
@@ -79,8 +79,8 @@ find ~/Desktop -maxdepth 1 \( -name "Screenshot*" -o -name "Screen Shot*" \)
|
|
|
79
79
|
Organize a directory into type-based subdirectories (Documents, Images,
|
|
80
80
|
Archives, Installers, Screenshots):
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
node scripts/organize-by-type.mjs ~/Downloads
|
|
83
|
+
node scripts/organize-by-type.mjs ~/Desktop
|
|
84
84
|
|
|
85
85
|
The script creates subdirectories and moves matching files. It does NOT delete
|
|
86
86
|
anything.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Organize files in a directory by type into subdirectories.
|
|
4
|
+
*
|
|
5
|
+
* Scans the top level of the given directory and moves files into category
|
|
6
|
+
* subdirectories: Screenshots, Documents, Images, Archives, and Installers.
|
|
7
|
+
* Categories are determined by file extension and name prefix. Files that do
|
|
8
|
+
* not match any category are left in place. Does NOT delete anything.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
renameSync,
|
|
16
|
+
statSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { extname, join } from "node:path";
|
|
19
|
+
|
|
20
|
+
const HELP = `organize-by-type — sort files into type-based subdirectories
|
|
21
|
+
|
|
22
|
+
Usage: node scripts/organize-by-type.mjs <directory> [-h|--help]
|
|
23
|
+
|
|
24
|
+
Creates subdirectories (Documents, Images, Archives, Installers, Screenshots)
|
|
25
|
+
and moves matching top-level files into them. Does NOT delete any files.`;
|
|
26
|
+
|
|
27
|
+
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
28
|
+
console.log(HELP);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const CATEGORIES = [
|
|
33
|
+
{
|
|
34
|
+
name: "Screenshots",
|
|
35
|
+
test: (name) =>
|
|
36
|
+
name.startsWith("Screenshot") || name.startsWith("Screen Shot"),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "Documents",
|
|
40
|
+
test: (_name, ext) =>
|
|
41
|
+
[
|
|
42
|
+
".pdf",
|
|
43
|
+
".doc",
|
|
44
|
+
".docx",
|
|
45
|
+
".txt",
|
|
46
|
+
".md",
|
|
47
|
+
".rtf",
|
|
48
|
+
".csv",
|
|
49
|
+
".xlsx",
|
|
50
|
+
].includes(ext),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "Images",
|
|
54
|
+
test: (_name, ext) =>
|
|
55
|
+
[".png", ".jpg", ".jpeg", ".gif", ".webp"].includes(ext),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "Archives",
|
|
59
|
+
test: (name, ext) =>
|
|
60
|
+
[".zip", ".rar"].includes(ext) || name.endsWith(".tar.gz"),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "Installers",
|
|
64
|
+
test: (_name, ext) => ext === ".dmg",
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function main() {
|
|
69
|
+
const dir = process.argv[2];
|
|
70
|
+
if (!dir) {
|
|
71
|
+
console.error("Usage: node scripts/organize-by-type.mjs <directory>");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
75
|
+
console.error(`Error: Directory not found: ${dir}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Create subdirectories
|
|
80
|
+
for (const cat of CATEGORIES) {
|
|
81
|
+
mkdirSync(join(dir, cat.name), { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let moved = 0;
|
|
85
|
+
for (const name of readdirSync(dir)) {
|
|
86
|
+
const fullPath = join(dir, name);
|
|
87
|
+
const stat = statSync(fullPath, { throwIfNoEntry: false });
|
|
88
|
+
if (!stat || !stat.isFile()) continue;
|
|
89
|
+
|
|
90
|
+
const ext = extname(name).toLowerCase();
|
|
91
|
+
for (const cat of CATEGORIES) {
|
|
92
|
+
if (cat.test(name, ext)) {
|
|
93
|
+
const dest = join(dir, cat.name, name);
|
|
94
|
+
renameSync(fullPath, dest);
|
|
95
|
+
console.log(`${name} -> ${cat.name}/`);
|
|
96
|
+
moved++;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`\nOrganization complete: ${dir} (${moved} files moved)`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
main();
|