clementine-agent 1.0.46 → 1.0.47

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.
@@ -1075,6 +1075,12 @@ You have a declarative registry of every integration you can configure. Use it:
1075
1075
 
1076
1076
  Companion tools: \`env_list\` (masked values + backend), \`env_unset\` (removes + clears Keychain entry).
1077
1077
 
1078
+ ### Self-update (when ${owner} asks you to update yourself)
1079
+
1080
+ Call \`self_update\` — **never** manually \`cd ~/clementine && git pull\` or hunt for a source directory. There may be multiple clementine-related directories in home (stale \`~/clementine\`, the real \`~/clementine-dev\`, the data dir \`~/.clementine\`). \`self_update\` knows which source tree this daemon is actually running from — the others are stale or irrelevant and touching them will produce nothing useful while creating dangerous diverging state.
1081
+
1082
+ If you're unsure what's happening first, run \`where_is_source\` — it reports the absolute source path, current branch/commit, and whether there are uncommitted changes. \`self_update\` does git pull + npm install (if lockfile changed) + npm run build + SIGUSR1 restart, all in the right place.
1083
+
1078
1084
  ### When a tool call is refused
1079
1085
 
1080
1086
  If any tool call fails with "not in my function schema" / "tool not allowed" / "unknown tool" while the tool appears in your SDK inventory:
@@ -1396,6 +1402,8 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
1396
1402
  mcpTool('list_allowed_tools'),
1397
1403
  mcpTool('disallow_tool'),
1398
1404
  mcpTool('self_restart'),
1405
+ mcpTool('self_update'),
1406
+ mcpTool('where_is_source'),
1399
1407
  mcpTool('cron_list'),
1400
1408
  mcpTool('add_cron_job'),
1401
1409
  mcpTool('memory_report'),
@@ -1381,8 +1381,128 @@ export function registerAdminTools(server) {
1381
1381
  (args_description ? `Args: ${args_description}` : ''));
1382
1382
  }
1383
1383
  });
1384
- // ── Self-Restart ────────────────────────────────────────────────────────
1385
- server.tool('self_restart', 'Restart the Clementine daemon to pick up code changes. Sends SIGUSR1 to the running process, which triggers a graceful restart.', { _empty: z.string().optional().describe('(no parameters needed)') }, async () => {
1384
+ // ── Self-Update / Self-Restart ─────────────────────────────────────────
1385
+ /** Resolve the git-clone source dir this daemon is running from. */
1386
+ function resolvePackageRoot() {
1387
+ // This file is at clementine-dev/dist/tools/admin-tools.js at runtime.
1388
+ // Climb two dirs to get the package root.
1389
+ const here = path.dirname(fileURLToPath(import.meta.url));
1390
+ return path.resolve(here, '..', '..');
1391
+ }
1392
+ server.tool('where_is_source', 'Report the absolute path of the source tree this daemon is running from, whether it\'s a git clone, the current commit, and whether it has local uncommitted changes. Call this first before any self_update to confirm which checkout you\'re about to modify — avoids the "multiple clones in home dir" confusion where an agent updates the wrong one.', {}, async () => {
1393
+ const root = resolvePackageRoot();
1394
+ const gitDir = path.join(root, '.git');
1395
+ const isGit = existsSync(gitDir);
1396
+ const lines = [`Package root: ${root}`, `Git repo: ${isGit ? 'yes' : 'no'}`];
1397
+ if (isGit) {
1398
+ try {
1399
+ const commit = execSync('git rev-parse --short HEAD', { cwd: root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
1400
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
1401
+ const status = execSync('git status --porcelain', { cwd: root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
1402
+ lines.push(`Branch: ${branch} @ ${commit}`);
1403
+ lines.push(`Uncommitted changes: ${status ? 'yes' : 'no'}`);
1404
+ if (status)
1405
+ lines.push(`\n${status}`);
1406
+ }
1407
+ catch { /* best-effort */ }
1408
+ }
1409
+ lines.push('', 'ONLY modify files under this path when updating source. Other ~/clementine* directories may be stale checkouts — ignore them.');
1410
+ return textResult(lines.join('\n'));
1411
+ });
1412
+ server.tool('self_update', 'Update Clementine to the latest main-branch code and restart. Runs git pull + npm install (if lockfile changed) + npm run build in the running daemon\'s source dir, then signals SIGUSR1 to restart. Owner-DM only. Use this instead of manually invoking git/npm — it operates on the correct source dir and handles the restart cleanly. Returns immediately; the daemon will be unreachable for ~15s during rebuild+restart.', {
1413
+ branch: z.string().optional().describe('Branch to pull (default "main")'),
1414
+ }, async ({ branch }) => {
1415
+ const gate = requireOwnerDm();
1416
+ if (!gate.ok)
1417
+ return textResult(gate.message);
1418
+ const root = resolvePackageRoot();
1419
+ if (!existsSync(path.join(root, '.git'))) {
1420
+ return textResult(`Refused: ${root} is not a git clone. This daemon was likely installed via npm — updates should go through \`npm install -g clementine-agent@latest\`, not self_update.`);
1421
+ }
1422
+ const targetBranch = branch ?? 'main';
1423
+ const out = [];
1424
+ const runQuiet = (cmd, args, timeout = 120000) => {
1425
+ try {
1426
+ const res = execSync(`${cmd} ${args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`, {
1427
+ cwd: root, encoding: 'utf-8', timeout,
1428
+ stdio: ['ignore', 'pipe', 'pipe'],
1429
+ });
1430
+ return { ok: true, output: res };
1431
+ }
1432
+ catch (e) {
1433
+ return { ok: false, output: (e?.stdout ?? '') + '\n' + (e?.stderr ?? '') + '\n' + String(e).slice(0, 200) };
1434
+ }
1435
+ };
1436
+ // 1. Stash local changes so git pull is clean
1437
+ const statusProbe = runQuiet('git', ['status', '--porcelain']);
1438
+ const hadLocal = statusProbe.ok && statusProbe.output.trim().length > 0;
1439
+ let stashed = false;
1440
+ if (hadLocal) {
1441
+ const stashRes = runQuiet('git', ['stash', 'push', '-u', '-m', `self_update ${new Date().toISOString()}`]);
1442
+ if (stashRes.ok) {
1443
+ stashed = true;
1444
+ out.push('Stashed local changes (restore with `git stash pop` in the source dir if needed).');
1445
+ }
1446
+ else {
1447
+ return textResult(`Refused: couldn't stash local changes in ${root}. ${stashRes.output.slice(0, 200)}`);
1448
+ }
1449
+ }
1450
+ // 2. Checkout target branch if not already there
1451
+ const branchProbe = runQuiet('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
1452
+ if (branchProbe.ok && branchProbe.output.trim() !== targetBranch) {
1453
+ const coRes = runQuiet('git', ['checkout', targetBranch]);
1454
+ if (!coRes.ok)
1455
+ return textResult(`git checkout ${targetBranch} failed: ${coRes.output.slice(0, 300)}`);
1456
+ }
1457
+ // 3. Record pre-pull hashes so we know if package-lock changed
1458
+ const preLock = existsSync(path.join(root, 'package-lock.json')) ? readFileSync(path.join(root, 'package-lock.json'), 'utf-8').length : 0;
1459
+ const preCommit = runQuiet('git', ['rev-parse', 'HEAD']).output.trim();
1460
+ // 4. Pull
1461
+ const pullRes = runQuiet('git', ['pull', '--ff-only', 'origin', targetBranch], 60000);
1462
+ if (!pullRes.ok)
1463
+ return textResult(`git pull failed: ${pullRes.output.slice(0, 300)}\n\n${stashed ? 'Local changes are preserved in git stash; inspect with `git stash list`.' : ''}`);
1464
+ const postCommit = runQuiet('git', ['rev-parse', 'HEAD']).output.trim();
1465
+ if (preCommit === postCommit) {
1466
+ out.push(`Already up to date at ${preCommit.slice(0, 7)}.`);
1467
+ return textResult(out.join('\n'));
1468
+ }
1469
+ out.push(`Pulled: ${preCommit.slice(0, 7)} → ${postCommit.slice(0, 7)}`);
1470
+ // 5. npm install only if package-lock changed
1471
+ const postLock = existsSync(path.join(root, 'package-lock.json')) ? readFileSync(path.join(root, 'package-lock.json'), 'utf-8').length : 0;
1472
+ if (postLock !== preLock) {
1473
+ out.push('package-lock.json changed — running npm install...');
1474
+ const installRes = runQuiet('npm', ['install'], 180000);
1475
+ if (!installRes.ok)
1476
+ return textResult(`${out.join('\n')}\n\nnpm install failed: ${installRes.output.slice(0, 300)}`);
1477
+ out.push(' ok');
1478
+ }
1479
+ // 6. Build
1480
+ out.push('Building...');
1481
+ const buildRes = runQuiet('npm', ['run', 'build'], 300000);
1482
+ if (!buildRes.ok)
1483
+ return textResult(`${out.join('\n')}\n\nBuild failed: ${buildRes.output.slice(0, 300)}`);
1484
+ out.push(' ok');
1485
+ // 7. Trigger graceful self-restart
1486
+ const pidFile = path.join(BASE_DIR, `.${(env['ASSISTANT_NAME'] ?? 'clementine').toLowerCase()}.pid`);
1487
+ if (existsSync(pidFile)) {
1488
+ const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
1489
+ if (!isNaN(pid)) {
1490
+ try {
1491
+ process.kill(pid, 0);
1492
+ process.kill(pid, 'SIGUSR1');
1493
+ out.push(`Sent SIGUSR1 to PID ${pid}. I'll be back in ~15s on the new build.`);
1494
+ }
1495
+ catch {
1496
+ out.push('Daemon PID not running — no restart signal sent. New build is on disk; launch manually.');
1497
+ }
1498
+ }
1499
+ }
1500
+ else {
1501
+ out.push('No PID file found. Build succeeded but restart not triggered — run `clementine launch` manually.');
1502
+ }
1503
+ return textResult(out.join('\n'));
1504
+ });
1505
+ server.tool('self_restart', 'Restart the Clementine daemon to pick up code changes. Sends SIGUSR1 to the running process, which triggers a graceful restart. Use self_update instead if you also need to pull/build first.', { _empty: z.string().optional().describe('(no parameters needed)') }, async () => {
1386
1506
  const pidFile = path.join(BASE_DIR, `.${(env['ASSISTANT_NAME'] ?? 'clementine').toLowerCase()}.pid`);
1387
1507
  if (!existsSync(pidFile)) {
1388
1508
  return textResult('No PID file found — daemon may not be running.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.0.46",
3
+ "version": "1.0.47",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",