@forwardimpact/basecamp 0.2.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 -89
- package/package.json +9 -14
- package/{basecamp.js → src/basecamp.js} +153 -204
- 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
- package/scripts/build-pkg.sh +0 -115
- package/scripts/compile.sh +0 -25
- package/scripts/install.sh +0 -108
- package/scripts/pkg-resources/conclusion.html +0 -62
- package/scripts/pkg-resources/welcome.html +0 -64
- package/scripts/postinstall +0 -84
- package/scripts/uninstall.sh +0 -73
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
// node basecamp.js --daemon Run continuously (poll every 60s)
|
|
8
8
|
// node basecamp.js --run <task> Run a specific task immediately
|
|
9
9
|
// node basecamp.js --init <path> Initialize a new knowledge base
|
|
10
|
-
// node basecamp.js --install-launchd Install macOS LaunchAgent
|
|
11
|
-
// node basecamp.js --uninstall-launchd Remove macOS LaunchAgent
|
|
12
10
|
// node basecamp.js --validate Validate agents and skills exist
|
|
13
11
|
// node basecamp.js --status Show task status
|
|
14
12
|
// node basecamp.js --help Show this help
|
|
@@ -18,7 +16,6 @@ import {
|
|
|
18
16
|
writeFileSync,
|
|
19
17
|
existsSync,
|
|
20
18
|
mkdirSync,
|
|
21
|
-
readdirSync,
|
|
22
19
|
unlinkSync,
|
|
23
20
|
chmodSync,
|
|
24
21
|
} from "node:fs";
|
|
@@ -34,16 +31,25 @@ const BASECAMP_HOME = join(HOME, ".fit", "basecamp");
|
|
|
34
31
|
const CONFIG_PATH = join(BASECAMP_HOME, "scheduler.json");
|
|
35
32
|
const STATE_PATH = join(BASECAMP_HOME, "state.json");
|
|
36
33
|
const LOG_DIR = join(BASECAMP_HOME, "logs");
|
|
37
|
-
const PLIST_NAME = "com.fit-basecamp.scheduler";
|
|
38
|
-
const PLIST_PATH = join(HOME, "Library", "LaunchAgents", `${PLIST_NAME}.plist`);
|
|
39
34
|
const __dirname =
|
|
40
35
|
import.meta.dirname || dirname(fileURLToPath(import.meta.url));
|
|
41
|
-
const
|
|
36
|
+
const SHARE_DIR = "/usr/local/share/fit-basecamp";
|
|
42
37
|
const SOCKET_PATH = join(BASECAMP_HOME, "basecamp.sock");
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
}
|
|
47
53
|
|
|
48
54
|
let daemonStartedAt = null;
|
|
49
55
|
|
|
@@ -87,23 +93,36 @@ function log(msg) {
|
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
function findClaude() {
|
|
90
|
-
|
|
91
|
-
"claude",
|
|
96
|
+
const paths = [
|
|
92
97
|
"/usr/local/bin/claude",
|
|
93
98
|
join(HOME, ".claude", "bin", "claude"),
|
|
94
99
|
join(HOME, ".local", "bin", "claude"),
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
encoding: "utf8",
|
|
99
|
-
});
|
|
100
|
-
return c;
|
|
101
|
-
} catch {}
|
|
102
|
-
if (existsSync(c)) return c;
|
|
103
|
-
}
|
|
100
|
+
"/opt/homebrew/bin/claude",
|
|
101
|
+
];
|
|
102
|
+
for (const p of paths) if (existsSync(p)) return p;
|
|
104
103
|
return "claude";
|
|
105
104
|
}
|
|
106
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
|
+
|
|
107
126
|
function loadConfig() {
|
|
108
127
|
return readJSON(CONFIG_PATH, { tasks: {} });
|
|
109
128
|
}
|
|
@@ -180,7 +199,7 @@ function shouldRun(task, taskState, now) {
|
|
|
180
199
|
|
|
181
200
|
// --- Task execution ---------------------------------------------------------
|
|
182
201
|
|
|
183
|
-
function runTask(taskName, task, _config, state) {
|
|
202
|
+
async function runTask(taskName, task, _config, state) {
|
|
184
203
|
if (!task.kb) {
|
|
185
204
|
log(`Task ${taskName}: no "kb" specified, skipping.`);
|
|
186
205
|
return;
|
|
@@ -209,6 +228,58 @@ function runTask(taskName, task, _config, state) {
|
|
|
209
228
|
if (task.agent) spawnArgs.push("--agent", task.agent);
|
|
210
229
|
spawnArgs.push("-p", prompt);
|
|
211
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
|
+
|
|
212
283
|
return new Promise((resolve) => {
|
|
213
284
|
const child = spawn(claude, spawnArgs, {
|
|
214
285
|
cwd: kbPath,
|
|
@@ -345,36 +416,6 @@ function handleStatusRequest(socket) {
|
|
|
345
416
|
});
|
|
346
417
|
}
|
|
347
418
|
|
|
348
|
-
function handleRestartRequest(socket) {
|
|
349
|
-
send(socket, { type: "ack", command: "restart" });
|
|
350
|
-
const uid = execSync("id -u", { encoding: "utf8" }).trim();
|
|
351
|
-
setTimeout(() => {
|
|
352
|
-
try {
|
|
353
|
-
execSync(`launchctl kickstart -k gui/${uid}/${PLIST_NAME}`);
|
|
354
|
-
} catch {
|
|
355
|
-
process.exit(0);
|
|
356
|
-
}
|
|
357
|
-
}, 100);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function handleRunRequest(socket, taskName) {
|
|
361
|
-
if (!taskName) {
|
|
362
|
-
send(socket, { type: "error", message: "Missing task name" });
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
const config = loadConfig();
|
|
366
|
-
const task = config.tasks[taskName];
|
|
367
|
-
if (!task) {
|
|
368
|
-
send(socket, { type: "error", message: `Task not found: ${taskName}` });
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
send(socket, { type: "ack", command: "run", task: taskName });
|
|
372
|
-
const state = loadState();
|
|
373
|
-
runTask(taskName, task, config, state).catch((err) => {
|
|
374
|
-
console.error(`[socket] runTask error for ${taskName}:`, err.message);
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
|
|
378
419
|
function handleMessage(socket, line) {
|
|
379
420
|
let request;
|
|
380
421
|
try {
|
|
@@ -384,21 +425,32 @@ function handleMessage(socket, line) {
|
|
|
384
425
|
return;
|
|
385
426
|
}
|
|
386
427
|
|
|
387
|
-
|
|
388
|
-
status: () => handleStatusRequest(socket),
|
|
389
|
-
restart: () => handleRestartRequest(socket),
|
|
390
|
-
run: () => handleRunRequest(socket, request.task),
|
|
391
|
-
};
|
|
428
|
+
if (request.type === "status") return handleStatusRequest(socket);
|
|
392
429
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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;
|
|
401
448
|
}
|
|
449
|
+
|
|
450
|
+
send(socket, {
|
|
451
|
+
type: "error",
|
|
452
|
+
message: `Unknown request type: ${request.type}`,
|
|
453
|
+
});
|
|
402
454
|
}
|
|
403
455
|
|
|
404
456
|
function startSocketServer() {
|
|
@@ -431,9 +483,6 @@ function startSocketServer() {
|
|
|
431
483
|
|
|
432
484
|
const cleanup = () => {
|
|
433
485
|
server.close();
|
|
434
|
-
try {
|
|
435
|
-
unlinkSync(SOCKET_PATH);
|
|
436
|
-
} catch {}
|
|
437
486
|
process.exit(0);
|
|
438
487
|
};
|
|
439
488
|
process.on("SIGTERM", cleanup);
|
|
@@ -449,7 +498,7 @@ function daemon() {
|
|
|
449
498
|
log("Scheduler daemon started. Polling every 60 seconds.");
|
|
450
499
|
log(`Config: ${CONFIG_PATH} State: ${STATE_PATH}`);
|
|
451
500
|
startSocketServer();
|
|
452
|
-
runDueTasks();
|
|
501
|
+
runDueTasks().catch((err) => log(`Error: ${err.message}`));
|
|
453
502
|
setInterval(async () => {
|
|
454
503
|
try {
|
|
455
504
|
await runDueTasks();
|
|
@@ -461,17 +510,18 @@ function daemon() {
|
|
|
461
510
|
|
|
462
511
|
// --- Init knowledge base ----------------------------------------------------
|
|
463
512
|
|
|
464
|
-
function
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (
|
|
469
|
-
ensureDir(d);
|
|
470
|
-
copyDirRecursive(s, d);
|
|
471
|
-
} else if (!existsSync(d)) {
|
|
472
|
-
writeFileSync(d, readFileSync(s));
|
|
473
|
-
}
|
|
513
|
+
function findTemplateDir() {
|
|
514
|
+
const bundle = getBundlePath();
|
|
515
|
+
if (bundle) {
|
|
516
|
+
const tpl = join(bundle.resources, "template");
|
|
517
|
+
if (existsSync(tpl)) return tpl;
|
|
474
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;
|
|
475
525
|
}
|
|
476
526
|
|
|
477
527
|
function initKB(targetPath) {
|
|
@@ -480,6 +530,11 @@ function initKB(targetPath) {
|
|
|
480
530
|
console.error(`Knowledge base already exists at ${dest}`);
|
|
481
531
|
process.exit(1);
|
|
482
532
|
}
|
|
533
|
+
const tpl = findTemplateDir();
|
|
534
|
+
if (!tpl) {
|
|
535
|
+
console.error("Template not found. Reinstall fit-basecamp.");
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
483
538
|
|
|
484
539
|
ensureDir(dest);
|
|
485
540
|
for (const d of [
|
|
@@ -487,80 +542,16 @@ function initKB(targetPath) {
|
|
|
487
542
|
"knowledge/Organizations",
|
|
488
543
|
"knowledge/Projects",
|
|
489
544
|
"knowledge/Topics",
|
|
490
|
-
".claude/skills",
|
|
491
545
|
])
|
|
492
546
|
ensureDir(join(dest, d));
|
|
493
547
|
|
|
494
|
-
|
|
548
|
+
execSync(`cp -R "${tpl}/." "${dest}/"`);
|
|
495
549
|
|
|
496
550
|
console.log(
|
|
497
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`,
|
|
498
552
|
);
|
|
499
553
|
}
|
|
500
554
|
|
|
501
|
-
// --- LaunchAgent ------------------------------------------------------------
|
|
502
|
-
|
|
503
|
-
function installLaunchd() {
|
|
504
|
-
const execPath =
|
|
505
|
-
typeof Deno !== "undefined" ? Deno.execPath() : process.execPath;
|
|
506
|
-
const isCompiled = IS_COMPILED || !execPath.includes("node");
|
|
507
|
-
const progArgs = isCompiled
|
|
508
|
-
? ` <string>${execPath}</string>\n <string>--daemon</string>`
|
|
509
|
-
: ` <string>${execPath}</string>\n <string>${join(__dirname, "basecamp.js")}</string>\n <string>--daemon</string>`;
|
|
510
|
-
|
|
511
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
512
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
513
|
-
<plist version="1.0">
|
|
514
|
-
<dict>
|
|
515
|
-
<key>Label</key>
|
|
516
|
-
<string>${PLIST_NAME}</string>
|
|
517
|
-
<key>ProgramArguments</key>
|
|
518
|
-
<array>
|
|
519
|
-
${progArgs}
|
|
520
|
-
</array>
|
|
521
|
-
<key>RunAtLoad</key>
|
|
522
|
-
<true/>
|
|
523
|
-
<key>KeepAlive</key>
|
|
524
|
-
<true/>
|
|
525
|
-
<key>StandardOutPath</key>
|
|
526
|
-
<string>${join(LOG_DIR, "launchd-stdout.log")}</string>
|
|
527
|
-
<key>StandardErrorPath</key>
|
|
528
|
-
<string>${join(LOG_DIR, "launchd-stderr.log")}</string>
|
|
529
|
-
<key>WorkingDirectory</key>
|
|
530
|
-
<string>${BASECAMP_HOME}</string>
|
|
531
|
-
<key>EnvironmentVariables</key>
|
|
532
|
-
<dict>
|
|
533
|
-
<key>PATH</key>
|
|
534
|
-
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${join(HOME, ".local", "bin")}</string>
|
|
535
|
-
</dict>
|
|
536
|
-
</dict>
|
|
537
|
-
</plist>`;
|
|
538
|
-
|
|
539
|
-
ensureDir(dirname(PLIST_PATH));
|
|
540
|
-
ensureDir(LOG_DIR);
|
|
541
|
-
writeFileSync(PLIST_PATH, plist);
|
|
542
|
-
|
|
543
|
-
try {
|
|
544
|
-
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, {
|
|
545
|
-
stdio: "ignore",
|
|
546
|
-
});
|
|
547
|
-
} catch {}
|
|
548
|
-
execSync(`launchctl load "${PLIST_PATH}"`);
|
|
549
|
-
console.log(
|
|
550
|
-
`LaunchAgent installed and loaded.\n Plist: ${PLIST_PATH}\n Logs: ${LOG_DIR}/\n Config: ${CONFIG_PATH}`,
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function uninstallLaunchd() {
|
|
555
|
-
try {
|
|
556
|
-
execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null`);
|
|
557
|
-
} catch {}
|
|
558
|
-
try {
|
|
559
|
-
execSync(`rm -f "${PLIST_PATH}"`);
|
|
560
|
-
} catch {}
|
|
561
|
-
console.log("LaunchAgent uninstalled.");
|
|
562
|
-
}
|
|
563
|
-
|
|
564
555
|
// --- Status -----------------------------------------------------------------
|
|
565
556
|
|
|
566
557
|
function showStatus() {
|
|
@@ -570,35 +561,23 @@ function showStatus() {
|
|
|
570
561
|
|
|
571
562
|
const tasks = Object.entries(config.tasks || {});
|
|
572
563
|
if (tasks.length === 0) {
|
|
573
|
-
console.log(
|
|
574
|
-
`Tasks: (none configured)\n\nEdit ${CONFIG_PATH} to add tasks.`,
|
|
575
|
-
);
|
|
564
|
+
console.log(`No tasks configured.\n\nEdit ${CONFIG_PATH} to add tasks.`);
|
|
576
565
|
return;
|
|
577
566
|
}
|
|
578
567
|
|
|
579
568
|
console.log("Tasks:");
|
|
580
569
|
for (const [name, task] of tasks) {
|
|
581
570
|
const s = state.tasks[name] || {};
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
` ${task.enabled !== false ? "+" : "-"} ${name}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
console.log(lines.join("\n"));
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
try {
|
|
596
|
-
execSync(`launchctl list 2>/dev/null | grep ${PLIST_NAME}`, {
|
|
597
|
-
encoding: "utf8",
|
|
598
|
-
});
|
|
599
|
-
console.log("\nLaunchAgent: loaded");
|
|
600
|
-
} catch {
|
|
601
|
-
console.log("\nLaunchAgent: not loaded (run --install-launchd to start)");
|
|
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
|
+
);
|
|
602
581
|
}
|
|
603
582
|
}
|
|
604
583
|
|
|
@@ -631,7 +610,7 @@ function validate() {
|
|
|
631
610
|
}
|
|
632
611
|
const kbPath = expandPath(task.kb);
|
|
633
612
|
if (!existsSync(kbPath)) {
|
|
634
|
-
console.log(` [FAIL] ${name}: path
|
|
613
|
+
console.log(` [FAIL] ${name}: path not found: ${kbPath}`);
|
|
635
614
|
errors++;
|
|
636
615
|
continue;
|
|
637
616
|
}
|
|
@@ -646,25 +625,17 @@ function validate() {
|
|
|
646
625
|
? join("agents", sub.endsWith(".md") ? sub : sub + ".md")
|
|
647
626
|
: join("skills", sub, "SKILL.md");
|
|
648
627
|
const found = findInLocalOrGlobal(kbPath, relPath);
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
` [FAIL] ${name}: ${kind} "${sub}" not found in ${join(kbPath, ".claude", relPath)} or ${join(HOME, ".claude", relPath)}`,
|
|
654
|
-
);
|
|
655
|
-
errors++;
|
|
656
|
-
}
|
|
628
|
+
console.log(
|
|
629
|
+
` [${found ? "OK" : "FAIL"}] ${name}: ${kind} "${sub}"${found ? "" : " not found"}`,
|
|
630
|
+
);
|
|
631
|
+
if (!found) errors++;
|
|
657
632
|
}
|
|
658
633
|
|
|
659
634
|
if (!task.agent && !task.skill)
|
|
660
635
|
console.log(` [OK] ${name}: no agent or skill to validate`);
|
|
661
636
|
}
|
|
662
637
|
|
|
663
|
-
console.log(
|
|
664
|
-
errors > 0
|
|
665
|
-
? `\nValidation failed: ${errors} error(s) found.`
|
|
666
|
-
: "\nAll tasks validated successfully.",
|
|
667
|
-
);
|
|
638
|
+
console.log(errors > 0 ? `\n${errors} error(s).` : "\nAll OK.");
|
|
668
639
|
if (errors > 0) process.exit(1);
|
|
669
640
|
}
|
|
670
641
|
|
|
@@ -673,39 +644,19 @@ function validate() {
|
|
|
673
644
|
function showHelp() {
|
|
674
645
|
const bin = "fit-basecamp";
|
|
675
646
|
console.log(`
|
|
676
|
-
Basecamp
|
|
647
|
+
Basecamp — Run scheduled Claude tasks across knowledge bases.
|
|
677
648
|
|
|
678
649
|
Usage:
|
|
679
650
|
${bin} Run due tasks once and exit
|
|
680
651
|
${bin} --daemon Run continuously (poll every 60s)
|
|
681
652
|
${bin} --run <task> Run a specific task immediately
|
|
682
653
|
${bin} --init <path> Initialize a new knowledge base
|
|
683
|
-
${bin} --install-launchd Install macOS LaunchAgent for auto-start
|
|
684
|
-
${bin} --uninstall-launchd Remove macOS LaunchAgent
|
|
685
654
|
${bin} --validate Validate agents and skills exist
|
|
686
655
|
${bin} --status Show task status
|
|
687
|
-
${bin} --help Show this help
|
|
688
656
|
|
|
689
657
|
Config: ~/.fit/basecamp/scheduler.json
|
|
690
658
|
State: ~/.fit/basecamp/state.json
|
|
691
659
|
Logs: ~/.fit/basecamp/logs/
|
|
692
|
-
|
|
693
|
-
Config format:
|
|
694
|
-
{
|
|
695
|
-
"tasks": {
|
|
696
|
-
"sync-mail": {
|
|
697
|
-
"kb": "~/Documents/Personal",
|
|
698
|
-
"schedule": { "type": "interval", "minutes": 5 },
|
|
699
|
-
"prompt": "Sync Apple Mail.", "skill": "sync-apple-mail",
|
|
700
|
-
"agent": null, "enabled": true
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
Schedule types:
|
|
706
|
-
interval: { "type": "interval", "minutes": 5 }
|
|
707
|
-
cron: { "type": "cron", "expression": "0 8 * * *" }
|
|
708
|
-
once: { "type": "once", "runAt": "2025-02-12T10:00:00Z" }
|
|
709
660
|
`);
|
|
710
661
|
}
|
|
711
662
|
|
|
@@ -719,8 +670,6 @@ const commands = {
|
|
|
719
670
|
"--help": showHelp,
|
|
720
671
|
"-h": showHelp,
|
|
721
672
|
"--daemon": daemon,
|
|
722
|
-
"--install-launchd": installLaunchd,
|
|
723
|
-
"--uninstall-launchd": uninstallLaunchd,
|
|
724
673
|
"--validate": validate,
|
|
725
674
|
"--status": showStatus,
|
|
726
675
|
"--init": () => {
|
|
@@ -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
|