@integrity-labs/agt-cli 0.14.12 → 0.14.14

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.
@@ -19,7 +19,7 @@ import {
19
19
  resolveChannels,
20
20
  resolveDmTarget,
21
21
  wrapScheduledTaskPrompt
22
- } from "../chunk-NSHSUWZQ.js";
22
+ } from "../chunk-VWCF6BOZ.js";
23
23
  import {
24
24
  findTaskByTemplate,
25
25
  getProjectDir,
@@ -45,6 +45,7 @@ import {
45
45
  import { createHash } from "crypto";
46
46
  import { readFileSync, writeFileSync, appendFileSync, mkdirSync, chmodSync, existsSync, rmSync, readdirSync, statSync, unlinkSync, copyFileSync } from "fs";
47
47
  import https from "https";
48
+ import { execFileSync as syncExecFile } from "child_process";
48
49
  import { join as join2, dirname } from "path";
49
50
  import { homedir as homedir2 } from "os";
50
51
  import { fileURLToPath } from "url";
@@ -81,6 +82,116 @@ ${trimmedOverrides}
81
82
  `;
82
83
  }
83
84
 
85
+ // src/lib/plugin-skill-layout.ts
86
+ function extractDescription(content) {
87
+ const match = content.match(/^---\s*([\s\S]*?)---/);
88
+ if (!match) return null;
89
+ const frontmatter = match[1] ?? "";
90
+ const descMatch = frontmatter.match(/^\s*description:\s*(?:"((?:\\.|[^"\\])*)"|([^\n]+))\s*$/m);
91
+ if (!descMatch) return null;
92
+ const quoted = descMatch[1];
93
+ if (quoted !== void 0) {
94
+ return quoted.replace(/\\(["\\])/g, "$1").trim();
95
+ }
96
+ return descMatch[2]?.trim() ?? null;
97
+ }
98
+ function sanitizeScopeSlug(skillId, pluginSlug) {
99
+ const normalized = skillId.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-");
100
+ const prefix = pluginSlug.replace(/-/g, "");
101
+ const stripped = normalized.startsWith(`${prefix}-`) ? normalized.slice(prefix.length + 1) : normalized;
102
+ return stripped.replace(/^-|-$/g, "") || normalized || "scope";
103
+ }
104
+ function buildPluginBundle(skills) {
105
+ if (skills.length === 0) {
106
+ throw new Error("buildPluginBundle: empty skills list");
107
+ }
108
+ const pluginSlug = skills[0].plugin_slug;
109
+ const ordered = [...skills].sort((a, b) => a.skill_id.localeCompare(b.skill_id));
110
+ const entries = ordered.map((s) => {
111
+ const scopeSlug = sanitizeScopeSlug(s.skill_id, pluginSlug);
112
+ const description = extractDescription(s.content);
113
+ return {
114
+ skillId: s.skill_id,
115
+ skillName: s.skill_name,
116
+ description,
117
+ scopeSlug,
118
+ scopePath: `scopes/${scopeSlug}.md`,
119
+ content: s.content
120
+ };
121
+ });
122
+ const scopePathGroups = /* @__PURE__ */ new Map();
123
+ for (const entry of entries) {
124
+ const bucket = scopePathGroups.get(entry.scopePath);
125
+ if (bucket) bucket.push(entry);
126
+ else scopePathGroups.set(entry.scopePath, [entry]);
127
+ }
128
+ for (const [scopePath, group] of scopePathGroups) {
129
+ if (group.length > 1) {
130
+ const conflicts = group.map((e) => `${e.skillId} (${e.skillName})`).join(", ");
131
+ throw new Error(
132
+ `buildPluginBundle: duplicate scope path '${scopePath}' for plugin '${pluginSlug}' \u2014 conflicting skills: ${conflicts}`
133
+ );
134
+ }
135
+ }
136
+ const pluginName = slugToTitle(pluginSlug);
137
+ const umbrellaDescription = entries.map((e) => e.description ?? `Use for ${e.skillName}.`).join(" ");
138
+ const scopeList = entries.map((e) => {
139
+ const label = e.skillName || slugToTitle(e.scopeSlug);
140
+ const desc = e.description ? ` \u2014 ${e.description}` : "";
141
+ return `- **${label}** ([${e.scopePath}](${e.scopePath}))${desc}`;
142
+ }).join("\n");
143
+ const umbrella = [
144
+ "---",
145
+ `name: "${pluginName}"`,
146
+ `description: "${escapeYamlDouble(umbrellaDescription)}"`,
147
+ "---",
148
+ "",
149
+ `# ${pluginName}`,
150
+ "",
151
+ `This skill bundles ${entries.length === 1 ? "one scope" : `${entries.length} scopes`} for the ${pluginSlug} plugin. Each scope has a dedicated reference under \`scopes/\` that you should load on demand when the user's intent maps to it.`,
152
+ "",
153
+ "## Scopes",
154
+ "",
155
+ scopeList,
156
+ "",
157
+ "## How to use",
158
+ "",
159
+ `Identify which scope matches the user's request, then read the matching file under \`scopes/\` for the exact tool names and usage details. Do not guess tool names from this umbrella file alone.`,
160
+ ""
161
+ ].join("\n");
162
+ const files = [
163
+ { relativePath: "SKILL.md", content: umbrella },
164
+ ...entries.map((e) => ({
165
+ relativePath: e.scopePath,
166
+ content: e.content
167
+ }))
168
+ ];
169
+ return {
170
+ pluginSlug,
171
+ files,
172
+ scopePaths: entries.map((e) => e.scopePath)
173
+ };
174
+ }
175
+ function slugToTitle(slug) {
176
+ return slug.split(/[-_]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
177
+ }
178
+ function escapeYamlDouble(value) {
179
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
180
+ }
181
+ function groupSkillsByPlugin(skills) {
182
+ const map = /* @__PURE__ */ new Map();
183
+ for (const s of skills) {
184
+ const bucket = map.get(s.plugin_slug);
185
+ if (bucket) bucket.push(s);
186
+ else map.set(s.plugin_slug, [s]);
187
+ }
188
+ return map;
189
+ }
190
+ function bundleFingerprint(files) {
191
+ const sorted = [...files].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
192
+ return sorted.map((f) => `${f.relativePath}\0${f.content}`).join("");
193
+ }
194
+
84
195
  // src/lib/gateway-client.ts
85
196
  import { EventEmitter } from "events";
86
197
  import WebSocket from "ws";
@@ -691,11 +802,23 @@ function appendScheduleLinkFooter(body, url, medium) {
691
802
 
692
803
  ${footer}`;
693
804
  }
805
+ var warnedNullConsoleUrl = false;
694
806
  function withScheduleLinkFooter(opts) {
695
807
  if (!opts.taskId) return opts.body;
696
808
  if (!scheduleLinkFooterEnabled(opts.codeName, opts.env)) return opts.body;
697
809
  const consoleUrl = getConsoleUrl(opts.env);
698
- if (!consoleUrl) return opts.body;
810
+ if (!consoleUrl) {
811
+ if (!warnedNullConsoleUrl && opts.log) {
812
+ warnedNullConsoleUrl = true;
813
+ try {
814
+ opts.log(
815
+ "[schedule-link] AGT_CONSOLE_URL unset and NEXT_PUBLIC_APP_URL unset \u2014 schedule-edit deep-link footer disabled. Run `agt setup` again or export AGT_CONSOLE_URL (e.g. https://app.augmented.team) to restore it."
816
+ );
817
+ } catch {
818
+ }
819
+ }
820
+ return opts.body;
821
+ }
699
822
  const url = buildScheduleEditLink(consoleUrl, opts.agentId, opts.taskId);
700
823
  return appendScheduleLinkFooter(opts.body, url, opts.medium);
701
824
  }
@@ -2608,44 +2731,74 @@ async function processAgent(agent, agentStates) {
2608
2731
  for (const ctx of refreshData.plugin_contexts ?? []) {
2609
2732
  contextBySlug.set(ctx.plugin_slug, { values: ctx.values ?? {}, overrides: (ctx.overrides ?? "").trim() });
2610
2733
  }
2611
- for (const ps of refreshData.plugin_skills ?? []) {
2734
+ const pluginGroups = groupSkillsByPlugin(
2735
+ refreshData.plugin_skills ?? []
2736
+ );
2737
+ for (const [pluginSlug, scopes] of pluginGroups) {
2612
2738
  try {
2613
- const skillId = `plugin-${ps.plugin_slug}-${ps.skill_id}`.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2614
- currentPluginSkillIds.add(skillId);
2615
- const ctx = contextBySlug.get(ps.plugin_slug);
2616
- const renderedContent = renderPluginSkillContent(
2617
- ps.content,
2618
- ctx?.values ?? {},
2619
- ctx?.overrides ?? "",
2620
- (warning) => log(`[plugin-context] ${ps.plugin_slug}/${ps.skill_id}: ${warning}`)
2621
- );
2622
- const contentHash = createHash2("sha256").update(renderedContent).digest("hex").slice(0, 12);
2623
- const hashKey = `plugin-skill:${agent.code_name}:${skillId}`;
2739
+ const pluginSkillId = `plugin-${pluginSlug}`.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2740
+ currentPluginSkillIds.add(pluginSkillId);
2741
+ const ctx = contextBySlug.get(pluginSlug);
2742
+ const renderedScopes = scopes.map((s) => ({
2743
+ plugin_slug: s.plugin_slug,
2744
+ skill_id: s.skill_id,
2745
+ skill_name: s.skill_name,
2746
+ content: renderPluginSkillContent(
2747
+ s.content,
2748
+ ctx?.values ?? {},
2749
+ ctx?.overrides ?? "",
2750
+ (warning) => log(`[plugin-context] ${s.plugin_slug}/${s.skill_id}: ${warning}`)
2751
+ )
2752
+ }));
2753
+ const bundle = buildPluginBundle(renderedScopes);
2754
+ const contentHash = createHash2("sha256").update(bundleFingerprint(bundle.files)).digest("hex").slice(0, 12);
2755
+ const hashKey = `plugin-skill:${agent.agent_id}:${pluginSkillId}`;
2624
2756
  if (knownSkillHashes.get(hashKey) === contentHash) continue;
2625
- frameworkAdapter.installSkillFiles(agent.code_name, skillId, [
2626
- { relativePath: "SKILL.md", content: renderedContent }
2627
- ]);
2757
+ frameworkAdapter.installSkillFiles(agent.code_name, pluginSkillId, bundle.files);
2628
2758
  knownSkillHashes.set(hashKey, contentHash);
2629
- installedPluginSkills.push(ps.skill_name);
2630
- log(`Installed plugin skill '${skillId}' for '${agent.code_name}'`);
2759
+ for (const s of scopes) installedPluginSkills.push(s.skill_name);
2760
+ log(`Installed plugin '${pluginSkillId}' for '${agent.code_name}' (${scopes.length} scope(s))`);
2631
2761
  } catch (err) {
2632
- log(`Plugin skill install failed for '${agent.code_name}' / '${ps.skill_id}': ${err.message}`);
2762
+ log(`Plugin install failed for '${agent.code_name}' / '${pluginSlug}': ${err.message}`);
2633
2763
  }
2634
2764
  }
2635
2765
  try {
2636
- const agentSkillsDir = join2(config.configDir, agent.code_name, "project", ".claude", "skills");
2637
- if (existsSync(agentSkillsDir)) {
2638
- const { readdirSync: readdirSync2, rmSync: rmSync2 } = await import("fs");
2639
- for (const entry of readdirSync2(agentSkillsDir)) {
2640
- if (entry.startsWith("plugin-") && !currentPluginSkillIds.has(entry)) {
2641
- const orphanPath = join2(agentSkillsDir, entry);
2642
- rmSync2(orphanPath, { recursive: true, force: true });
2643
- log(`Removed orphaned plugin skill '${entry}' for '${agent.code_name}'`);
2644
- const provisionSkillPath = join2(config.configDir, agent.code_name, "skills", entry);
2645
- if (existsSync(provisionSkillPath)) {
2646
- rmSync2(provisionSkillPath, { recursive: true, force: true });
2647
- }
2766
+ const { readdirSync: readdirSync2, rmSync: rmSync2 } = await import("fs");
2767
+ const { homedir: homedir3 } = await import("os");
2768
+ const frameworkId2 = frameworkAdapter.id;
2769
+ const candidateSkillDirs = [
2770
+ // Claude Code framework runtime tree
2771
+ join2(homedir3(), ".augmented", agent.code_name, "skills"),
2772
+ // Claude Code project tree
2773
+ join2(homedir3(), ".augmented", agent.code_name, "project", ".claude", "skills"),
2774
+ // OpenClaw framework runtime tree
2775
+ join2(homedir3(), `.openclaw-${agent.code_name}`, "skills"),
2776
+ // Defensive: legacy provision-side path, not currently an
2777
+ // install target but cheap to sweep.
2778
+ join2(agentDir, ".claude", "skills")
2779
+ ];
2780
+ const existingDirs = candidateSkillDirs.filter((d) => existsSync(d));
2781
+ const discoveredEntries = /* @__PURE__ */ new Set();
2782
+ for (const dir of existingDirs) {
2783
+ try {
2784
+ for (const entry of readdirSync2(dir)) {
2785
+ if (entry.startsWith("plugin-")) discoveredEntries.add(entry);
2648
2786
  }
2787
+ } catch {
2788
+ }
2789
+ }
2790
+ const removeSkillFolder = (entry, reason) => {
2791
+ for (const dir of existingDirs) {
2792
+ const p = join2(dir, entry);
2793
+ if (existsSync(p)) {
2794
+ rmSync2(p, { recursive: true, force: true });
2795
+ }
2796
+ }
2797
+ log(`Removed ${reason} '${entry}' for '${agent.code_name}' (framework=${frameworkId2})`);
2798
+ };
2799
+ for (const entry of discoveredEntries) {
2800
+ if (!currentPluginSkillIds.has(entry)) {
2801
+ removeSkillFolder(entry, "orphaned plugin skill");
2649
2802
  }
2650
2803
  }
2651
2804
  } catch (err) {
@@ -3739,7 +3892,7 @@ function ensureRealtimePluginContextStarted(agentStates) {
3739
3892
  const agent = agentStates.find((a) => a.agentId === payload.agent_id);
3740
3893
  if (agent) {
3741
3894
  for (const key of knownSkillHashes.keys()) {
3742
- if (key.startsWith(`plugin-skill:${agent.codeName}:`)) {
3895
+ if (key.startsWith(`plugin-skill:${agent.agentId}:`)) {
3743
3896
  knownSkillHashes.delete(key);
3744
3897
  }
3745
3898
  }
@@ -4487,7 +4640,7 @@ ${a.detail}`;
4487
4640
  ${blocks.join("\n\n")}`);
4488
4641
  }
4489
4642
  async function deliverScheduledTaskOutput(agentCodeName, agentId, rawTarget, body, taskId) {
4490
- const withLink = (b, medium) => withScheduleLinkFooter({ body: b, medium, codeName: agentCodeName, agentId, taskId });
4643
+ const withLink = (b, medium) => withScheduleLinkFooter({ body: b, medium, codeName: agentCodeName, agentId, taskId, log });
4491
4644
  if (typeof rawTarget === "string") {
4492
4645
  if (rawTarget.startsWith("channel:")) {
4493
4646
  const result = await sendTaskNotification(agentCodeName, "slack", rawTarget, withLink(body, "slack"));
@@ -5124,6 +5277,42 @@ async function ensureHostFrameworkBinaries() {
5124
5277
  log(`[framework-install] failed: ${err.message}`);
5125
5278
  }
5126
5279
  }
5280
+ function restartRunningChannelMcps(basenames) {
5281
+ try {
5282
+ const psOutput = syncExecFile(
5283
+ "ps",
5284
+ ["-eo", "pid=,command="],
5285
+ { encoding: "utf-8", timeout: 5e3 }
5286
+ );
5287
+ const lines = psOutput.split("\n").filter((l) => l.trim());
5288
+ const selfPid = process.pid;
5289
+ let signalled = 0;
5290
+ for (const line of lines) {
5291
+ const match = line.match(/^\s*(\d+)\s+(.*)$/);
5292
+ if (!match) continue;
5293
+ const pid = Number(match[1]);
5294
+ const command = match[2];
5295
+ if (pid === selfPid) continue;
5296
+ const hit = basenames.find((b) => new RegExp(`/${b}\\.js(\\s|$)`).test(command));
5297
+ if (!hit) continue;
5298
+ try {
5299
+ process.kill(pid, "SIGTERM");
5300
+ signalled++;
5301
+ log(`[manager] sent SIGTERM to pid=${pid} (${hit}.js) \u2014 will respawn on next message`);
5302
+ } catch (err) {
5303
+ const code = err.code;
5304
+ if (code && code !== "ESRCH") {
5305
+ log(`[manager] failed to signal pid=${pid}: ${code}`);
5306
+ }
5307
+ }
5308
+ }
5309
+ if (signalled === 0) {
5310
+ log(`[manager] no running instances to restart (deploy still applies to future spawns)`);
5311
+ }
5312
+ } catch (err) {
5313
+ log(`[manager] restartRunningChannelMcps failed: ${err.message}`);
5314
+ }
5315
+ }
5127
5316
  function deployMcpAssets() {
5128
5317
  const targetDir = join2(homedir2(), ".augmented", "_mcp");
5129
5318
  mkdirSync(targetDir, { recursive: true });
@@ -5144,17 +5333,40 @@ function deployMcpAssets() {
5144
5333
  log("[manager] MCP assets not found in CLI package \u2014 skipping deployment");
5145
5334
  return;
5146
5335
  }
5336
+ const changedBasenames = [];
5337
+ const fileHash = (p) => {
5338
+ try {
5339
+ if (!existsSync(p)) return null;
5340
+ return createHash("sha256").update(readFileSync(p)).digest("hex");
5341
+ } catch {
5342
+ return null;
5343
+ }
5344
+ };
5345
+ const RESTARTABLE_CHANNEL_FILES = /* @__PURE__ */ new Set([
5346
+ "slack-channel.js",
5347
+ "direct-chat-channel.js",
5348
+ "telegram-channel.js"
5349
+ ]);
5147
5350
  for (const file of ["index.js", "slack-channel.js", "direct-chat-channel.js", "telegram-channel.js"]) {
5148
5351
  const src = join2(mcpSourceDir, file);
5149
5352
  const dst = join2(targetDir, file);
5150
5353
  if (!existsSync(src)) continue;
5354
+ const before = fileHash(dst);
5151
5355
  try {
5152
5356
  copyFileSync(src, dst);
5357
+ const after = fileHash(dst);
5358
+ if (before !== after && RESTARTABLE_CHANNEL_FILES.has(file)) {
5359
+ changedBasenames.push(file.replace(/\.js$/, ""));
5360
+ }
5153
5361
  } catch (err) {
5154
5362
  log(`[manager] Failed to deploy ${file}: ${err.message}`);
5155
5363
  }
5156
5364
  }
5157
5365
  log(`[manager] MCP assets deployed to ${targetDir}`);
5366
+ if (changedBasenames.length > 0) {
5367
+ log(`[manager] Bundle(s) updated: ${changedBasenames.join(", ")} \u2014 signalling running instances to restart`);
5368
+ restartRunningChannelMcps(changedBasenames);
5369
+ }
5158
5370
  const localMcpPath = join2(targetDir, "index.js");
5159
5371
  try {
5160
5372
  const agentsDir = join2(homedir2(), ".augmented", "agents");