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.
- package/dist/agent/assistant.js +8 -0
- package/dist/tools/admin-tools.js +122 -2
- package/package.json +1 -1
package/dist/agent/assistant.js
CHANGED
|
@@ -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
|
-
|
|
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.');
|