clementine-agent 1.0.79 → 1.0.81
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 +21 -1
- package/dist/cli/dashboard.js +52 -0
- package/dist/cli/index.js +63 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -556,7 +556,7 @@ Both paths trigger an automatic daemon restart when the new agent is detected.
|
|
|
556
556
|
|
|
557
557
|
### Agent configuration
|
|
558
558
|
|
|
559
|
-
Each agent is defined by a YAML frontmatter file in `~/.clementine/agents/<slug>/agent.md`:
|
|
559
|
+
Each agent is defined by a YAML frontmatter file in `~/.clementine/vault/00-System/agents/<slug>/agent.md`:
|
|
560
560
|
|
|
561
561
|
| Field | Description |
|
|
562
562
|
|-------|-------------|
|
|
@@ -585,6 +585,26 @@ The dashboard's "The Office" page shows each agent as an animated desk station w
|
|
|
585
585
|
- Channel assignment, model badge, project badge, tool count
|
|
586
586
|
- Edit and "Let Go" (delete) actions
|
|
587
587
|
|
|
588
|
+
### Per-agent heartbeats
|
|
589
|
+
|
|
590
|
+
Each specialist (Ross / Sasha / your hires) gets their own autonomous heartbeat scheduler alongside Clementine's. The cycle:
|
|
591
|
+
|
|
592
|
+
1. **Cheap tick** every 30 min: load the agent's state, hash three signals (pending delegated tasks, latest goal update, latest cron run). If unchanged → silent tick, no LLM call, no cost.
|
|
593
|
+
2. **LLM tick** when a signal *changes* between ticks (a delegated task arrived, a goal moved, a cron deliverable to review): the scheduler invokes `assistant.heartbeat()` with the agent's profile. Output flows through their dedicated Discord bot to their channel.
|
|
594
|
+
3. **Self-adjusting cadence**: agents end their LLM-tick output with `[NEXT_CHECK: Xm]` to set when to check in next (5–720 min). Clamped at the bounds. Default 30m if omitted.
|
|
595
|
+
|
|
596
|
+
State per agent at `~/.clementine/heartbeat/agents/<slug>/state.json`. Live observability via:
|
|
597
|
+
|
|
598
|
+
```bash
|
|
599
|
+
curl -H "X-Token: $(cat ~/.clementine/.dashboard-token)" \
|
|
600
|
+
http://localhost:3030/api/agent-heartbeats | jq
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Routing rules — Clementine remains the master delegator:
|
|
604
|
+
- Inbox triage runs as Clementine, but she'll hand off via `team_message` when an item clearly belongs to a specialist (she's allowed to guess).
|
|
605
|
+
- Daily-plan goal-priorities owned by a specialist now fire goal-triggers (which run as the owner) instead of queueing as Clementine's work.
|
|
606
|
+
- Goal advancement triggers route to `goal.owner` automatically.
|
|
607
|
+
|
|
588
608
|
---
|
|
589
609
|
|
|
590
610
|
## Scheduled tasks & cron jobs
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -1049,6 +1049,55 @@ function getHeartbeat() {
|
|
|
1049
1049
|
return {};
|
|
1050
1050
|
}
|
|
1051
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Read per-agent heartbeat states from disk (one state.json per agent
|
|
1054
|
+
* under ~/.clementine/heartbeat/agents/<slug>/). Returns an array sorted
|
|
1055
|
+
* by next-due-soonest, with relative-time strings convenient for UI use.
|
|
1056
|
+
*/
|
|
1057
|
+
function getAgentHeartbeats() {
|
|
1058
|
+
const agentsRoot = path.join(BASE_DIR, 'heartbeat', 'agents');
|
|
1059
|
+
if (!existsSync(agentsRoot))
|
|
1060
|
+
return [];
|
|
1061
|
+
const out = [];
|
|
1062
|
+
let dirs = [];
|
|
1063
|
+
try {
|
|
1064
|
+
dirs = readdirSync(agentsRoot, { withFileTypes: true })
|
|
1065
|
+
.filter((d) => d.isDirectory())
|
|
1066
|
+
.map((d) => d.name);
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
return [];
|
|
1070
|
+
}
|
|
1071
|
+
const now = Date.now();
|
|
1072
|
+
for (const slug of dirs) {
|
|
1073
|
+
const stateFile = path.join(agentsRoot, slug, 'state.json');
|
|
1074
|
+
if (!existsSync(stateFile))
|
|
1075
|
+
continue;
|
|
1076
|
+
try {
|
|
1077
|
+
const state = JSON.parse(readFileSync(stateFile, 'utf-8'));
|
|
1078
|
+
const lastTickMs = state.lastTickAt ? new Date(String(state.lastTickAt)).getTime() : 0;
|
|
1079
|
+
const nextCheckMs = state.nextCheckAt ? new Date(String(state.nextCheckAt)).getTime() : 0;
|
|
1080
|
+
out.push({
|
|
1081
|
+
slug,
|
|
1082
|
+
lastTickAt: state.lastTickAt ?? null,
|
|
1083
|
+
nextCheckAt: state.nextCheckAt ?? null,
|
|
1084
|
+
silentTickCount: Number(state.silentTickCount ?? 0),
|
|
1085
|
+
fingerprint: state.fingerprint ?? '',
|
|
1086
|
+
lastSignalSummary: state.lastSignalSummary ?? null,
|
|
1087
|
+
lastTickAgoMs: lastTickMs ? now - lastTickMs : null,
|
|
1088
|
+
nextCheckInMs: nextCheckMs ? nextCheckMs - now : null,
|
|
1089
|
+
isDue: nextCheckMs > 0 && nextCheckMs <= now,
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
catch { /* skip malformed */ }
|
|
1093
|
+
}
|
|
1094
|
+
out.sort((a, b) => {
|
|
1095
|
+
const ai = typeof a.nextCheckInMs === 'number' ? a.nextCheckInMs : Number.MAX_SAFE_INTEGER;
|
|
1096
|
+
const bi = typeof b.nextCheckInMs === 'number' ? b.nextCheckInMs : Number.MAX_SAFE_INTEGER;
|
|
1097
|
+
return ai - bi;
|
|
1098
|
+
});
|
|
1099
|
+
return out;
|
|
1100
|
+
}
|
|
1052
1101
|
async function getMemory() {
|
|
1053
1102
|
const memoryFile = path.join(VAULT_DIR, '00-System', 'MEMORY.md');
|
|
1054
1103
|
let content = '';
|
|
@@ -1925,6 +1974,9 @@ export async function cmdDashboard(opts) {
|
|
|
1925
1974
|
app.get('/api/heartbeat', (_req, res) => {
|
|
1926
1975
|
res.json(getHeartbeat());
|
|
1927
1976
|
});
|
|
1977
|
+
app.get('/api/agent-heartbeats', (_req, res) => {
|
|
1978
|
+
res.json(getAgentHeartbeats());
|
|
1979
|
+
});
|
|
1928
1980
|
app.get('/api/heartbeat/agent/:slug', (req, res) => {
|
|
1929
1981
|
const slug = req.params.slug;
|
|
1930
1982
|
const state = getHeartbeat();
|
package/dist/cli/index.js
CHANGED
|
@@ -1074,10 +1074,16 @@ function cmdTools() {
|
|
|
1074
1074
|
}
|
|
1075
1075
|
// ── Program ──────────────────────────────────────────────────────────
|
|
1076
1076
|
const program = new Command();
|
|
1077
|
+
let pkgVersion = '0.0.0';
|
|
1078
|
+
try {
|
|
1079
|
+
const pkgRaw = readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf-8');
|
|
1080
|
+
pkgVersion = String(JSON.parse(pkgRaw).version ?? '0.0.0');
|
|
1081
|
+
}
|
|
1082
|
+
catch { /* fall back to placeholder */ }
|
|
1077
1083
|
program
|
|
1078
1084
|
.name('clementine')
|
|
1079
1085
|
.description('Clementine Personal AI Assistant')
|
|
1080
|
-
.version(
|
|
1086
|
+
.version(pkgVersion);
|
|
1081
1087
|
program
|
|
1082
1088
|
.command('launch')
|
|
1083
1089
|
.description('Start the assistant (daemon by default)')
|
|
@@ -1609,11 +1615,46 @@ async function cmdUpdate(options) {
|
|
|
1609
1615
|
console.log();
|
|
1610
1616
|
console.log(` ${DIM}Updating ${getAssistantName()}...${RESET}`);
|
|
1611
1617
|
console.log();
|
|
1612
|
-
// 1.
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1618
|
+
// 1. Detect install flavor. Two valid paths:
|
|
1619
|
+
// - git-clone install (PACKAGE_ROOT has .git) → pull + rebuild path below
|
|
1620
|
+
// - npm-global install (no .git) → delegate to `npm install -g clementine-agent@latest`
|
|
1621
|
+
const isGitInstall = existsSync(path.join(PACKAGE_ROOT, '.git'));
|
|
1622
|
+
if (!isGitInstall) {
|
|
1623
|
+
if (options.dryRun) {
|
|
1624
|
+
console.log(` ${DIM}[dry-run]${RESET} Would run: npm install -g clementine-agent@latest`);
|
|
1625
|
+
if (options.restart)
|
|
1626
|
+
console.log(` ${DIM}[dry-run]${RESET} Would restart the daemon`);
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
console.log(` ${DIM}npm-global install detected at ${PACKAGE_ROOT}${RESET}`);
|
|
1630
|
+
console.log(` ${DIM}Running: npm install -g clementine-agent@latest${RESET}`);
|
|
1631
|
+
console.log();
|
|
1632
|
+
try {
|
|
1633
|
+
execSync('npm install -g clementine-agent@latest', { stdio: 'inherit' });
|
|
1634
|
+
console.log();
|
|
1635
|
+
console.log(` ${GREEN}OK${RESET} Updated via npm`);
|
|
1636
|
+
}
|
|
1637
|
+
catch (err) {
|
|
1638
|
+
console.error(` ${RED}FAIL${RESET} npm update failed: ${String(err).slice(0, 200)}`);
|
|
1639
|
+
console.error(` ${YELLOW}Hint${RESET} If you see EACCES, see README "Troubleshooting" for npm prefix setup.`);
|
|
1640
|
+
process.exit(1);
|
|
1641
|
+
}
|
|
1642
|
+
if (options.restart) {
|
|
1643
|
+
try {
|
|
1644
|
+
console.log(` ${DIM}Restarting daemon...${RESET}`);
|
|
1645
|
+
execSync('clementine restart', { stdio: 'inherit' });
|
|
1646
|
+
console.log(` ${GREEN}OK${RESET} Daemon restarted`);
|
|
1647
|
+
}
|
|
1648
|
+
catch (err) {
|
|
1649
|
+
console.error(` ${YELLOW}WARN${RESET} Restart failed: ${String(err).slice(0, 200)}. Run \`clementine restart\` manually.`);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
else {
|
|
1653
|
+
console.log();
|
|
1654
|
+
console.log(` ${DIM}Restart your daemon to pick up the new code:${RESET}`);
|
|
1655
|
+
console.log(` clementine restart`);
|
|
1656
|
+
}
|
|
1657
|
+
return;
|
|
1617
1658
|
}
|
|
1618
1659
|
let step = 0;
|
|
1619
1660
|
const S = () => `[${++step}]`;
|
|
@@ -1643,11 +1684,16 @@ async function cmdUpdate(options) {
|
|
|
1643
1684
|
try {
|
|
1644
1685
|
const status = execSync('git status --porcelain', { cwd: PACKAGE_ROOT, encoding: 'utf-8' }).trim();
|
|
1645
1686
|
if (status) {
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
console.log(` ${
|
|
1687
|
+
if (options.dryRun) {
|
|
1688
|
+
console.log(` ${S()} Would stash local changes`);
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
console.log(` ${S()} Stashing local changes...`);
|
|
1692
|
+
const stashOut = execSync('git stash', { cwd: PACKAGE_ROOT, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
1693
|
+
didStash = !stashOut.includes('No local changes');
|
|
1694
|
+
if (didStash) {
|
|
1695
|
+
console.log(` ${GREEN}OK${RESET} Stashed local changes`);
|
|
1696
|
+
}
|
|
1651
1697
|
}
|
|
1652
1698
|
}
|
|
1653
1699
|
}
|
|
@@ -1688,15 +1734,18 @@ async function cmdUpdate(options) {
|
|
|
1688
1734
|
const pid = readPid();
|
|
1689
1735
|
const wasRunning = pid && isProcessAlive(pid);
|
|
1690
1736
|
if (wasRunning) {
|
|
1691
|
-
|
|
1692
|
-
|
|
1737
|
+
if (options.dryRun) {
|
|
1738
|
+
console.log(` ${S()} Would stop daemon (PID ${pid})`);
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
console.log(` ${S()} Stopping daemon (PID ${pid})...`);
|
|
1693
1742
|
stopDaemon(pid);
|
|
1694
1743
|
try {
|
|
1695
1744
|
unlinkSync(getPidFilePath());
|
|
1696
1745
|
}
|
|
1697
1746
|
catch { /* ignore */ }
|
|
1747
|
+
console.log(` ${GREEN}OK${RESET} Daemon stopped`);
|
|
1698
1748
|
}
|
|
1699
|
-
console.log(` ${GREEN}OK${RESET} Daemon stopped`);
|
|
1700
1749
|
}
|
|
1701
1750
|
// Helper: if update fails after stopping daemon, relaunch before exiting
|
|
1702
1751
|
function failAndRestart(backupDir) {
|