clementine-agent 1.0.46 → 1.0.48
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 +15 -6
- package/dist/agent/mcp-bridge.js +13 -0
- package/dist/tools/admin-tools.js +122 -2
- package/package.json +1 -1
package/dist/agent/assistant.js
CHANGED
|
@@ -1075,16 +1075,23 @@ 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
|
-
###
|
|
1078
|
+
### Self-update (when ${owner} asks you to update yourself)
|
|
1079
1079
|
|
|
1080
|
-
|
|
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
1081
|
|
|
1082
|
-
|
|
1083
|
-
2. Retry the original call
|
|
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.
|
|
1084
1083
|
|
|
1085
|
-
|
|
1084
|
+
### Calling Claude Desktop connector tools (Drive, Gmail, etc.)
|
|
1086
1085
|
|
|
1087
|
-
\`
|
|
1086
|
+
**Do not inspect \`claude-integrations.json\` or any integration file to decide whether a tool is "loaded."** That file is a telemetry log, not a source of truth. Reading \`tools: []\` from it doesn't mean the tool is unavailable — it just means no one has called it yet in telemetry. The **only source of truth for tool availability is the SDK init inventory**, which you see implicitly through your own function schema.
|
|
1087
|
+
|
|
1088
|
+
The right sequence when the user asks you to do something with a connector:
|
|
1089
|
+
|
|
1090
|
+
1. **Just call the tool.** \`mcp__claude_ai_Google_Drive__search_files\`, \`mcp__claude_ai_Gmail__authenticate\`, etc. — attempt the call. If it works, great. If it returns an auth error, report the auth error. If it returns results, use them.
|
|
1091
|
+
2. **Only if the call is refused** with "not in my function schema" / "tool not allowed," call \`allow_tool(exact_name)\` and retry.
|
|
1092
|
+
3. **Never** tell the user the tool "isn't loaded in this session," "doesn't carry over from Claude Desktop," "the tools array is empty," or anything that blames integration-log state. Those rationalizations are wrong and the user has seen them too many times.
|
|
1093
|
+
|
|
1094
|
+
\`list_allowed_tools\` / \`disallow_tool\` manage the whitelist. Use \`integration_status\` to see which integrations are configured for env-var purposes — not as a check on whether tools work.
|
|
1088
1095
|
|
|
1089
1096
|
## Context Window Management
|
|
1090
1097
|
|
|
@@ -1396,6 +1403,8 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1396
1403
|
mcpTool('list_allowed_tools'),
|
|
1397
1404
|
mcpTool('disallow_tool'),
|
|
1398
1405
|
mcpTool('self_restart'),
|
|
1406
|
+
mcpTool('self_update'),
|
|
1407
|
+
mcpTool('where_is_source'),
|
|
1399
1408
|
mcpTool('cron_list'),
|
|
1400
1409
|
mcpTool('add_cron_job'),
|
|
1401
1410
|
mcpTool('memory_report'),
|
package/dist/agent/mcp-bridge.js
CHANGED
|
@@ -443,6 +443,19 @@ export async function probeAvailableTools(force = false) {
|
|
|
443
443
|
}
|
|
444
444
|
const inv = { probedAt: new Date().toISOString(), tools };
|
|
445
445
|
saveToolInventory(inv);
|
|
446
|
+
// Also sync claude-integrations.json so the agent-facing integration list
|
|
447
|
+
// stays consistent with what the SDK actually has available. Without this
|
|
448
|
+
// sync, the integrations file stays empty for freshly-connected services
|
|
449
|
+
// (e.g. Google_Drive tools: []) even though the inventory probe sees
|
|
450
|
+
// them — and the agent reads the integrations file, sees empty, and
|
|
451
|
+
// confabulates that the tools aren't loaded.
|
|
452
|
+
try {
|
|
453
|
+
const result = registerClaudeIntegrationsFromToolList(tools);
|
|
454
|
+
if (result.added.length + result.updated.length > 0) {
|
|
455
|
+
logger.info({ added: result.added, updated: result.updated }, 'Synced integrations from probed tool inventory');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch { /* non-fatal */ }
|
|
446
459
|
logger.info({ toolCount: tools.length }, 'Tool inventory probed');
|
|
447
460
|
return inv;
|
|
448
461
|
}
|
|
@@ -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.');
|