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 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
@@ -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('1.0.0');
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. Check we're in a git repo
1613
- if (!existsSync(path.join(PACKAGE_ROOT, '.git'))) {
1614
- console.error(` ${RED}FAIL${RESET} Package root is not a git repository: ${PACKAGE_ROOT}`);
1615
- console.error(' Update requires a git-cloned installation.');
1616
- process.exit(1);
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
- console.log(` ${S()} Stashing local changes...`);
1647
- const stashOut = execSync('git stash', { cwd: PACKAGE_ROOT, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
1648
- didStash = !stashOut.includes('No local changes');
1649
- if (didStash) {
1650
- console.log(` ${GREEN}OK${RESET} Stashed local changes`);
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
- console.log(` ${S()} Stopping daemon (PID ${pid})...`);
1692
- if (!options.dryRun) {
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.79",
3
+ "version": "1.0.81",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",