@forwardimpact/basecamp 2.0.0 → 2.3.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/config/scheduler.json +5 -0
- package/package.json +1 -1
- package/src/basecamp.js +288 -57
- package/template/.claude/agents/chief-of-staff.md +6 -2
- package/template/.claude/agents/concierge.md +2 -3
- package/template/.claude/agents/librarian.md +4 -6
- package/template/.claude/agents/recruiter.md +269 -0
- package/template/.claude/settings.json +0 -4
- package/template/.claude/skills/analyze-cv/SKILL.md +269 -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/right-to-be-forgotten/SKILL.md +333 -0
- 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 +376 -0
- package/template/.claude/skills/upstream-skill/SKILL.md +207 -0
- package/template/.claude/skills/weekly-update/SKILL.md +250 -0
- package/template/CLAUDE.md +68 -40
- 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
package/config/scheduler.json
CHANGED
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
"kb": "~/Documents/Personal",
|
|
20
20
|
"schedule": { "type": "cron", "expression": "0 7,18 * * *" },
|
|
21
21
|
"enabled": true
|
|
22
|
+
},
|
|
23
|
+
"recruiter": {
|
|
24
|
+
"kb": "~/Documents/Personal",
|
|
25
|
+
"schedule": { "type": "interval", "minutes": 30 },
|
|
26
|
+
"enabled": true
|
|
22
27
|
}
|
|
23
28
|
}
|
|
24
29
|
}
|
package/package.json
CHANGED
package/src/basecamp.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// node basecamp.js --daemon Run continuously (poll every 60s)
|
|
8
8
|
// node basecamp.js --wake <agent> Wake a specific agent immediately
|
|
9
9
|
// node basecamp.js --init <path> Initialize a new knowledge base
|
|
10
|
+
// node basecamp.js --update [path] Update KB with latest CLAUDE.md, agents and skills
|
|
11
|
+
// node basecamp.js --stop Gracefully stop daemon and children
|
|
10
12
|
// node basecamp.js --validate Validate agent definitions exist
|
|
11
13
|
// node basecamp.js --status Show agent status
|
|
12
14
|
// node basecamp.js --help Show this help
|
|
@@ -20,8 +22,9 @@ import {
|
|
|
20
22
|
chmodSync,
|
|
21
23
|
readdirSync,
|
|
22
24
|
statSync,
|
|
25
|
+
cpSync,
|
|
26
|
+
copyFileSync,
|
|
23
27
|
} from "node:fs";
|
|
24
|
-
import { execSync } from "node:child_process";
|
|
25
28
|
import { join, dirname, resolve } from "node:path";
|
|
26
29
|
import { homedir } from "node:os";
|
|
27
30
|
import { fileURLToPath } from "node:url";
|
|
@@ -48,10 +51,13 @@ let daemonStartedAt = null;
|
|
|
48
51
|
// Matches the 30-minute child_process timeout plus a buffer.
|
|
49
52
|
const MAX_AGENT_RUNTIME_MS = 35 * 60_000;
|
|
50
53
|
|
|
54
|
+
/** Active child PIDs spawned by posix_spawn (for graceful shutdown). */
|
|
55
|
+
const activeChildren = new Set();
|
|
56
|
+
|
|
51
57
|
// --- Helpers ----------------------------------------------------------------
|
|
52
58
|
|
|
53
59
|
function ensureDir(dir) {
|
|
54
|
-
|
|
60
|
+
mkdirSync(dir, { recursive: true });
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
function readJSON(path, fallback) {
|
|
@@ -162,13 +168,8 @@ function cronMatches(expr, d) {
|
|
|
162
168
|
// --- Scheduling logic -------------------------------------------------------
|
|
163
169
|
|
|
164
170
|
function floorToMinute(d) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
d.getMonth(),
|
|
168
|
-
d.getDate(),
|
|
169
|
-
d.getHours(),
|
|
170
|
-
d.getMinutes(),
|
|
171
|
-
).getTime();
|
|
171
|
+
const t = d.getTime();
|
|
172
|
+
return t - (t % 60_000);
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
function shouldWake(agent, agentState, now) {
|
|
@@ -197,7 +198,16 @@ function shouldWake(agent, agentState, now) {
|
|
|
197
198
|
|
|
198
199
|
// --- Agent execution --------------------------------------------------------
|
|
199
200
|
|
|
200
|
-
|
|
201
|
+
function failAgent(agentState, error) {
|
|
202
|
+
Object.assign(agentState, {
|
|
203
|
+
status: "failed",
|
|
204
|
+
startedAt: null,
|
|
205
|
+
lastWokeAt: new Date().toISOString(),
|
|
206
|
+
lastError: String(error).slice(0, 500),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function wakeAgent(agentName, agent, state) {
|
|
201
211
|
if (!agent.kb) {
|
|
202
212
|
log(`Agent ${agentName}: no "kb" specified, skipping.`);
|
|
203
213
|
return;
|
|
@@ -226,6 +236,7 @@ async function wakeAgent(agentName, agent, _config, state) {
|
|
|
226
236
|
undefined,
|
|
227
237
|
kbPath,
|
|
228
238
|
);
|
|
239
|
+
activeChildren.add(pid);
|
|
229
240
|
|
|
230
241
|
// Read stdout and stderr concurrently to avoid pipe deadlocks,
|
|
231
242
|
// then wait for the child to exit.
|
|
@@ -234,6 +245,7 @@ async function wakeAgent(agentName, agent, _config, state) {
|
|
|
234
245
|
posixSpawn.readAll(stderrFd),
|
|
235
246
|
]);
|
|
236
247
|
const exitCode = await posixSpawn.waitForExit(pid);
|
|
248
|
+
activeChildren.delete(pid);
|
|
237
249
|
|
|
238
250
|
if (exitCode === 0) {
|
|
239
251
|
log(`Agent ${agentName} completed. Output: ${stdout.slice(0, 200)}...`);
|
|
@@ -241,24 +253,13 @@ async function wakeAgent(agentName, agent, _config, state) {
|
|
|
241
253
|
} else {
|
|
242
254
|
const errMsg = stderr || stdout || `Exit code ${exitCode}`;
|
|
243
255
|
log(`Agent ${agentName} failed: ${errMsg.slice(0, 300)}`);
|
|
244
|
-
|
|
245
|
-
status: "failed",
|
|
246
|
-
startedAt: null,
|
|
247
|
-
lastWokeAt: new Date().toISOString(),
|
|
248
|
-
lastError: errMsg.slice(0, 500),
|
|
249
|
-
});
|
|
256
|
+
failAgent(as, errMsg);
|
|
250
257
|
}
|
|
251
|
-
saveState(state);
|
|
252
258
|
} catch (err) {
|
|
253
259
|
log(`Agent ${agentName} failed: ${err.message}`);
|
|
254
|
-
|
|
255
|
-
status: "failed",
|
|
256
|
-
startedAt: null,
|
|
257
|
-
lastWokeAt: new Date().toISOString(),
|
|
258
|
-
lastError: err.message.slice(0, 500),
|
|
259
|
-
});
|
|
260
|
-
saveState(state);
|
|
260
|
+
failAgent(as, err.message);
|
|
261
261
|
}
|
|
262
|
+
saveState(state);
|
|
262
263
|
}
|
|
263
264
|
|
|
264
265
|
/**
|
|
@@ -336,7 +337,7 @@ async function wakeDueAgents() {
|
|
|
336
337
|
let wokeAny = false;
|
|
337
338
|
for (const [name, agent] of Object.entries(config.agents)) {
|
|
338
339
|
if (shouldWake(agent, state.agents[name] || {}, now)) {
|
|
339
|
-
await wakeAgent(name, agent,
|
|
340
|
+
await wakeAgent(name, agent, state);
|
|
340
341
|
wokeAny = true;
|
|
341
342
|
}
|
|
342
343
|
}
|
|
@@ -392,25 +393,31 @@ function computeNextWakeAt(agent, agentState, now) {
|
|
|
392
393
|
* @returns {string|null}
|
|
393
394
|
*/
|
|
394
395
|
function resolveBriefingFile(agentName, agentConfig) {
|
|
395
|
-
// 1. Scan state directory for agent-specific files
|
|
396
|
+
// 1. Scan state directory for agent-specific files (latest by mtime)
|
|
396
397
|
const stateDir = join(CACHE_DIR, "state");
|
|
397
398
|
if (existsSync(stateDir)) {
|
|
398
399
|
const prefix = agentName.replace(/-/g, "_") + "_";
|
|
399
400
|
const matches = readdirSync(stateDir).filter(
|
|
400
401
|
(f) => f.startsWith(prefix) && f.endsWith(".md"),
|
|
401
402
|
);
|
|
402
|
-
if (matches.length
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
403
|
+
if (matches.length > 0) {
|
|
404
|
+
let latest = join(stateDir, matches[0]);
|
|
405
|
+
let latestMtime = statSync(latest).mtimeMs;
|
|
406
|
+
for (let i = 1; i < matches.length; i++) {
|
|
407
|
+
const p = join(stateDir, matches[i]);
|
|
408
|
+
const mt = statSync(p).mtimeMs;
|
|
409
|
+
if (mt > latestMtime) {
|
|
410
|
+
latest = p;
|
|
411
|
+
latestMtime = mt;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return latest;
|
|
407
415
|
}
|
|
408
416
|
}
|
|
409
417
|
|
|
410
|
-
// 2. Fall back to KB briefings directory
|
|
418
|
+
// 2. Fall back to KB briefings directory (latest by name)
|
|
411
419
|
if (agentConfig.kb) {
|
|
412
|
-
const
|
|
413
|
-
const dir = join(kbPath, "knowledge", "Briefings");
|
|
420
|
+
const dir = join(expandPath(agentConfig.kb), "knowledge", "Briefings");
|
|
414
421
|
if (existsSync(dir)) {
|
|
415
422
|
const files = readdirSync(dir)
|
|
416
423
|
.filter((f) => f.endsWith(".md"))
|
|
@@ -475,6 +482,14 @@ function handleMessage(socket, line) {
|
|
|
475
482
|
|
|
476
483
|
if (request.type === "status") return handleStatusRequest(socket);
|
|
477
484
|
|
|
485
|
+
if (request.type === "shutdown") {
|
|
486
|
+
log("Shutdown requested via socket.");
|
|
487
|
+
send(socket, { type: "ack", command: "shutdown" });
|
|
488
|
+
socket.end();
|
|
489
|
+
killActiveChildren();
|
|
490
|
+
process.exit(0);
|
|
491
|
+
}
|
|
492
|
+
|
|
478
493
|
if (request.type === "wake") {
|
|
479
494
|
if (!request.agent) {
|
|
480
495
|
send(socket, { type: "error", message: "Missing agent name" });
|
|
@@ -491,7 +506,7 @@ function handleMessage(socket, line) {
|
|
|
491
506
|
}
|
|
492
507
|
send(socket, { type: "ack", command: "wake", agent: request.agent });
|
|
493
508
|
const state = loadState();
|
|
494
|
-
wakeAgent(request.agent, agent,
|
|
509
|
+
wakeAgent(request.agent, agent, state).catch(() => {});
|
|
495
510
|
return;
|
|
496
511
|
}
|
|
497
512
|
|
|
@@ -530,7 +545,11 @@ function startSocketServer() {
|
|
|
530
545
|
});
|
|
531
546
|
|
|
532
547
|
const cleanup = () => {
|
|
548
|
+
killActiveChildren();
|
|
533
549
|
server.close();
|
|
550
|
+
try {
|
|
551
|
+
unlinkSync(SOCKET_PATH);
|
|
552
|
+
} catch {}
|
|
534
553
|
process.exit(0);
|
|
535
554
|
};
|
|
536
555
|
process.on("SIGTERM", cleanup);
|
|
@@ -539,6 +558,71 @@ function startSocketServer() {
|
|
|
539
558
|
return server;
|
|
540
559
|
}
|
|
541
560
|
|
|
561
|
+
// --- Graceful shutdown -------------------------------------------------------
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Send SIGTERM to all tracked child processes (running claude sessions).
|
|
565
|
+
* Called on daemon shutdown to prevent orphaned processes.
|
|
566
|
+
*/
|
|
567
|
+
function killActiveChildren() {
|
|
568
|
+
for (const pid of activeChildren) {
|
|
569
|
+
try {
|
|
570
|
+
process.kill(pid, "SIGTERM");
|
|
571
|
+
log(`Sent SIGTERM to child PID ${pid}`);
|
|
572
|
+
} catch {
|
|
573
|
+
// Already exited
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
activeChildren.clear();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Connect to the daemon socket and request graceful shutdown.
|
|
581
|
+
* Waits up to 5 seconds for the daemon to exit.
|
|
582
|
+
* @returns {Promise<boolean>} true if shutdown succeeded
|
|
583
|
+
*/
|
|
584
|
+
async function requestShutdown() {
|
|
585
|
+
if (!existsSync(SOCKET_PATH)) {
|
|
586
|
+
console.log("Daemon not running (no socket).");
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const { createConnection } = await import("node:net");
|
|
591
|
+
return new Promise((resolve) => {
|
|
592
|
+
const timeout = setTimeout(() => {
|
|
593
|
+
console.log("Shutdown timed out.");
|
|
594
|
+
socket.destroy();
|
|
595
|
+
resolve(false);
|
|
596
|
+
}, 5000);
|
|
597
|
+
|
|
598
|
+
const socket = createConnection(SOCKET_PATH, () => {
|
|
599
|
+
socket.write(JSON.stringify({ type: "shutdown" }) + "\n");
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
let buffer = "";
|
|
603
|
+
socket.on("data", (data) => {
|
|
604
|
+
buffer += data.toString();
|
|
605
|
+
if (buffer.includes("\n")) {
|
|
606
|
+
clearTimeout(timeout);
|
|
607
|
+
console.log("Daemon stopped.");
|
|
608
|
+
socket.destroy();
|
|
609
|
+
resolve(true);
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
socket.on("error", () => {
|
|
614
|
+
clearTimeout(timeout);
|
|
615
|
+
console.log("Daemon not running (connection refused).");
|
|
616
|
+
resolve(false);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
socket.on("close", () => {
|
|
620
|
+
clearTimeout(timeout);
|
|
621
|
+
resolve(true);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
542
626
|
// --- Daemon -----------------------------------------------------------------
|
|
543
627
|
|
|
544
628
|
function daemon() {
|
|
@@ -563,7 +647,11 @@ function daemon() {
|
|
|
563
647
|
|
|
564
648
|
// --- Init knowledge base ----------------------------------------------------
|
|
565
649
|
|
|
566
|
-
|
|
650
|
+
/**
|
|
651
|
+
* Resolve the template directory or exit with an error.
|
|
652
|
+
* @returns {string}
|
|
653
|
+
*/
|
|
654
|
+
function requireTemplateDir() {
|
|
567
655
|
const bundle = getBundlePath();
|
|
568
656
|
if (bundle) {
|
|
569
657
|
const tpl = join(bundle.resources, "template");
|
|
@@ -574,7 +662,91 @@ function findTemplateDir() {
|
|
|
574
662
|
join(__dirname, "..", "template"),
|
|
575
663
|
])
|
|
576
664
|
if (existsSync(d)) return d;
|
|
577
|
-
|
|
665
|
+
console.error("Template not found. Reinstall fit-basecamp.");
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Copy bundled files (CLAUDE.md, skills, agents) from template to a KB.
|
|
671
|
+
* Shared by --init and --update.
|
|
672
|
+
* @param {string} tpl Path to the template directory
|
|
673
|
+
* @param {string} dest Path to the target knowledge base
|
|
674
|
+
*/
|
|
675
|
+
function copyBundledFiles(tpl, dest) {
|
|
676
|
+
// CLAUDE.md
|
|
677
|
+
copyFileSync(join(tpl, "CLAUDE.md"), join(dest, "CLAUDE.md"));
|
|
678
|
+
console.log(` Updated CLAUDE.md`);
|
|
679
|
+
|
|
680
|
+
// Settings — merge template permissions into existing settings
|
|
681
|
+
mergeSettings(tpl, dest);
|
|
682
|
+
|
|
683
|
+
// Skills and agents
|
|
684
|
+
for (const sub of ["skills", "agents"]) {
|
|
685
|
+
const src = join(tpl, ".claude", sub);
|
|
686
|
+
if (!existsSync(src)) continue;
|
|
687
|
+
cpSync(src, join(dest, ".claude", sub), { recursive: true });
|
|
688
|
+
const entries = readdirSync(src, { withFileTypes: true }).filter((d) =>
|
|
689
|
+
sub === "skills" ? d.isDirectory() : d.name.endsWith(".md"),
|
|
690
|
+
);
|
|
691
|
+
const names = entries.map((d) =>
|
|
692
|
+
sub === "agents" ? d.name.replace(".md", "") : d.name,
|
|
693
|
+
);
|
|
694
|
+
console.log(` Updated ${names.length} ${sub}: ${names.join(", ")}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Merge template settings.json into the destination's settings.json.
|
|
700
|
+
* Adds any missing entries from allow, deny, and additionalDirectories
|
|
701
|
+
* without removing user customizations.
|
|
702
|
+
* @param {string} tpl Template directory
|
|
703
|
+
* @param {string} dest Knowledge base directory
|
|
704
|
+
*/
|
|
705
|
+
function mergeSettings(tpl, dest) {
|
|
706
|
+
const src = join(tpl, ".claude", "settings.json");
|
|
707
|
+
if (!existsSync(src)) return;
|
|
708
|
+
|
|
709
|
+
const destPath = join(dest, ".claude", "settings.json");
|
|
710
|
+
|
|
711
|
+
// No existing settings — copy template directly
|
|
712
|
+
if (!existsSync(destPath)) {
|
|
713
|
+
ensureDir(join(dest, ".claude"));
|
|
714
|
+
copyFileSync(src, destPath);
|
|
715
|
+
console.log(` Created settings.json`);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const template = readJSON(src, {});
|
|
720
|
+
const existing = readJSON(destPath, {});
|
|
721
|
+
const tp = template.permissions || {};
|
|
722
|
+
const ep = (existing.permissions ||= {});
|
|
723
|
+
let added = 0;
|
|
724
|
+
|
|
725
|
+
// Merge array fields
|
|
726
|
+
for (const key of ["allow", "deny", "additionalDirectories"]) {
|
|
727
|
+
if (!tp[key]?.length) continue;
|
|
728
|
+
const set = new Set((ep[key] ||= []));
|
|
729
|
+
for (const entry of tp[key]) {
|
|
730
|
+
if (!set.has(entry)) {
|
|
731
|
+
ep[key].push(entry);
|
|
732
|
+
set.add(entry);
|
|
733
|
+
added++;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Merge scalar fields
|
|
739
|
+
if (tp.defaultMode && !ep.defaultMode) {
|
|
740
|
+
ep.defaultMode = tp.defaultMode;
|
|
741
|
+
added++;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (added > 0) {
|
|
745
|
+
writeJSON(destPath, existing);
|
|
746
|
+
console.log(` Updated settings.json (${added} new entries)`);
|
|
747
|
+
} else {
|
|
748
|
+
console.log(` Settings up to date`);
|
|
749
|
+
}
|
|
578
750
|
}
|
|
579
751
|
|
|
580
752
|
function initKB(targetPath) {
|
|
@@ -583,11 +755,7 @@ function initKB(targetPath) {
|
|
|
583
755
|
console.error(`Knowledge base already exists at ${dest}`);
|
|
584
756
|
process.exit(1);
|
|
585
757
|
}
|
|
586
|
-
const tpl =
|
|
587
|
-
if (!tpl) {
|
|
588
|
-
console.error("Template not found. Reinstall fit-basecamp.");
|
|
589
|
-
process.exit(1);
|
|
590
|
-
}
|
|
758
|
+
const tpl = requireTemplateDir();
|
|
591
759
|
|
|
592
760
|
ensureDir(dest);
|
|
593
761
|
for (const d of [
|
|
@@ -599,13 +767,70 @@ function initKB(targetPath) {
|
|
|
599
767
|
])
|
|
600
768
|
ensureDir(join(dest, d));
|
|
601
769
|
|
|
602
|
-
|
|
770
|
+
// User-specific files (not overwritten by --update)
|
|
771
|
+
copyFileSync(join(tpl, "USER.md"), join(dest, "USER.md"));
|
|
772
|
+
|
|
773
|
+
// Bundled files (shared with --update)
|
|
774
|
+
copyBundledFiles(tpl, dest);
|
|
603
775
|
|
|
604
776
|
console.log(
|
|
605
777
|
`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`,
|
|
606
778
|
);
|
|
607
779
|
}
|
|
608
780
|
|
|
781
|
+
// --- Update knowledge base --------------------------------------------------
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Update an existing knowledge base with the latest bundled files.
|
|
785
|
+
* User data (USER.md, knowledge/) is untouched.
|
|
786
|
+
* Settings.json is merged — new template entries are added without
|
|
787
|
+
* removing user customizations.
|
|
788
|
+
* @param {string} targetPath
|
|
789
|
+
*/
|
|
790
|
+
function updateKB(targetPath) {
|
|
791
|
+
const dest = expandPath(targetPath);
|
|
792
|
+
if (!existsSync(join(dest, "CLAUDE.md"))) {
|
|
793
|
+
console.error(`No knowledge base found at ${dest}`);
|
|
794
|
+
process.exit(1);
|
|
795
|
+
}
|
|
796
|
+
const tpl = requireTemplateDir();
|
|
797
|
+
copyBundledFiles(tpl, dest);
|
|
798
|
+
console.log(`\nKnowledge base updated: ${dest}`);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Run --update for an explicit path or every unique KB in the scheduler config.
|
|
803
|
+
*/
|
|
804
|
+
function runUpdate() {
|
|
805
|
+
if (args[1]) {
|
|
806
|
+
updateKB(args[1]);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Discover unique KB paths from config
|
|
811
|
+
const config = loadConfig();
|
|
812
|
+
const kbPaths = [
|
|
813
|
+
...new Set(
|
|
814
|
+
Object.values(config.agents)
|
|
815
|
+
.filter((a) => a.kb)
|
|
816
|
+
.map((a) => expandPath(a.kb)),
|
|
817
|
+
),
|
|
818
|
+
];
|
|
819
|
+
|
|
820
|
+
if (kbPaths.length === 0) {
|
|
821
|
+
console.error(
|
|
822
|
+
"No knowledge bases configured and no path given.\n" +
|
|
823
|
+
"Usage: fit-basecamp --update [path]",
|
|
824
|
+
);
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
for (const kb of kbPaths) {
|
|
829
|
+
console.log(`\nUpdating ${kb}...`);
|
|
830
|
+
updateKB(kb);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
609
834
|
// --- Status -----------------------------------------------------------------
|
|
610
835
|
|
|
611
836
|
function showStatus() {
|
|
@@ -693,6 +918,8 @@ Usage:
|
|
|
693
918
|
${bin} --daemon Run continuously (poll every 60s)
|
|
694
919
|
${bin} --wake <agent> Wake a specific agent immediately
|
|
695
920
|
${bin} --init <path> Initialize a new knowledge base
|
|
921
|
+
${bin} --update [path] Update KB with latest CLAUDE.md, agents and skills
|
|
922
|
+
${bin} --stop Gracefully stop daemon and all running agents
|
|
696
923
|
${bin} --validate Validate agent definitions exist
|
|
697
924
|
${bin} --status Show agent status
|
|
698
925
|
|
|
@@ -708,34 +935,38 @@ const args = process.argv.slice(2);
|
|
|
708
935
|
const command = args[0];
|
|
709
936
|
ensureDir(BASECAMP_HOME);
|
|
710
937
|
|
|
938
|
+
function requireArg(usage) {
|
|
939
|
+
if (!args[1]) {
|
|
940
|
+
console.error(usage);
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
943
|
+
return args[1];
|
|
944
|
+
}
|
|
945
|
+
|
|
711
946
|
const commands = {
|
|
712
947
|
"--help": showHelp,
|
|
713
948
|
"-h": showHelp,
|
|
714
949
|
"--daemon": daemon,
|
|
715
950
|
"--validate": validate,
|
|
716
|
-
"--
|
|
717
|
-
|
|
718
|
-
if (!
|
|
719
|
-
console.error("Usage: node basecamp.js --init <path>");
|
|
720
|
-
process.exit(1);
|
|
721
|
-
}
|
|
722
|
-
initKB(args[1]);
|
|
951
|
+
"--stop": async () => {
|
|
952
|
+
const stopped = await requestShutdown();
|
|
953
|
+
if (!stopped) process.exit(1);
|
|
723
954
|
},
|
|
955
|
+
"--status": showStatus,
|
|
956
|
+
"--init": () => initKB(requireArg("Usage: fit-basecamp --init <path>")),
|
|
957
|
+
"--update": runUpdate,
|
|
724
958
|
"--wake": async () => {
|
|
725
|
-
|
|
726
|
-
console.error("Usage: node basecamp.js --wake <agent-name>");
|
|
727
|
-
process.exit(1);
|
|
728
|
-
}
|
|
959
|
+
const name = requireArg("Usage: fit-basecamp --wake <agent-name>");
|
|
729
960
|
const config = loadConfig(),
|
|
730
961
|
state = loadState(),
|
|
731
|
-
agent = config.agents[
|
|
962
|
+
agent = config.agents[name];
|
|
732
963
|
if (!agent) {
|
|
733
964
|
console.error(
|
|
734
|
-
`Agent "${
|
|
965
|
+
`Agent "${name}" not found. Available: ${Object.keys(config.agents).join(", ") || "(none)"}`,
|
|
735
966
|
);
|
|
736
967
|
process.exit(1);
|
|
737
968
|
}
|
|
738
|
-
await wakeAgent(
|
|
969
|
+
await wakeAgent(name, agent, state);
|
|
739
970
|
},
|
|
740
971
|
};
|
|
741
972
|
|
|
@@ -6,6 +6,8 @@ description: >
|
|
|
6
6
|
key moments (morning, evening) by the Basecamp scheduler.
|
|
7
7
|
model: sonnet
|
|
8
8
|
permissionMode: bypassPermissions
|
|
9
|
+
skills:
|
|
10
|
+
- weekly-update
|
|
9
11
|
---
|
|
10
12
|
|
|
11
13
|
You are the chief of staff — the user's executive assistant. You create daily
|
|
@@ -18,10 +20,12 @@ Read the state files from other agents:
|
|
|
18
20
|
|
|
19
21
|
1. **Postman:** `~/.cache/fit/basecamp/state/postman_triage.md`
|
|
20
22
|
- Urgent emails, items needing reply, threads awaiting response
|
|
21
|
-
2. **Concierge:** `~/.cache/fit/basecamp/state/
|
|
23
|
+
2. **Concierge:** `~/.cache/fit/basecamp/state/concierge_triage.md`
|
|
22
24
|
- Today's meetings, prep status, unprocessed transcripts
|
|
23
|
-
3. **Librarian:** `~/.cache/fit/basecamp/state/
|
|
25
|
+
3. **Librarian:** `~/.cache/fit/basecamp/state/librarian_triage.md`
|
|
24
26
|
- Pending processing, graph size
|
|
27
|
+
4. **Recruiter:** `~/.cache/fit/basecamp/state/recruiter_triage.md`
|
|
28
|
+
- Candidate pipeline, new assessments, interview scheduling
|
|
25
29
|
|
|
26
30
|
Also read directly:
|
|
27
31
|
|
|
@@ -36,11 +36,10 @@ Assess the current state:
|
|
|
36
36
|
- Check each session's `_memo.md` against
|
|
37
37
|
`~/.cache/fit/basecamp/state/graph_processed`
|
|
38
38
|
|
|
39
|
-
Write
|
|
40
|
-
`~/.cache/fit/basecamp/state/concierge_outlook.md`:
|
|
39
|
+
Write triage results to `~/.cache/fit/basecamp/state/concierge_triage.md`:
|
|
41
40
|
|
|
42
41
|
```
|
|
43
|
-
# Calendar
|
|
42
|
+
# Calendar Triage — {YYYY-MM-DD HH:MM}
|
|
44
43
|
|
|
45
44
|
## Next Meeting
|
|
46
45
|
**{title}** at {time} with {attendees}
|
|
@@ -9,6 +9,7 @@ permissionMode: bypassPermissions
|
|
|
9
9
|
skills:
|
|
10
10
|
- extract-entities
|
|
11
11
|
- organize-files
|
|
12
|
+
- manage-tasks
|
|
12
13
|
---
|
|
13
14
|
|
|
14
15
|
You are the librarian — the user's knowledge curator. Each time you are woken,
|
|
@@ -20,20 +21,17 @@ Assess what needs processing:
|
|
|
20
21
|
|
|
21
22
|
1. Check for unprocessed synced files (mail and calendar data):
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
(Run from the extract-entities skill directory:
|
|
26
|
-
`.claude/skills/extract-entities/`)
|
|
24
|
+
node .claude/skills/extract-entities/scripts/state.mjs check
|
|
27
25
|
|
|
28
26
|
2. Count existing knowledge graph entities:
|
|
29
27
|
|
|
30
28
|
ls knowledge/People/ knowledge/Organizations/ knowledge/Projects/
|
|
31
29
|
knowledge/Topics/ 2>/dev/null | wc -l
|
|
32
30
|
|
|
33
|
-
Write
|
|
31
|
+
Write triage results to `~/.cache/fit/basecamp/state/librarian_triage.md`:
|
|
34
32
|
|
|
35
33
|
```
|
|
36
|
-
# Knowledge
|
|
34
|
+
# Knowledge Triage — {YYYY-MM-DD HH:MM}
|
|
37
35
|
|
|
38
36
|
## Pending Processing
|
|
39
37
|
- {count} unprocessed synced files
|