@forwardimpact/basecamp 0.3.0 → 1.0.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 +69 -84
- package/package.json +9 -11
- package/{basecamp.js → src/basecamp.js} +153 -113
- package/template/.claude/settings.json +49 -0
- package/template/.claude/skills/draft-emails/SKILL.md +32 -3
- package/template/.claude/skills/draft-emails/scripts/scan-emails.sh +3 -2
- package/template/.claude/skills/extract-entities/SKILL.md +0 -1
- package/template/.claude/skills/extract-entities/references/TEMPLATES.md +1 -0
- package/template/.claude/skills/process-hyprnote/SKILL.md +335 -0
- package/template/.claude/skills/sync-apple-calendar/SKILL.md +6 -3
- package/template/.claude/skills/sync-apple-calendar/scripts/sync.py +13 -4
- package/template/.claude/skills/sync-apple-mail/SKILL.md +17 -5
- package/template/.claude/skills/sync-apple-mail/references/SCHEMA.md +32 -5
- package/template/.claude/skills/sync-apple-mail/scripts/sync.py +134 -27
- package/template/CLAUDE.md +60 -14
- package/build.js +0 -122
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
writeFileSync,
|
|
17
17
|
existsSync,
|
|
18
18
|
mkdirSync,
|
|
19
|
-
readdirSync,
|
|
20
19
|
unlinkSync,
|
|
21
20
|
chmodSync,
|
|
22
21
|
} from "node:fs";
|
|
@@ -34,9 +33,24 @@ const STATE_PATH = join(BASECAMP_HOME, "state.json");
|
|
|
34
33
|
const LOG_DIR = join(BASECAMP_HOME, "logs");
|
|
35
34
|
const __dirname =
|
|
36
35
|
import.meta.dirname || dirname(fileURLToPath(import.meta.url));
|
|
37
|
-
const
|
|
36
|
+
const SHARE_DIR = "/usr/local/share/fit-basecamp";
|
|
38
37
|
const SOCKET_PATH = join(BASECAMP_HOME, "basecamp.sock");
|
|
39
38
|
|
|
39
|
+
// --- posix_spawn (macOS app bundle only) ------------------------------------
|
|
40
|
+
|
|
41
|
+
const USE_POSIX_SPAWN = !!process.env.BASECAMP_BUNDLE;
|
|
42
|
+
let posixSpawn;
|
|
43
|
+
if (USE_POSIX_SPAWN) {
|
|
44
|
+
try {
|
|
45
|
+
posixSpawn = await import("./posix-spawn.js");
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(
|
|
48
|
+
"Failed to load posix-spawn, falling back to child_process:",
|
|
49
|
+
err.message,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
let daemonStartedAt = null;
|
|
41
55
|
|
|
42
56
|
// --- Helpers ----------------------------------------------------------------
|
|
@@ -79,23 +93,36 @@ function log(msg) {
|
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
function findClaude() {
|
|
82
|
-
|
|
83
|
-
"claude",
|
|
96
|
+
const paths = [
|
|
84
97
|
"/usr/local/bin/claude",
|
|
85
98
|
join(HOME, ".claude", "bin", "claude"),
|
|
86
99
|
join(HOME, ".local", "bin", "claude"),
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
encoding: "utf8",
|
|
91
|
-
});
|
|
92
|
-
return c;
|
|
93
|
-
} catch {}
|
|
94
|
-
if (existsSync(c)) return c;
|
|
95
|
-
}
|
|
100
|
+
"/opt/homebrew/bin/claude",
|
|
101
|
+
];
|
|
102
|
+
for (const p of paths) if (existsSync(p)) return p;
|
|
96
103
|
return "claude";
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Detect if running from inside a macOS .app bundle.
|
|
108
|
+
* The binary is at Basecamp.app/Contents/MacOS/fit-basecamp.
|
|
109
|
+
* @returns {{ bundle: string, resources: string } | null}
|
|
110
|
+
*/
|
|
111
|
+
function getBundlePath() {
|
|
112
|
+
try {
|
|
113
|
+
const exe = process.execPath || "";
|
|
114
|
+
const macosDir = dirname(exe);
|
|
115
|
+
const contentsDir = dirname(macosDir);
|
|
116
|
+
const resourcesDir = join(contentsDir, "Resources");
|
|
117
|
+
if (existsSync(join(resourcesDir, "config"))) {
|
|
118
|
+
return { bundle: dirname(contentsDir), resources: resourcesDir };
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
/* not in bundle */
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
99
126
|
function loadConfig() {
|
|
100
127
|
return readJSON(CONFIG_PATH, { tasks: {} });
|
|
101
128
|
}
|
|
@@ -172,7 +199,7 @@ function shouldRun(task, taskState, now) {
|
|
|
172
199
|
|
|
173
200
|
// --- Task execution ---------------------------------------------------------
|
|
174
201
|
|
|
175
|
-
function runTask(taskName, task, _config, state) {
|
|
202
|
+
async function runTask(taskName, task, _config, state) {
|
|
176
203
|
if (!task.kb) {
|
|
177
204
|
log(`Task ${taskName}: no "kb" specified, skipping.`);
|
|
178
205
|
return;
|
|
@@ -201,6 +228,58 @@ function runTask(taskName, task, _config, state) {
|
|
|
201
228
|
if (task.agent) spawnArgs.push("--agent", task.agent);
|
|
202
229
|
spawnArgs.push("-p", prompt);
|
|
203
230
|
|
|
231
|
+
// Use posix_spawn when running inside the app bundle for TCC inheritance.
|
|
232
|
+
// Fall back to child_process.spawn for dev mode and other platforms.
|
|
233
|
+
if (posixSpawn) {
|
|
234
|
+
try {
|
|
235
|
+
const { pid, stdoutFd, stderrFd } = posixSpawn.spawn(
|
|
236
|
+
claude,
|
|
237
|
+
spawnArgs,
|
|
238
|
+
undefined,
|
|
239
|
+
kbPath,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Read stdout and stderr concurrently to avoid pipe deadlocks,
|
|
243
|
+
// then wait for the child to exit.
|
|
244
|
+
const [stdout, stderr] = await Promise.all([
|
|
245
|
+
posixSpawn.readAll(stdoutFd),
|
|
246
|
+
posixSpawn.readAll(stderrFd),
|
|
247
|
+
]);
|
|
248
|
+
const exitCode = await posixSpawn.waitForExit(pid);
|
|
249
|
+
|
|
250
|
+
if (exitCode === 0) {
|
|
251
|
+
log(`Task ${taskName} completed. Output: ${stdout.slice(0, 200)}...`);
|
|
252
|
+
Object.assign(ts, {
|
|
253
|
+
status: "finished",
|
|
254
|
+
startedAt: null,
|
|
255
|
+
lastRunAt: new Date().toISOString(),
|
|
256
|
+
lastError: null,
|
|
257
|
+
runCount: (ts.runCount || 0) + 1,
|
|
258
|
+
});
|
|
259
|
+
} else {
|
|
260
|
+
const errMsg = stderr || stdout || `Exit code ${exitCode}`;
|
|
261
|
+
log(`Task ${taskName} failed: ${errMsg.slice(0, 300)}`);
|
|
262
|
+
Object.assign(ts, {
|
|
263
|
+
status: "failed",
|
|
264
|
+
startedAt: null,
|
|
265
|
+
lastRunAt: new Date().toISOString(),
|
|
266
|
+
lastError: errMsg.slice(0, 500),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
saveState(state);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
log(`Task ${taskName} failed: ${err.message}`);
|
|
272
|
+
Object.assign(ts, {
|
|
273
|
+
status: "failed",
|
|
274
|
+
startedAt: null,
|
|
275
|
+
lastRunAt: new Date().toISOString(),
|
|
276
|
+
lastError: err.message.slice(0, 500),
|
|
277
|
+
});
|
|
278
|
+
saveState(state);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
204
283
|
return new Promise((resolve) => {
|
|
205
284
|
const child = spawn(claude, spawnArgs, {
|
|
206
285
|
cwd: kbPath,
|
|
@@ -337,29 +416,6 @@ function handleStatusRequest(socket) {
|
|
|
337
416
|
});
|
|
338
417
|
}
|
|
339
418
|
|
|
340
|
-
function handleRestartRequest(socket) {
|
|
341
|
-
send(socket, { type: "ack", command: "restart" });
|
|
342
|
-
setTimeout(() => process.exit(0), 100);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function handleRunRequest(socket, taskName) {
|
|
346
|
-
if (!taskName) {
|
|
347
|
-
send(socket, { type: "error", message: "Missing task name" });
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const config = loadConfig();
|
|
351
|
-
const task = config.tasks[taskName];
|
|
352
|
-
if (!task) {
|
|
353
|
-
send(socket, { type: "error", message: `Task not found: ${taskName}` });
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
send(socket, { type: "ack", command: "run", task: taskName });
|
|
357
|
-
const state = loadState();
|
|
358
|
-
runTask(taskName, task, config, state).catch((err) => {
|
|
359
|
-
console.error(`[socket] runTask error for ${taskName}:`, err.message);
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
|
|
363
419
|
function handleMessage(socket, line) {
|
|
364
420
|
let request;
|
|
365
421
|
try {
|
|
@@ -369,21 +425,32 @@ function handleMessage(socket, line) {
|
|
|
369
425
|
return;
|
|
370
426
|
}
|
|
371
427
|
|
|
372
|
-
|
|
373
|
-
status: () => handleStatusRequest(socket),
|
|
374
|
-
restart: () => handleRestartRequest(socket),
|
|
375
|
-
run: () => handleRunRequest(socket, request.task),
|
|
376
|
-
};
|
|
428
|
+
if (request.type === "status") return handleStatusRequest(socket);
|
|
377
429
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
430
|
+
if (request.type === "run") {
|
|
431
|
+
if (!request.task) {
|
|
432
|
+
send(socket, { type: "error", message: "Missing task name" });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const config = loadConfig();
|
|
436
|
+
const task = config.tasks[request.task];
|
|
437
|
+
if (!task) {
|
|
438
|
+
send(socket, {
|
|
439
|
+
type: "error",
|
|
440
|
+
message: `Task not found: ${request.task}`,
|
|
441
|
+
});
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
send(socket, { type: "ack", command: "run", task: request.task });
|
|
445
|
+
const state = loadState();
|
|
446
|
+
runTask(request.task, task, config, state).catch(() => {});
|
|
447
|
+
return;
|
|
386
448
|
}
|
|
449
|
+
|
|
450
|
+
send(socket, {
|
|
451
|
+
type: "error",
|
|
452
|
+
message: `Unknown request type: ${request.type}`,
|
|
453
|
+
});
|
|
387
454
|
}
|
|
388
455
|
|
|
389
456
|
function startSocketServer() {
|
|
@@ -416,9 +483,6 @@ function startSocketServer() {
|
|
|
416
483
|
|
|
417
484
|
const cleanup = () => {
|
|
418
485
|
server.close();
|
|
419
|
-
try {
|
|
420
|
-
unlinkSync(SOCKET_PATH);
|
|
421
|
-
} catch {}
|
|
422
486
|
process.exit(0);
|
|
423
487
|
};
|
|
424
488
|
process.on("SIGTERM", cleanup);
|
|
@@ -434,7 +498,7 @@ function daemon() {
|
|
|
434
498
|
log("Scheduler daemon started. Polling every 60 seconds.");
|
|
435
499
|
log(`Config: ${CONFIG_PATH} State: ${STATE_PATH}`);
|
|
436
500
|
startSocketServer();
|
|
437
|
-
runDueTasks();
|
|
501
|
+
runDueTasks().catch((err) => log(`Error: ${err.message}`));
|
|
438
502
|
setInterval(async () => {
|
|
439
503
|
try {
|
|
440
504
|
await runDueTasks();
|
|
@@ -446,17 +510,18 @@ function daemon() {
|
|
|
446
510
|
|
|
447
511
|
// --- Init knowledge base ----------------------------------------------------
|
|
448
512
|
|
|
449
|
-
function
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (
|
|
454
|
-
ensureDir(d);
|
|
455
|
-
copyDirRecursive(s, d);
|
|
456
|
-
} else if (!existsSync(d)) {
|
|
457
|
-
writeFileSync(d, readFileSync(s));
|
|
458
|
-
}
|
|
513
|
+
function findTemplateDir() {
|
|
514
|
+
const bundle = getBundlePath();
|
|
515
|
+
if (bundle) {
|
|
516
|
+
const tpl = join(bundle.resources, "template");
|
|
517
|
+
if (existsSync(tpl)) return tpl;
|
|
459
518
|
}
|
|
519
|
+
for (const d of [
|
|
520
|
+
join(SHARE_DIR, "template"),
|
|
521
|
+
join(__dirname, "..", "template"),
|
|
522
|
+
])
|
|
523
|
+
if (existsSync(d)) return d;
|
|
524
|
+
return null;
|
|
460
525
|
}
|
|
461
526
|
|
|
462
527
|
function initKB(targetPath) {
|
|
@@ -465,6 +530,11 @@ function initKB(targetPath) {
|
|
|
465
530
|
console.error(`Knowledge base already exists at ${dest}`);
|
|
466
531
|
process.exit(1);
|
|
467
532
|
}
|
|
533
|
+
const tpl = findTemplateDir();
|
|
534
|
+
if (!tpl) {
|
|
535
|
+
console.error("Template not found. Reinstall fit-basecamp.");
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
468
538
|
|
|
469
539
|
ensureDir(dest);
|
|
470
540
|
for (const d of [
|
|
@@ -472,11 +542,10 @@ function initKB(targetPath) {
|
|
|
472
542
|
"knowledge/Organizations",
|
|
473
543
|
"knowledge/Projects",
|
|
474
544
|
"knowledge/Topics",
|
|
475
|
-
".claude/skills",
|
|
476
545
|
])
|
|
477
546
|
ensureDir(join(dest, d));
|
|
478
547
|
|
|
479
|
-
|
|
548
|
+
execSync(`cp -R "${tpl}/." "${dest}/"`);
|
|
480
549
|
|
|
481
550
|
console.log(
|
|
482
551
|
`Knowledge base initialized at ${dest}\n\nNext steps:\n 1. Edit ${dest}/USER.md with your name, email, and domain\n 2. cd ${dest} && claude`,
|
|
@@ -492,26 +561,23 @@ function showStatus() {
|
|
|
492
561
|
|
|
493
562
|
const tasks = Object.entries(config.tasks || {});
|
|
494
563
|
if (tasks.length === 0) {
|
|
495
|
-
console.log(
|
|
496
|
-
`Tasks: (none configured)\n\nEdit ${CONFIG_PATH} to add tasks.`,
|
|
497
|
-
);
|
|
564
|
+
console.log(`No tasks configured.\n\nEdit ${CONFIG_PATH} to add tasks.`);
|
|
498
565
|
return;
|
|
499
566
|
}
|
|
500
567
|
|
|
501
568
|
console.log("Tasks:");
|
|
502
569
|
for (const [name, task] of tasks) {
|
|
503
570
|
const s = state.tasks[name] || {};
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
` ${task.enabled !== false ? "+" : "-"} ${name}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
console.log(lines.join("\n"));
|
|
571
|
+
const kbStatus =
|
|
572
|
+
task.kb && !existsSync(expandPath(task.kb)) ? " (not found)" : "";
|
|
573
|
+
console.log(
|
|
574
|
+
` ${task.enabled !== false ? "+" : "-"} ${name}\n` +
|
|
575
|
+
` KB: ${task.kb || "(none)"}${kbStatus} Schedule: ${JSON.stringify(task.schedule)}\n` +
|
|
576
|
+
` Status: ${s.status || "never-run"} Last: ${s.lastRunAt ? new Date(s.lastRunAt).toLocaleString() : "never"} Runs: ${s.runCount || 0}` +
|
|
577
|
+
(task.agent ? `\n Agent: ${task.agent}` : "") +
|
|
578
|
+
(task.skill ? `\n Skill: ${task.skill}` : "") +
|
|
579
|
+
(s.lastError ? `\n Error: ${s.lastError.slice(0, 80)}` : ""),
|
|
580
|
+
);
|
|
515
581
|
}
|
|
516
582
|
}
|
|
517
583
|
|
|
@@ -544,7 +610,7 @@ function validate() {
|
|
|
544
610
|
}
|
|
545
611
|
const kbPath = expandPath(task.kb);
|
|
546
612
|
if (!existsSync(kbPath)) {
|
|
547
|
-
console.log(` [FAIL] ${name}: path
|
|
613
|
+
console.log(` [FAIL] ${name}: path not found: ${kbPath}`);
|
|
548
614
|
errors++;
|
|
549
615
|
continue;
|
|
550
616
|
}
|
|
@@ -559,25 +625,17 @@ function validate() {
|
|
|
559
625
|
? join("agents", sub.endsWith(".md") ? sub : sub + ".md")
|
|
560
626
|
: join("skills", sub, "SKILL.md");
|
|
561
627
|
const found = findInLocalOrGlobal(kbPath, relPath);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
` [FAIL] ${name}: ${kind} "${sub}" not found in ${join(kbPath, ".claude", relPath)} or ${join(HOME, ".claude", relPath)}`,
|
|
567
|
-
);
|
|
568
|
-
errors++;
|
|
569
|
-
}
|
|
628
|
+
console.log(
|
|
629
|
+
` [${found ? "OK" : "FAIL"}] ${name}: ${kind} "${sub}"${found ? "" : " not found"}`,
|
|
630
|
+
);
|
|
631
|
+
if (!found) errors++;
|
|
570
632
|
}
|
|
571
633
|
|
|
572
634
|
if (!task.agent && !task.skill)
|
|
573
635
|
console.log(` [OK] ${name}: no agent or skill to validate`);
|
|
574
636
|
}
|
|
575
637
|
|
|
576
|
-
console.log(
|
|
577
|
-
errors > 0
|
|
578
|
-
? `\nValidation failed: ${errors} error(s) found.`
|
|
579
|
-
: "\nAll tasks validated successfully.",
|
|
580
|
-
);
|
|
638
|
+
console.log(errors > 0 ? `\n${errors} error(s).` : "\nAll OK.");
|
|
581
639
|
if (errors > 0) process.exit(1);
|
|
582
640
|
}
|
|
583
641
|
|
|
@@ -586,7 +644,7 @@ function validate() {
|
|
|
586
644
|
function showHelp() {
|
|
587
645
|
const bin = "fit-basecamp";
|
|
588
646
|
console.log(`
|
|
589
|
-
Basecamp
|
|
647
|
+
Basecamp — Run scheduled Claude tasks across knowledge bases.
|
|
590
648
|
|
|
591
649
|
Usage:
|
|
592
650
|
${bin} Run due tasks once and exit
|
|
@@ -595,28 +653,10 @@ Usage:
|
|
|
595
653
|
${bin} --init <path> Initialize a new knowledge base
|
|
596
654
|
${bin} --validate Validate agents and skills exist
|
|
597
655
|
${bin} --status Show task status
|
|
598
|
-
${bin} --help Show this help
|
|
599
656
|
|
|
600
657
|
Config: ~/.fit/basecamp/scheduler.json
|
|
601
658
|
State: ~/.fit/basecamp/state.json
|
|
602
659
|
Logs: ~/.fit/basecamp/logs/
|
|
603
|
-
|
|
604
|
-
Config format:
|
|
605
|
-
{
|
|
606
|
-
"tasks": {
|
|
607
|
-
"sync-mail": {
|
|
608
|
-
"kb": "~/Documents/Personal",
|
|
609
|
-
"schedule": { "type": "interval", "minutes": 5 },
|
|
610
|
-
"prompt": "Sync Apple Mail.", "skill": "sync-apple-mail",
|
|
611
|
-
"agent": null, "enabled": true
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
Schedule types:
|
|
617
|
-
interval: { "type": "interval", "minutes": 5 }
|
|
618
|
-
cron: { "type": "cron", "expression": "0 8 * * *" }
|
|
619
|
-
once: { "type": "once", "runAt": "2025-02-12T10:00:00Z" }
|
|
620
660
|
`);
|
|
621
661
|
}
|
|
622
662
|
|
|
@@ -1,10 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
3
|
"permissions": {
|
|
4
|
+
"defaultMode": "acceptEdits",
|
|
4
5
|
"allow": [
|
|
6
|
+
"Bash(python3 *)",
|
|
7
|
+
"Bash(sqlite3 *)",
|
|
8
|
+
"Bash(bash scripts/*)",
|
|
9
|
+
"Bash(sh scripts/*)",
|
|
10
|
+
"Bash(node *)",
|
|
11
|
+
"Bash(npm install *)",
|
|
12
|
+
"Bash(npx *)",
|
|
13
|
+
"Bash(rg *)",
|
|
14
|
+
"Bash(find *)",
|
|
15
|
+
"Bash(cat *)",
|
|
16
|
+
"Bash(head *)",
|
|
17
|
+
"Bash(tail *)",
|
|
18
|
+
"Bash(ls *)",
|
|
19
|
+
"Bash(ls)",
|
|
20
|
+
"Bash(grep *)",
|
|
21
|
+
"Bash(echo *)",
|
|
22
|
+
"Bash(date *)",
|
|
23
|
+
"Bash(mkdir *)",
|
|
24
|
+
"Bash(mv *)",
|
|
25
|
+
"Bash(cp *)",
|
|
26
|
+
"Bash(touch *)",
|
|
27
|
+
"Bash(sort *)",
|
|
28
|
+
"Bash(wc *)",
|
|
29
|
+
"Bash(cut *)",
|
|
30
|
+
"Bash(tr *)",
|
|
31
|
+
"Bash(uniq *)",
|
|
32
|
+
"Bash(awk *)",
|
|
33
|
+
"Bash(sed *)",
|
|
34
|
+
"Bash(basename *)",
|
|
35
|
+
"Bash(dirname *)",
|
|
36
|
+
"Bash(realpath *)",
|
|
37
|
+
"Bash(pwd)",
|
|
38
|
+
"Bash(which *)",
|
|
39
|
+
"Bash(textutil *)",
|
|
40
|
+
"Bash(pdftotext *)",
|
|
5
41
|
"Read(~/Library/Mail/**)",
|
|
6
42
|
"Read(~/Library/Group Containers/group.com.apple.calendar/**)",
|
|
7
43
|
"Read(~/Library/Calendars/**)",
|
|
44
|
+
"Read(~/Library/Application Support/hyprnote/**)",
|
|
8
45
|
"Read(~/Desktop/**)",
|
|
9
46
|
"Read(~/Documents/**)",
|
|
10
47
|
"Read(~/Downloads/**)",
|
|
@@ -15,6 +52,17 @@
|
|
|
15
52
|
"Edit(~/.cache/fit/basecamp/**)"
|
|
16
53
|
],
|
|
17
54
|
"deny": [
|
|
55
|
+
"Bash(curl *)",
|
|
56
|
+
"Bash(wget *)",
|
|
57
|
+
"Bash(open *)",
|
|
58
|
+
"Bash(osascript *)",
|
|
59
|
+
"Bash(sudo *)",
|
|
60
|
+
"Bash(rm -rf *)",
|
|
61
|
+
"Bash(chmod *)",
|
|
62
|
+
"Bash(chown *)",
|
|
63
|
+
"Bash(killall *)",
|
|
64
|
+
"Bash(launchctl *)",
|
|
65
|
+
"Bash(brew *)",
|
|
18
66
|
"Edit(~/Library/**)",
|
|
19
67
|
"Read(~/Pictures/**)",
|
|
20
68
|
"Read(~/Music/**)",
|
|
@@ -31,6 +79,7 @@
|
|
|
31
79
|
"~/Library/Mail",
|
|
32
80
|
"~/Library/Group Containers/group.com.apple.calendar",
|
|
33
81
|
"~/Library/Calendars",
|
|
82
|
+
"~/Library/Application Support/hyprnote",
|
|
34
83
|
"~/Desktop",
|
|
35
84
|
"~/Documents",
|
|
36
85
|
"~/Downloads",
|
|
@@ -16,13 +16,15 @@ Run when the user asks to draft, reply to, or respond to an email.
|
|
|
16
16
|
## Prerequisites
|
|
17
17
|
|
|
18
18
|
- Knowledge base populated (from `extract-entities` skill)
|
|
19
|
-
- Synced email data in `~/.cache/fit/basecamp/apple_mail/`
|
|
19
|
+
- Synced email data in `~/.cache/fit/basecamp/apple_mail/` or
|
|
20
|
+
`~/.cache/fit/basecamp/gmail/`
|
|
20
21
|
|
|
21
22
|
## Inputs
|
|
22
23
|
|
|
23
24
|
- `knowledge/People/*.md` — person context
|
|
24
25
|
- `knowledge/Organizations/*.md` — organization context
|
|
25
|
-
- `~/.cache/fit/basecamp/apple_mail/*.md`
|
|
26
|
+
- `~/.cache/fit/basecamp/apple_mail/*.md` or `~/.cache/fit/basecamp/gmail/*.md`
|
|
27
|
+
— email threads
|
|
26
28
|
- `~/.cache/fit/basecamp/apple_calendar/*.json` — calendar events (for
|
|
27
29
|
scheduling)
|
|
28
30
|
- `drafts/last_processed` — timestamp of last processing run
|
|
@@ -116,7 +118,7 @@ cat "knowledge/Organizations/Company Name.md"
|
|
|
116
118
|
**Calendar (for scheduling emails):**
|
|
117
119
|
|
|
118
120
|
```bash
|
|
119
|
-
ls ~/.cache/fit/basecamp/apple_calendar/ 2>/dev/null
|
|
121
|
+
ls ~/.cache/fit/basecamp/apple_calendar/ ~/.cache/fit/basecamp/google_calendar/ 2>/dev/null
|
|
120
122
|
cat "$HOME/.cache/fit/basecamp/apple_calendar/event123.json"
|
|
121
123
|
```
|
|
122
124
|
|
|
@@ -184,6 +186,33 @@ date -u '+%Y-%m-%dT%H:%M:%SZ' > drafts/last_processed
|
|
|
184
186
|
- {id}: {subject} — {reason}
|
|
185
187
|
```
|
|
186
188
|
|
|
189
|
+
## Recruitment & Staffing Emails
|
|
190
|
+
|
|
191
|
+
**CRITICAL: Candidates must NEVER be copied on internal emails about them.**
|
|
192
|
+
|
|
193
|
+
When an email involves recruitment, staffing, or hiring:
|
|
194
|
+
|
|
195
|
+
1. **Identify the candidate** — Determine who the candidate is from the email
|
|
196
|
+
thread and knowledge base (`knowledge/Candidates/`, `knowledge/People/`)
|
|
197
|
+
2. **Strip the candidate from recipients** — The draft must ONLY be addressed to
|
|
198
|
+
internal stakeholders (hiring managers, recruiters, interview panel, etc.).
|
|
199
|
+
The candidate’s email address must NOT appear in To, CC, or BCC
|
|
200
|
+
3. **Only recruiters email candidates directly** — If the email is a direct
|
|
201
|
+
reply TO a candidate (e.g., scheduling an interview, extending an offer),
|
|
202
|
+
flag it clearly so only the recruiter sends it. Add a note:
|
|
203
|
+
`⚠️ RECRUITER ONLY — This email goes directly to the candidate.`
|
|
204
|
+
|
|
205
|
+
**Examples of internal recruitment emails (candidate must NOT be copied):**
|
|
206
|
+
|
|
207
|
+
- Interview feedback or debrief
|
|
208
|
+
- Candidate evaluation or comparison
|
|
209
|
+
- Hiring decision discussions
|
|
210
|
+
- Compensation/offer discussions
|
|
211
|
+
- Reference check follow-ups between colleagues
|
|
212
|
+
|
|
213
|
+
**When in doubt:** If an email thread mentions a candidate by name and involves
|
|
214
|
+
multiple internal recipients, treat it as internal and exclude the candidate.
|
|
215
|
+
|
|
187
216
|
## Constraints
|
|
188
217
|
|
|
189
218
|
- Never actually send emails — only create drafts
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Usage: bash scripts/scan-emails.sh
|
|
5
5
|
#
|
|
6
|
-
# Checks ~/.cache/fit/basecamp/apple_mail/ for email files
|
|
7
|
-
#
|
|
6
|
+
# Checks ~/.cache/fit/basecamp/apple_mail/ for email files not yet
|
|
7
|
+
# listed in drafts/drafted or drafts/ignored.
|
|
8
8
|
# Outputs tab-separated: email_id<TAB>subject
|
|
9
9
|
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
|
|
12
12
|
MAIL_DIRS=(
|
|
13
13
|
"$HOME/.cache/fit/basecamp/apple_mail"
|
|
14
|
+
"$HOME/.cache/fit/basecamp/gmail"
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
for dir in "${MAIL_DIRS[@]}"; do
|
|
@@ -264,7 +264,6 @@ person?"** test:
|
|
|
264
264
|
- Transactional service providers (bank employees, support reps)
|
|
265
265
|
- One-time administrative contacts
|
|
266
266
|
- Large group meeting attendees you didn't interact with
|
|
267
|
-
- Internal colleagues (@user.domain)
|
|
268
267
|
- Assistants handling only logistics
|
|
269
268
|
|
|
270
269
|
For people who don't get their own note, add to the Organization note's
|
|
@@ -10,6 +10,7 @@ Templates for creating new knowledge base notes.
|
|
|
10
10
|
## Info
|
|
11
11
|
**Role:** {role or inferred role with qualifier}
|
|
12
12
|
**Organization:** [[Organizations/{organization}]]
|
|
13
|
+
**Reports to:** [[People/{{Person}}]]
|
|
13
14
|
**Email:** {email}
|
|
14
15
|
**Aliases:** {comma-separated variants}
|
|
15
16
|
**First met:** {YYYY-MM-DD}
|