@integrity-labs/agt-cli 0.10.0 → 0.10.2
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/bin/agt.js +242 -8
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-2TSCVXHE.js → chunk-QU7FBXH3.js} +4 -2
- package/dist/chunk-QU7FBXH3.js.map +1 -0
- package/dist/{chunk-UWH2MMKY.js → chunk-VCKY6MN2.js} +400 -17
- package/dist/chunk-VCKY6MN2.js.map +1 -0
- package/dist/{claude-scheduler-VFBZFE6U.js → claude-scheduler-7PVWQHWU.js} +2 -2
- package/dist/lib/manager-worker.js +385 -99
- package/dist/lib/manager-worker.js.map +1 -1
- package/mcp/direct-chat-channel.js +13985 -0
- package/mcp/index.js +181 -0
- package/package.json +1 -1
- package/dist/chunk-2TSCVXHE.js.map +0 -1
- package/dist/chunk-UWH2MMKY.js.map +0 -1
- /package/dist/{claude-scheduler-VFBZFE6U.js.map → claude-scheduler-7PVWQHWU.js.map} +0 -0
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
getFramework,
|
|
7
7
|
getHostId,
|
|
8
8
|
provision,
|
|
9
|
+
provisionIsolationHook,
|
|
9
10
|
provisionStopHook,
|
|
10
11
|
requireHost,
|
|
11
12
|
resolveChannels
|
|
12
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-VCKY6MN2.js";
|
|
13
14
|
import {
|
|
14
15
|
findTaskByTemplate,
|
|
15
16
|
getProjectDir,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
loadSchedulerState,
|
|
18
19
|
markTaskFired,
|
|
19
20
|
syncTasksToScheduler
|
|
20
|
-
} from "../chunk-
|
|
21
|
+
} from "../chunk-QU7FBXH3.js";
|
|
21
22
|
import {
|
|
22
23
|
getProjectDir as getProjectDir2,
|
|
23
24
|
injectMessage,
|
|
@@ -37,6 +38,38 @@ import { join, dirname } from "path";
|
|
|
37
38
|
import { homedir } from "os";
|
|
38
39
|
import { fileURLToPath } from "url";
|
|
39
40
|
|
|
41
|
+
// src/lib/plugin-context-render.ts
|
|
42
|
+
var PLUGIN_CONTEXT_PLACEHOLDER_RE = /\{\{\s*context\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
|
|
43
|
+
var TEAM_OVERRIDES_HEADER = "## Team Overrides\n\n> **The following overrides anything you've read above.** If any rule here conflicts with earlier instructions in this skill, follow what is written here. These are user-supplied directives that take precedence over the plugin's default guidance.\n";
|
|
44
|
+
function formatContextValue(value) {
|
|
45
|
+
if (value === null || value === void 0) return "";
|
|
46
|
+
if (typeof value === "string") return value;
|
|
47
|
+
if (typeof value === "boolean" || typeof value === "number") return String(value);
|
|
48
|
+
try {
|
|
49
|
+
return JSON.stringify(value);
|
|
50
|
+
} catch {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function renderPluginSkillContent(raw, values, overrides, warn = () => {
|
|
55
|
+
}) {
|
|
56
|
+
const substituted = raw.replace(PLUGIN_CONTEXT_PLACEHOLDER_RE, (_match, fieldName) => {
|
|
57
|
+
if (!(fieldName in values)) {
|
|
58
|
+
warn(`unresolved placeholder {{context.${fieldName}}} \u2014 substituting empty string`);
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
return formatContextValue(values[fieldName]);
|
|
62
|
+
});
|
|
63
|
+
const trimmedOverrides = overrides.trim();
|
|
64
|
+
if (!trimmedOverrides) return substituted;
|
|
65
|
+
const separator = substituted.endsWith("\n") ? "\n" : "\n\n";
|
|
66
|
+
return `${substituted}${separator}---
|
|
67
|
+
|
|
68
|
+
${TEAM_OVERRIDES_HEADER}
|
|
69
|
+
${trimmedOverrides}
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
|
|
40
73
|
// src/lib/gateway-client.ts
|
|
41
74
|
import { EventEmitter } from "events";
|
|
42
75
|
import WebSocket from "ws";
|
|
@@ -314,6 +347,7 @@ var driftChannel = null;
|
|
|
314
347
|
var assignChannel = null;
|
|
315
348
|
var configChannel = null;
|
|
316
349
|
var kanbanChannel = null;
|
|
350
|
+
var pluginContextChannel = null;
|
|
317
351
|
var connected = false;
|
|
318
352
|
var tearingDown = false;
|
|
319
353
|
function ensureClient(config2) {
|
|
@@ -358,6 +392,7 @@ function startRealtimeChat(config2) {
|
|
|
358
392
|
assignChannel = null;
|
|
359
393
|
configChannel = null;
|
|
360
394
|
kanbanChannel = null;
|
|
395
|
+
pluginContextChannel = null;
|
|
361
396
|
if (client) {
|
|
362
397
|
try {
|
|
363
398
|
client.removeAllChannels();
|
|
@@ -489,6 +524,38 @@ function startRealtimeKanban(config2) {
|
|
|
489
524
|
});
|
|
490
525
|
log2(`[realtime] Subscribing to agent_kanban_items for ${agentIds.length} agent(s)`);
|
|
491
526
|
}
|
|
527
|
+
function startRealtimePluginContext(config2) {
|
|
528
|
+
const { agentIds, onContextChange, log: log2 } = config2;
|
|
529
|
+
if (agentIds.length === 0) return;
|
|
530
|
+
const sb = ensureClient(config2);
|
|
531
|
+
const filterStr = agentIds.length === 1 ? `agent_id=eq.${agentIds[0]}` : `agent_id=in.(${agentIds.join(",")})`;
|
|
532
|
+
pluginContextChannel = sb.channel("plugin-context-realtime").on("postgres_changes", {
|
|
533
|
+
event: "INSERT",
|
|
534
|
+
schema: "public",
|
|
535
|
+
table: "plugin_context",
|
|
536
|
+
filter: filterStr
|
|
537
|
+
}, (payload) => {
|
|
538
|
+
const row = payload.new;
|
|
539
|
+
log2(`[realtime] plugin_context INSERT for agent ${row.agent_id} (plugin ${row.plugin_id})`);
|
|
540
|
+
onContextChange(row);
|
|
541
|
+
}).on("postgres_changes", {
|
|
542
|
+
event: "UPDATE",
|
|
543
|
+
schema: "public",
|
|
544
|
+
table: "plugin_context",
|
|
545
|
+
filter: filterStr
|
|
546
|
+
}, (payload) => {
|
|
547
|
+
const row = payload.new;
|
|
548
|
+
log2(`[realtime] plugin_context UPDATE for agent ${row.agent_id} (plugin ${row.plugin_id})`);
|
|
549
|
+
onContextChange(row);
|
|
550
|
+
}).subscribe((status) => {
|
|
551
|
+
if (status === "SUBSCRIBED") {
|
|
552
|
+
log2("[realtime] Plugin context channel connected");
|
|
553
|
+
} else if (status === "CLOSED" || status === "CHANNEL_ERROR") {
|
|
554
|
+
log2(`[realtime] Plugin context channel: ${status}`);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
log2(`[realtime] Subscribing to plugin_context for ${agentIds.length} agent(s)`);
|
|
558
|
+
}
|
|
492
559
|
function isRealtimeConnected() {
|
|
493
560
|
return connected;
|
|
494
561
|
}
|
|
@@ -531,6 +598,13 @@ function stopRealtimeChat() {
|
|
|
531
598
|
}
|
|
532
599
|
kanbanChannel = null;
|
|
533
600
|
}
|
|
601
|
+
if (pluginContextChannel) {
|
|
602
|
+
try {
|
|
603
|
+
pluginContextChannel.unsubscribe();
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
pluginContextChannel = null;
|
|
607
|
+
}
|
|
534
608
|
if (client) {
|
|
535
609
|
try {
|
|
536
610
|
client.removeAllChannels();
|
|
@@ -818,6 +892,76 @@ function hashFile(filePath) {
|
|
|
818
892
|
return null;
|
|
819
893
|
}
|
|
820
894
|
}
|
|
895
|
+
var SKILLS_INDEX_START = "<!-- AGT:SKILLS_INDEX_START -->";
|
|
896
|
+
var SKILLS_INDEX_END = "<!-- AGT:SKILLS_INDEX_END -->";
|
|
897
|
+
function sanitizeSkillsIndexText(value) {
|
|
898
|
+
return value.replaceAll(SKILLS_INDEX_START, "").replaceAll(SKILLS_INDEX_END, "").replace(/<!--[\s\S]*?-->/g, "").replace(/\r?\n+/g, " ").trim();
|
|
899
|
+
}
|
|
900
|
+
function parseSkillFrontmatter(content) {
|
|
901
|
+
if (!content.startsWith("---")) return {};
|
|
902
|
+
const end = content.indexOf("\n---", 3);
|
|
903
|
+
if (end === -1) return {};
|
|
904
|
+
const block = content.slice(3, end);
|
|
905
|
+
const out = {};
|
|
906
|
+
for (const line of block.split("\n")) {
|
|
907
|
+
const m = line.match(/^(name|description)\s*:\s*(.*)$/);
|
|
908
|
+
if (m && m[1] && m[2] !== void 0) out[m[1]] = m[2].trim().replace(/^["']|["']$/g, "");
|
|
909
|
+
}
|
|
910
|
+
return out;
|
|
911
|
+
}
|
|
912
|
+
async function refreshSkillsIndexInClaudeMd(configDir, codeName, log2) {
|
|
913
|
+
const { readdirSync: readdirSync2, readFileSync: rfs, existsSync: ex, writeFileSync: writeFileSync2 } = await import("fs");
|
|
914
|
+
const skillsDir = join(configDir, codeName, "project", ".claude", "skills");
|
|
915
|
+
const claudeMdPath = join(configDir, codeName, "project", "CLAUDE.md");
|
|
916
|
+
if (!ex(skillsDir) || !ex(claudeMdPath)) return;
|
|
917
|
+
const entries = [];
|
|
918
|
+
for (const dir of readdirSync2(skillsDir).sort()) {
|
|
919
|
+
const skillFile = join(skillsDir, dir, "SKILL.md");
|
|
920
|
+
if (!ex(skillFile)) continue;
|
|
921
|
+
try {
|
|
922
|
+
const { name, description } = parseSkillFrontmatter(rfs(skillFile, "utf-8"));
|
|
923
|
+
entries.push({
|
|
924
|
+
id: dir,
|
|
925
|
+
name: sanitizeSkillsIndexText(name ?? dir),
|
|
926
|
+
description: sanitizeSkillsIndexText(description ?? "(no description)")
|
|
927
|
+
});
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const body = entries.length ? entries.map((e) => `- **${e.name}** \u2014 ${e.description}`).join("\n") : "_(no skills installed)_";
|
|
932
|
+
const section = `${SKILLS_INDEX_START}
|
|
933
|
+
## Available Skills
|
|
934
|
+
|
|
935
|
+
The following skills are installed in \`.claude/skills/\`. Claude Code auto-activates them when relevant \u2014 you don't need to read them manually, but you should know they exist.
|
|
936
|
+
|
|
937
|
+
${body}
|
|
938
|
+
|
|
939
|
+
## Updating Plugins (ENG-4341)
|
|
940
|
+
|
|
941
|
+
Plugin skills under \`.claude/skills/plugin-*/SKILL.md\` are **read-only** and managed by the platform. They are derived from the plugin's database row plus any per-agent context overrides, and are re-rendered every time the manager polls or a context change is broadcast over Supabase Realtime.
|
|
942
|
+
|
|
943
|
+
**Never edit \`.claude/skills/plugin-*/SKILL.md\` files directly.** If you do, your edit will be silently overwritten on the next manager refresh, AND it won't propagate to other agents using the same plugin.
|
|
944
|
+
|
|
945
|
+
To change a plugin's behavior (add a rule, update a default, tell the plugin not to do something), call the **\`plugin.improve\`** MCP tool with the user's request. The tool calls the platform API, which uses an LLM to translate the request into a structured update of the plugin's typed context fields and/or freeform overrides text. You'll get a diff back to show the user; on confirmation, call the tool again with \`auto_apply: true\` to apply.
|
|
946
|
+
|
|
947
|
+
Examples of when to use \`plugin.improve\`:
|
|
948
|
+
- *"Update the Coding plugin so we always use trunk instead of main"*
|
|
949
|
+
- *"Add a rule to the Coding plugin: never merge directly to main, always open a PR"*
|
|
950
|
+
- *"Tell the Knowledge Base plugin not to return results below 70% relevance"*
|
|
951
|
+
- *"The Slack plugin should post incidents to #oncall, not #general"*
|
|
952
|
+
${SKILLS_INDEX_END}`;
|
|
953
|
+
const current = rfs(claudeMdPath, "utf-8");
|
|
954
|
+
let next;
|
|
955
|
+
if (current.includes(SKILLS_INDEX_START) && current.includes(SKILLS_INDEX_END)) {
|
|
956
|
+
next = current.replace(new RegExp(`${SKILLS_INDEX_START}[\\s\\S]*?${SKILLS_INDEX_END}`), section);
|
|
957
|
+
} else {
|
|
958
|
+
next = current.trimEnd() + "\n\n" + section + "\n";
|
|
959
|
+
}
|
|
960
|
+
if (next !== current) {
|
|
961
|
+
writeFileSync2(claudeMdPath, next, "utf-8");
|
|
962
|
+
log2(`Refreshed skills index in CLAUDE.md for '${codeName}' (${entries.length} skills)`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
821
965
|
async function migrateToProfiles() {
|
|
822
966
|
const homeDir = process.env["HOME"] ?? "/tmp";
|
|
823
967
|
const sharedConfigPath = join(homeDir, ".openclaw", "openclaw.json");
|
|
@@ -1186,6 +1330,7 @@ async function pollCycle() {
|
|
|
1186
1330
|
ensureRealtimeAssignStarted(agentStates);
|
|
1187
1331
|
ensureRealtimeConfigStarted(agentStates);
|
|
1188
1332
|
ensureRealtimeKanbanStarted(agentStates);
|
|
1333
|
+
ensureRealtimePluginContextStarted(agentStates);
|
|
1189
1334
|
try {
|
|
1190
1335
|
const spawnData = await api.post("/host/kanban/recurring/spawn");
|
|
1191
1336
|
if (spawnData.spawned > 0) {
|
|
@@ -1527,6 +1672,36 @@ async function processAgent(agent, agentStates) {
|
|
|
1527
1672
|
}
|
|
1528
1673
|
}
|
|
1529
1674
|
}
|
|
1675
|
+
const agentSessionMode = refreshData.agent.session_mode;
|
|
1676
|
+
if (agentSessionMode === "persistent" && (agentFrameworkCache.get(agent.code_name) ?? "openclaw") === "claude-code") {
|
|
1677
|
+
try {
|
|
1678
|
+
const projectDir = join(homedir(), ".augmented", agent.code_name, "project");
|
|
1679
|
+
mkdirSync(projectDir, { recursive: true });
|
|
1680
|
+
const channelsPath = join(projectDir, ".mcp-channels.json");
|
|
1681
|
+
let channelsMcp = { mcpServers: {} };
|
|
1682
|
+
try {
|
|
1683
|
+
channelsMcp = JSON.parse(readFileSync(channelsPath, "utf-8"));
|
|
1684
|
+
if (!channelsMcp.mcpServers) channelsMcp.mcpServers = {};
|
|
1685
|
+
} catch {
|
|
1686
|
+
}
|
|
1687
|
+
const localDirectChatChannel = join(homedir(), ".augmented", "_mcp", "direct-chat-channel.js");
|
|
1688
|
+
if (existsSync(localDirectChatChannel) && !channelsMcp.mcpServers["direct-chat"]) {
|
|
1689
|
+
channelsMcp.mcpServers["direct-chat"] = {
|
|
1690
|
+
command: "node",
|
|
1691
|
+
args: [localDirectChatChannel],
|
|
1692
|
+
env: {
|
|
1693
|
+
AGT_HOST: requireHost(),
|
|
1694
|
+
AGT_API_KEY: getApiKey() ?? "",
|
|
1695
|
+
AGT_AGENT_ID: agent.agent_id
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
writeFileSync(channelsPath, JSON.stringify(channelsMcp, null, 2));
|
|
1699
|
+
log(`Channel credentials written for '${agent.code_name}/direct-chat'`);
|
|
1700
|
+
}
|
|
1701
|
+
} catch (err) {
|
|
1702
|
+
log(`Failed to provision direct-chat channel for '${agent.code_name}': ${err.message}`);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1530
1705
|
let lastSecretsProvisionAt = state.agents.find((a) => a.agentId === agent.agent_id)?.lastSecretsProvisionAt ?? null;
|
|
1531
1706
|
let secretsHash = knownSecretsHashes.get(agent.agent_id) ?? null;
|
|
1532
1707
|
try {
|
|
@@ -1604,7 +1779,7 @@ async function processAgent(agent, agentStates) {
|
|
|
1604
1779
|
const expectedServerIds = /* @__PURE__ */ new Set();
|
|
1605
1780
|
for (const tk of toolkitData.toolkits) {
|
|
1606
1781
|
if (tk.agent_id !== agent.agent_id) continue;
|
|
1607
|
-
const serverId = tk.toolkit_id.replace(
|
|
1782
|
+
const serverId = tk.toolkit_id.replace(/[^a-z0-9]/gi, "_").toLowerCase();
|
|
1608
1783
|
expectedServerIds.add(serverId);
|
|
1609
1784
|
const mcpUrl = tk.mcp_url;
|
|
1610
1785
|
const mcpHeaders = tk.mcp_headers;
|
|
@@ -1621,7 +1796,19 @@ async function processAgent(agent, agentStates) {
|
|
|
1621
1796
|
const mcpPath = join2(homedir2(), ".augmented", "agents", agent.code_name, "provision", ".mcp.json");
|
|
1622
1797
|
const mcpConfig = JSON.parse(readFileSync2(mcpPath, "utf-8"));
|
|
1623
1798
|
if (mcpConfig.mcpServers) {
|
|
1624
|
-
const managedPrefixes = [
|
|
1799
|
+
const managedPrefixes = [
|
|
1800
|
+
"composio_",
|
|
1801
|
+
"one_",
|
|
1802
|
+
"pipedream_",
|
|
1803
|
+
"nango_",
|
|
1804
|
+
"paragon_",
|
|
1805
|
+
// Legacy hyphenated format
|
|
1806
|
+
"composio-",
|
|
1807
|
+
"one-",
|
|
1808
|
+
"pipedream-",
|
|
1809
|
+
"nango-",
|
|
1810
|
+
"paragon-"
|
|
1811
|
+
];
|
|
1625
1812
|
for (const key of Object.keys(mcpConfig.mcpServers)) {
|
|
1626
1813
|
if (managedPrefixes.some((p) => key.startsWith(p)) && !expectedServerIds.has(key)) {
|
|
1627
1814
|
frameworkAdapter.removeMcpServer(agent.code_name, key);
|
|
@@ -1657,93 +1844,6 @@ async function processAgent(agent, agentStates) {
|
|
|
1657
1844
|
}, delay);
|
|
1658
1845
|
}
|
|
1659
1846
|
}
|
|
1660
|
-
const resolvedDefIds = new Set(integrations.map((i) => i.definition_id));
|
|
1661
|
-
for (const capId of resolvedDefIds) {
|
|
1662
|
-
try {
|
|
1663
|
-
const capData = await api.post("/host/capability-skill", { definition_id: capId });
|
|
1664
|
-
const allSatisfied = capData.requiredIntegrations.every((reqId) => resolvedDefIds.has(reqId));
|
|
1665
|
-
if (!allSatisfied) {
|
|
1666
|
-
const missing = capData.requiredIntegrations.filter((reqId) => !resolvedDefIds.has(reqId));
|
|
1667
|
-
log(`Capability '${capId}' skipped \u2014 missing integration(s): ${missing.join(", ")}`);
|
|
1668
|
-
continue;
|
|
1669
|
-
}
|
|
1670
|
-
const skillContent = JSON.stringify(capData.skills.map((s) => s.files.map((f) => `${f.relativePath}:${f.content}`)));
|
|
1671
|
-
const skillHash = createHash("sha256").update(skillContent).digest("hex").slice(0, 16);
|
|
1672
|
-
const skillKey = `${agent.agent_id}:${capId}`;
|
|
1673
|
-
const prevSkillHash = knownSkillHashes.get(skillKey);
|
|
1674
|
-
if (skillHash !== prevSkillHash) {
|
|
1675
|
-
const installedSkillNames = [];
|
|
1676
|
-
if (frameworkAdapter.installSkillFiles && capData.skills) {
|
|
1677
|
-
for (const skill of capData.skills) {
|
|
1678
|
-
if (skill.files.length > 0) {
|
|
1679
|
-
frameworkAdapter.installSkillFiles(agent.code_name, skill.id, skill.files);
|
|
1680
|
-
installedSkillNames.push(skill.name || skill.id);
|
|
1681
|
-
log(`Installed skill '${skill.id}' for '${agent.code_name}' (${skill.files.length} file(s))`);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
knownSkillHashes.set(skillKey, skillHash);
|
|
1686
|
-
const agentFw2 = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
|
|
1687
|
-
if (agentFw2 === "claude-code" && installedSkillNames.length > 0 && isSessionHealthy(agent.code_name)) {
|
|
1688
|
-
const names = installedSkillNames.join(", ");
|
|
1689
|
-
injectMessage(
|
|
1690
|
-
agent.code_name,
|
|
1691
|
-
"system",
|
|
1692
|
-
`New skills installed: ${names}. These are available immediately \u2014 Claude Code loads skills on demand from .claude/skills/.`,
|
|
1693
|
-
{ task_name: "skill-update" },
|
|
1694
|
-
log
|
|
1695
|
-
).catch(() => {
|
|
1696
|
-
});
|
|
1697
|
-
log(`[hot-reload] Notified '${agent.code_name}' about new skills: ${names}`);
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
const ALLOWED_CLI_PACKAGES = /* @__PURE__ */ new Set([
|
|
1701
|
-
"xero-cli",
|
|
1702
|
-
"@openapitools/openapi-generator-cli",
|
|
1703
|
-
"gh",
|
|
1704
|
-
"@schpet/linear-cli",
|
|
1705
|
-
"@googleworkspace/cli",
|
|
1706
|
-
"@tobilu/qmd"
|
|
1707
|
-
]);
|
|
1708
|
-
if (intHash !== prevIntHash) {
|
|
1709
|
-
const { execFileSync } = await import("child_process");
|
|
1710
|
-
for (const tool of capData.cliTools) {
|
|
1711
|
-
if (!ALLOWED_CLI_PACKAGES.has(tool.package)) {
|
|
1712
|
-
log(`Skipping CLI tool '${tool.package}' for '${agent.code_name}' \u2014 not on the allowed packages list`);
|
|
1713
|
-
continue;
|
|
1714
|
-
}
|
|
1715
|
-
try {
|
|
1716
|
-
execFileSync("which", [tool.binary], { stdio: "ignore" });
|
|
1717
|
-
} catch {
|
|
1718
|
-
log(`Installing CLI tool '${tool.package}' for '${agent.code_name}'...`);
|
|
1719
|
-
try {
|
|
1720
|
-
execFileSync("npm", ["install", "-g", tool.package], { stdio: "ignore", timeout: 6e4 });
|
|
1721
|
-
log(`CLI tool '${tool.binary}' installed successfully`);
|
|
1722
|
-
} catch (installErr) {
|
|
1723
|
-
log(`Failed to install CLI tool '${tool.package}': ${installErr.message}`);
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
if (tool.binary === "qmd") {
|
|
1727
|
-
try {
|
|
1728
|
-
const agentDir2 = join(process.env["HOME"] ?? "/tmp", ".augmented", agent.code_name);
|
|
1729
|
-
execFileSync("qmd", ["collection", "add", agent.code_name, "project"], {
|
|
1730
|
-
stdio: "ignore",
|
|
1731
|
-
timeout: 3e4,
|
|
1732
|
-
cwd: agentDir2
|
|
1733
|
-
});
|
|
1734
|
-
log(`QMD collection '${agent.code_name}' configured`);
|
|
1735
|
-
} catch {
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
} catch (skillErr) {
|
|
1741
|
-
const msg = skillErr.message ?? "";
|
|
1742
|
-
if (!msg.includes("404") && !msg.includes("Unknown capability")) {
|
|
1743
|
-
log(`Capability fetch failed for '${capId}': ${msg}`);
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
1847
|
}
|
|
1748
1848
|
} catch (err) {
|
|
1749
1849
|
log(`Integration provisioning failed for '${agent.code_name}': ${err.message}`);
|
|
@@ -1808,6 +1908,107 @@ async function processAgent(agent, agentStates) {
|
|
|
1808
1908
|
log(`Kanban skill install failed for '${agent.code_name}': ${err.message}`);
|
|
1809
1909
|
}
|
|
1810
1910
|
}
|
|
1911
|
+
if (frameworkAdapter.installSkillFiles) {
|
|
1912
|
+
const currentPluginSkillIds = /* @__PURE__ */ new Set();
|
|
1913
|
+
const installedPluginSkills = [];
|
|
1914
|
+
const { createHash: createHash2 } = await import("crypto");
|
|
1915
|
+
const contextBySlug = /* @__PURE__ */ new Map();
|
|
1916
|
+
for (const ctx of refreshData.plugin_contexts ?? []) {
|
|
1917
|
+
contextBySlug.set(ctx.plugin_slug, { values: ctx.values ?? {}, overrides: (ctx.overrides ?? "").trim() });
|
|
1918
|
+
}
|
|
1919
|
+
for (const ps of refreshData.plugin_skills ?? []) {
|
|
1920
|
+
try {
|
|
1921
|
+
const skillId = `plugin-${ps.plugin_slug}-${ps.skill_id}`.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1922
|
+
currentPluginSkillIds.add(skillId);
|
|
1923
|
+
const ctx = contextBySlug.get(ps.plugin_slug);
|
|
1924
|
+
const renderedContent = renderPluginSkillContent(
|
|
1925
|
+
ps.content,
|
|
1926
|
+
ctx?.values ?? {},
|
|
1927
|
+
ctx?.overrides ?? "",
|
|
1928
|
+
(warning) => log(`[plugin-context] ${ps.plugin_slug}/${ps.skill_id}: ${warning}`)
|
|
1929
|
+
);
|
|
1930
|
+
const contentHash = createHash2("sha256").update(renderedContent).digest("hex").slice(0, 12);
|
|
1931
|
+
const hashKey = `plugin-skill:${agent.code_name}:${skillId}`;
|
|
1932
|
+
if (knownSkillHashes.get(hashKey) === contentHash) continue;
|
|
1933
|
+
frameworkAdapter.installSkillFiles(agent.code_name, skillId, [
|
|
1934
|
+
{ relativePath: "SKILL.md", content: renderedContent }
|
|
1935
|
+
]);
|
|
1936
|
+
knownSkillHashes.set(hashKey, contentHash);
|
|
1937
|
+
installedPluginSkills.push(ps.skill_name);
|
|
1938
|
+
log(`Installed plugin skill '${skillId}' for '${agent.code_name}'`);
|
|
1939
|
+
} catch (err) {
|
|
1940
|
+
log(`Plugin skill install failed for '${agent.code_name}' / '${ps.skill_id}': ${err.message}`);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
try {
|
|
1944
|
+
const agentSkillsDir = join(config.configDir, agent.code_name, "project", ".claude", "skills");
|
|
1945
|
+
if (existsSync(agentSkillsDir)) {
|
|
1946
|
+
const { readdirSync: readdirSync2, rmSync: rmSync2 } = await import("fs");
|
|
1947
|
+
for (const entry of readdirSync2(agentSkillsDir)) {
|
|
1948
|
+
if (entry.startsWith("plugin-") && !currentPluginSkillIds.has(entry)) {
|
|
1949
|
+
const orphanPath = join(agentSkillsDir, entry);
|
|
1950
|
+
rmSync2(orphanPath, { recursive: true, force: true });
|
|
1951
|
+
log(`Removed orphaned plugin skill '${entry}' for '${agent.code_name}'`);
|
|
1952
|
+
const provisionSkillPath = join(config.configDir, agent.code_name, "skills", entry);
|
|
1953
|
+
if (existsSync(provisionSkillPath)) {
|
|
1954
|
+
rmSync2(provisionSkillPath, { recursive: true, force: true });
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
} catch (err) {
|
|
1960
|
+
log(`Plugin skill cleanup failed for '${agent.code_name}': ${err.message}`);
|
|
1961
|
+
}
|
|
1962
|
+
try {
|
|
1963
|
+
const agentFwForIndex = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
|
|
1964
|
+
if (agentFwForIndex === "claude-code") {
|
|
1965
|
+
await refreshSkillsIndexInClaudeMd(config.configDir, agent.code_name, log);
|
|
1966
|
+
}
|
|
1967
|
+
} catch (err) {
|
|
1968
|
+
log(`Skills index refresh failed for '${agent.code_name}': ${err.message}`);
|
|
1969
|
+
}
|
|
1970
|
+
if (frameworkAdapter.executePluginHook && refreshData.plugin_install_hooks?.length) {
|
|
1971
|
+
for (const hook of refreshData.plugin_install_hooks) {
|
|
1972
|
+
try {
|
|
1973
|
+
const scriptHash = createHash2("sha256").update(hook.script).digest("hex").slice(0, 12);
|
|
1974
|
+
const hookKey = `${agent.agent_id}:${frameworkAdapter.id}:plugin-hook:${hook.plugin_slug}:on_install`;
|
|
1975
|
+
if (knownSkillHashes.get(hookKey) === scriptHash) continue;
|
|
1976
|
+
const result = await frameworkAdapter.executePluginHook({
|
|
1977
|
+
codeName: agent.code_name,
|
|
1978
|
+
pluginSlug: hook.plugin_slug,
|
|
1979
|
+
hookName: "on_install",
|
|
1980
|
+
script: hook.script
|
|
1981
|
+
});
|
|
1982
|
+
if (result.exitCode === 0) {
|
|
1983
|
+
knownSkillHashes.set(hookKey, scriptHash);
|
|
1984
|
+
log(`Plugin hook on_install '${hook.plugin_slug}' succeeded for '${agent.code_name}' (${result.durationMs}ms)`);
|
|
1985
|
+
} else if (result.timedOut) {
|
|
1986
|
+
log(`Plugin hook on_install '${hook.plugin_slug}' TIMED OUT for '${agent.code_name}' after ${result.durationMs}ms`);
|
|
1987
|
+
} else {
|
|
1988
|
+
const stderrHash = createHash2("sha256").update(result.stderr).digest("hex").slice(0, 12);
|
|
1989
|
+
log(
|
|
1990
|
+
`Plugin hook on_install '${hook.plugin_slug}' exited ${result.exitCode} for '${agent.code_name}' [stderr_hash=${stderrHash} stderr_len=${result.stderr.length}]`
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
log(`Plugin hook on_install failed for '${agent.code_name}' / '${hook.plugin_slug}': ${err.message}`);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
const agentFw2 = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
|
|
1999
|
+
if (agentFw2 === "claude-code" && installedPluginSkills.length > 0 && isSessionHealthy(agent.code_name)) {
|
|
2000
|
+
const names = installedPluginSkills.join(", ");
|
|
2001
|
+
injectMessage(
|
|
2002
|
+
agent.code_name,
|
|
2003
|
+
"system",
|
|
2004
|
+
`New plugin skills installed: ${names}. These are available immediately \u2014 Claude Code loads skills on demand from .claude/skills/.`,
|
|
2005
|
+
{ task_name: "plugin-skill-update" },
|
|
2006
|
+
log
|
|
2007
|
+
).catch(() => {
|
|
2008
|
+
});
|
|
2009
|
+
log(`[hot-reload] Notified '${agent.code_name}' about new plugin skills: ${names}`);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1811
2012
|
}
|
|
1812
2013
|
let boardItems = [];
|
|
1813
2014
|
const hasBoardTemplates = tasks.some((t) => BOARD_INJECT_TEMPLATES.has(t.template_id));
|
|
@@ -2309,6 +2510,12 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
|
|
|
2309
2510
|
});
|
|
2310
2511
|
const updated = markTaskFired(codeName, task.taskId, "ok");
|
|
2311
2512
|
claudeSchedulerStates.set(codeName, updated);
|
|
2513
|
+
if (task.scheduleKind === "at") {
|
|
2514
|
+
api.post("/host/schedules/disable", { agent_id: agentId, task_id: task.taskId }).catch(
|
|
2515
|
+
(err) => log(`[claude-scheduler] Failed to disable one-off task '${task.name}': ${err.message}`)
|
|
2516
|
+
);
|
|
2517
|
+
log(`[claude-scheduler] One-off task '${task.name}' fired and disabled for '${codeName}'`);
|
|
2518
|
+
}
|
|
2312
2519
|
} catch (err) {
|
|
2313
2520
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2314
2521
|
log(`[claude-scheduler] Task '${task.name}' failed for '${codeName}': ${errMsg}`);
|
|
@@ -2402,6 +2609,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
|
|
|
2402
2609
|
devChannels.push("server:slack");
|
|
2403
2610
|
}
|
|
2404
2611
|
}
|
|
2612
|
+
devChannels.push("server:direct-chat");
|
|
2405
2613
|
if (!agentRuntimeAuthenticated) {
|
|
2406
2614
|
const { execFileSync } = await import("child_process");
|
|
2407
2615
|
agentRuntimeAuthenticated = checkClaudeAuth(execFileSync);
|
|
@@ -2419,6 +2627,11 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
|
|
|
2419
2627
|
} catch (err) {
|
|
2420
2628
|
log(`[persistent-session] Failed to provision Stop hook for '${codeName}': ${err.message}`);
|
|
2421
2629
|
}
|
|
2630
|
+
try {
|
|
2631
|
+
provisionIsolationHook(codeName);
|
|
2632
|
+
} catch (err) {
|
|
2633
|
+
log(`[persistent-session] Failed to provision isolation hook for '${codeName}': ${err.message}`);
|
|
2634
|
+
}
|
|
2422
2635
|
startPersistentSession({
|
|
2423
2636
|
codeName,
|
|
2424
2637
|
agentId: agent.agent_id,
|
|
@@ -2508,6 +2721,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
|
|
|
2508
2721
|
var realtimeStarted = false;
|
|
2509
2722
|
var realtimeDriftStarted = false;
|
|
2510
2723
|
var realtimeKanbanStarted = false;
|
|
2724
|
+
var realtimePluginContextStarted = false;
|
|
2511
2725
|
var realtimeAssignStarted = false;
|
|
2512
2726
|
var realtimeConfigStarted = false;
|
|
2513
2727
|
var realtimeSubscribedAgentIds = /* @__PURE__ */ new Set();
|
|
@@ -2526,6 +2740,7 @@ function ensureRealtimeStarted(agentStates) {
|
|
|
2526
2740
|
realtimeAssignStarted = false;
|
|
2527
2741
|
realtimeConfigStarted = false;
|
|
2528
2742
|
realtimeKanbanStarted = false;
|
|
2743
|
+
realtimePluginContextStarted = false;
|
|
2529
2744
|
}
|
|
2530
2745
|
const activeAgentIds = agentStates.filter((a) => a.status === "active").map((a) => a.agentId);
|
|
2531
2746
|
if (activeAgentIds.length === 0) return;
|
|
@@ -2692,6 +2907,51 @@ function ensureRealtimeKanbanStarted(agentStates) {
|
|
|
2692
2907
|
log(`[realtime] Kanban subscription failed: ${err.message}`);
|
|
2693
2908
|
});
|
|
2694
2909
|
}
|
|
2910
|
+
function ensureRealtimePluginContextStarted(agentStates) {
|
|
2911
|
+
if (realtimePluginContextStarted) return;
|
|
2912
|
+
const activeAgentIds = agentStates.filter((a) => a.status === "active").map((a) => a.agentId);
|
|
2913
|
+
if (activeAgentIds.length === 0) return;
|
|
2914
|
+
const apiKey = process.env["AGT_API_KEY"];
|
|
2915
|
+
if (!apiKey) return;
|
|
2916
|
+
void exchangeApiKey(apiKey).then((exchange) => {
|
|
2917
|
+
if (!exchange.supabaseUrl || !exchange.supabaseAnonKey) return;
|
|
2918
|
+
startRealtimePluginContext({
|
|
2919
|
+
supabaseUrl: exchange.supabaseUrl,
|
|
2920
|
+
supabaseAnonKey: exchange.supabaseAnonKey,
|
|
2921
|
+
token: exchange.token,
|
|
2922
|
+
agentIds: activeAgentIds,
|
|
2923
|
+
onContextChange: (payload) => {
|
|
2924
|
+
const agent = agentStates.find((a) => a.agentId === payload.agent_id);
|
|
2925
|
+
if (agent) {
|
|
2926
|
+
for (const key of knownSkillHashes.keys()) {
|
|
2927
|
+
if (key.startsWith(`plugin-skill:${agent.codeName}:`)) {
|
|
2928
|
+
knownSkillHashes.delete(key);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
triggerEarlyPoll(`plugin context changed for agent ${payload.agent_id}`);
|
|
2933
|
+
},
|
|
2934
|
+
log
|
|
2935
|
+
});
|
|
2936
|
+
realtimePluginContextStarted = true;
|
|
2937
|
+
log(`[realtime] Plugin context subscription started for ${activeAgentIds.length} agent(s)`);
|
|
2938
|
+
}).catch((err) => {
|
|
2939
|
+
log(`[realtime] Plugin context subscription failed: ${err.message}`);
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
function triggerEarlyPoll(reason) {
|
|
2943
|
+
if (!running) return;
|
|
2944
|
+
if (pollTimer) {
|
|
2945
|
+
clearTimeout(pollTimer);
|
|
2946
|
+
pollTimer = null;
|
|
2947
|
+
}
|
|
2948
|
+
log(`[realtime] Triggering early poll: ${reason}`);
|
|
2949
|
+
pollTimer = setTimeout(() => {
|
|
2950
|
+
void pollCycle().then(() => {
|
|
2951
|
+
scheduleNext();
|
|
2952
|
+
});
|
|
2953
|
+
}, 0);
|
|
2954
|
+
}
|
|
2695
2955
|
var directChatInFlight = /* @__PURE__ */ new Set();
|
|
2696
2956
|
async function pollDirectChatMessages(agentStates) {
|
|
2697
2957
|
for (const agent of agentStates) {
|
|
@@ -2718,7 +2978,7 @@ async function processDirectChatMessage(agent, msg) {
|
|
|
2718
2978
|
try {
|
|
2719
2979
|
let reply;
|
|
2720
2980
|
if (fw === "claude-code") {
|
|
2721
|
-
const { getProjectDir: ccProjectDir } = await import("../claude-scheduler-
|
|
2981
|
+
const { getProjectDir: ccProjectDir } = await import("../claude-scheduler-7PVWQHWU.js");
|
|
2722
2982
|
const projDir = ccProjectDir(agent.codeName);
|
|
2723
2983
|
const mcpConfigPath = join(projDir, ".mcp.json");
|
|
2724
2984
|
const channelsConfigPath = join(projDir, ".mcp-channels.json");
|
|
@@ -2795,7 +3055,7 @@ async function processDirectChatMessage(agent, msg) {
|
|
|
2795
3055
|
} catch (err) {
|
|
2796
3056
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2797
3057
|
const errorId = createHash("sha256").update(errMsg).digest("hex").slice(0, 12);
|
|
2798
|
-
log(`[direct-chat] Failed to process message for '${agent.codeName}': error_id=${errorId}`);
|
|
3058
|
+
log(`[direct-chat] Failed to process message for '${agent.codeName}': error_id=${errorId} error=${errMsg.slice(0, 500)}`);
|
|
2799
3059
|
try {
|
|
2800
3060
|
await api.post("/host/direct-chat/reply", {
|
|
2801
3061
|
agent_id: agent.agentId,
|
|
@@ -3439,8 +3699,26 @@ function generateArtifacts(agent, refreshData, adapter) {
|
|
|
3439
3699
|
const teamTimezoneRaw = refreshData.team?.timezone;
|
|
3440
3700
|
const teamTimezone = typeof teamTimezoneRaw === "string" && teamTimezoneRaw.trim().length > 0 ? teamTimezoneRaw.trim() : void 0;
|
|
3441
3701
|
const agentTimezone = (taskTimezones.length === 1 ? taskTimezones[0] : void 0) ?? teamTimezone ?? "UTC";
|
|
3702
|
+
const agentPersonalitySeed = refreshData.agent.personality_seed;
|
|
3442
3703
|
const orgDefaults = refreshData.model_defaults;
|
|
3443
|
-
const personalitySeed = orgDefaults?.org?.settings?.personality_seed;
|
|
3704
|
+
const personalitySeed = agentPersonalitySeed || orgDefaults?.org?.settings?.personality_seed;
|
|
3705
|
+
let reportsTo;
|
|
3706
|
+
const agentData = refreshData.agent;
|
|
3707
|
+
const reportsToId = agentData.reports_to;
|
|
3708
|
+
const reportsToType = agentData.reports_to_type ?? "agent";
|
|
3709
|
+
if (reportsToId) {
|
|
3710
|
+
const reportsToName = agentData.reports_to_name;
|
|
3711
|
+
const reportsToTitle = agentData.reports_to_title;
|
|
3712
|
+
const reportsToDesc = agentData.reports_to_description;
|
|
3713
|
+
if (reportsToName) {
|
|
3714
|
+
reportsTo = {
|
|
3715
|
+
name: reportsToName,
|
|
3716
|
+
type: reportsToType,
|
|
3717
|
+
title: reportsToTitle,
|
|
3718
|
+
description: reportsToDesc
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3444
3722
|
const provisionInput = {
|
|
3445
3723
|
agent: refreshData.agent,
|
|
3446
3724
|
charterFrontmatter,
|
|
@@ -3550,7 +3828,8 @@ function startPolling() {
|
|
|
3550
3828
|
void startCaffeinate();
|
|
3551
3829
|
log(`Starting poll loop (interval=${config.intervalMs}ms, configDir=${config.configDir})`);
|
|
3552
3830
|
checkAndUpdateCli().catch((err) => log(`[self-update] Check failed: ${err.message}`));
|
|
3553
|
-
void
|
|
3831
|
+
void killAllAgtTmuxSessions().catch(() => {
|
|
3832
|
+
}).then(() => migrateToProfiles()).then(() => {
|
|
3554
3833
|
startGatewayPool();
|
|
3555
3834
|
return pollCycle();
|
|
3556
3835
|
}).then(() => {
|
|
@@ -3594,18 +3873,19 @@ async function stopPolling() {
|
|
|
3594
3873
|
pollTimer = null;
|
|
3595
3874
|
}
|
|
3596
3875
|
const shutdownTimer = setTimeout(() => {
|
|
3597
|
-
log("Shutdown timeout exceeded (
|
|
3876
|
+
log("Shutdown timeout exceeded (15s), forcing exit");
|
|
3598
3877
|
process.exit(1);
|
|
3599
|
-
},
|
|
3878
|
+
}, 15e3);
|
|
3600
3879
|
shutdownTimer.unref();
|
|
3601
3880
|
stopCaffeinate();
|
|
3602
3881
|
stopRealtimeChat();
|
|
3603
3882
|
stopGatewayPool();
|
|
3883
|
+
log("Killing tmux sessions...");
|
|
3884
|
+
await killAllAgtTmuxSessions();
|
|
3604
3885
|
log("Stopping persistent sessions...");
|
|
3605
3886
|
await stopAllSessionsAndWait(log, { timeoutMs: 4e3 });
|
|
3606
3887
|
log("Stopping gateway processes...");
|
|
3607
3888
|
await stopAllGateways();
|
|
3608
|
-
await killAllAgtTmuxSessions();
|
|
3609
3889
|
clearTimeout(shutdownTimer);
|
|
3610
3890
|
}
|
|
3611
3891
|
function startManager(opts) {
|
|
@@ -3633,7 +3913,7 @@ function deployMcpAssets() {
|
|
|
3633
3913
|
log("[manager] MCP assets not found in CLI package \u2014 skipping deployment");
|
|
3634
3914
|
return;
|
|
3635
3915
|
}
|
|
3636
|
-
for (const file of ["index.js", "slack-channel.js"]) {
|
|
3916
|
+
for (const file of ["index.js", "slack-channel.js", "direct-chat-channel.js"]) {
|
|
3637
3917
|
const src = join(mcpSourceDir, file);
|
|
3638
3918
|
const dst = join(targetDir, file);
|
|
3639
3919
|
if (!existsSync(src)) continue;
|
|
@@ -3674,8 +3954,14 @@ function deployMcpAssets() {
|
|
|
3674
3954
|
async function stopManager() {
|
|
3675
3955
|
await stopPolling();
|
|
3676
3956
|
}
|
|
3957
|
+
var shuttingDown = false;
|
|
3677
3958
|
for (const sig of ["SIGTERM", "SIGINT"]) {
|
|
3678
3959
|
process.on(sig, () => {
|
|
3960
|
+
if (shuttingDown) {
|
|
3961
|
+
log(`Received ${sig} again during shutdown \u2014 ignoring (cleanup in progress)`);
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
shuttingDown = true;
|
|
3679
3965
|
log(`Received ${sig}, shutting down`);
|
|
3680
3966
|
void stopPolling().then(() => {
|
|
3681
3967
|
process.exit(0);
|