@integrity-labs/agt-cli 0.27.8 → 0.27.10

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.
@@ -14368,6 +14368,50 @@ function isMode(value) {
14368
14368
  return value === "thinking" || value === "working" || value === "waiting";
14369
14369
  }
14370
14370
 
14371
+ // src/impersonation.ts
14372
+ var ENV_VAR = "AGT_ACT_AS_AGENT_ID";
14373
+ var OVERRIDE_ENV_VAR = "ENABLE_IMPERSONATION_CHANNELS";
14374
+ function isImpersonating() {
14375
+ return impersonatedAgentId() !== null;
14376
+ }
14377
+ function impersonatedAgentId() {
14378
+ const raw = process.env[ENV_VAR];
14379
+ if (typeof raw !== "string") return null;
14380
+ const trimmed = raw.trim();
14381
+ return trimmed.length > 0 ? trimmed : null;
14382
+ }
14383
+ function channelsEnabledOverride() {
14384
+ const raw = process.env[OVERRIDE_ENV_VAR];
14385
+ if (typeof raw !== "string") return false;
14386
+ const trimmed = raw.trim().toLowerCase();
14387
+ return trimmed === "1" || trimmed === "true" || trimmed === "yes" || trimmed === "on";
14388
+ }
14389
+ var IMPERSONATION_REFUSAL_CODE = "CHANNEL.IMPERSONATION_DISABLED";
14390
+ var IMPERSONATION_REFUSAL_MESSAGE = "Posting as an agent under impersonation is disabled in v0 \u2014 quit impersonation with `agt impersonate exit` to send as yourself.";
14391
+ function buildImpersonationRefusal(toolName) {
14392
+ return {
14393
+ content: [
14394
+ {
14395
+ type: "text",
14396
+ text: JSON.stringify({
14397
+ error: {
14398
+ code: IMPERSONATION_REFUSAL_CODE,
14399
+ message: IMPERSONATION_REFUSAL_MESSAGE,
14400
+ tool: toolName
14401
+ }
14402
+ })
14403
+ }
14404
+ ],
14405
+ isError: true
14406
+ };
14407
+ }
14408
+
14409
+ // src/channel-egress-tools.ts
14410
+ var DIRECT_CHAT_EGRESS_TOOLS = /* @__PURE__ */ new Set([
14411
+ "direct_chat.reply",
14412
+ "channel_request_input"
14413
+ ]);
14414
+
14371
14415
  // src/mcp-spawn-lock.ts
14372
14416
  import {
14373
14417
  existsSync,
@@ -14597,6 +14641,9 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
14597
14641
  }));
14598
14642
  mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
14599
14643
  const { name, arguments: args } = req.params;
14644
+ if (isImpersonating() && !channelsEnabledOverride() && DIRECT_CHAT_EGRESS_TOOLS.has(name)) {
14645
+ return buildImpersonationRefusal(name);
14646
+ }
14600
14647
  const progressResult = await progressRegistry.handle(name, args ?? {});
14601
14648
  if (progressResult !== void 0) return progressResult;
14602
14649
  if (name === "channel_request_input") {
package/dist/mcp/index.js CHANGED
@@ -21165,7 +21165,7 @@ var server = new McpServer({
21165
21165
  version: "0.1.0"
21166
21166
  });
21167
21167
  server.tool(
21168
- "kanban.list",
21168
+ "kanban_list",
21169
21169
  "List kanban board items for this agent. Returns active items and recently completed items (last 7 days).",
21170
21170
  {},
21171
21171
  async () => {
@@ -21194,7 +21194,7 @@ server.tool(
21194
21194
  }
21195
21195
  );
21196
21196
  server.tool(
21197
- "kanban.add",
21197
+ "kanban_add",
21198
21198
  "Add a new item to the kanban board.",
21199
21199
  {
21200
21200
  title: external_exports.string().describe("Item title (max 200 chars)"),
@@ -21235,7 +21235,7 @@ server.tool(
21235
21235
  }
21236
21236
  );
21237
21237
  server.tool(
21238
- "kanban.move",
21238
+ "kanban_move",
21239
21239
  "Move a kanban item to a different status column.",
21240
21240
  {
21241
21241
  id: external_exports.string().optional().describe("Item UUID (preferred)"),
@@ -21273,7 +21273,7 @@ server.tool(
21273
21273
  }
21274
21274
  );
21275
21275
  server.tool(
21276
- "kanban.update",
21276
+ "kanban_update",
21277
21277
  "Update notes or result on a kanban item without changing its status.",
21278
21278
  {
21279
21279
  id: external_exports.string().optional().describe("Item UUID (preferred)"),
@@ -21324,7 +21324,7 @@ server.tool(
21324
21324
  }
21325
21325
  );
21326
21326
  server.tool(
21327
- "kanban.progress",
21327
+ "kanban_progress",
21328
21328
  "Report what you're doing RIGHT NOW on a kanban item. Overwrites a single live status line (the Step row on the Slack progress card the requester is watching) \u2014 not a log. Call it at the start of each phase of a long task (ideally ~every minute) so the requester can watch the work move. Each call also renews the task heartbeat. Pass an empty step to clear it.",
21329
21329
  {
21330
21330
  id: external_exports.string().optional().describe("Item UUID (preferred)"),
@@ -21362,7 +21362,7 @@ server.tool(
21362
21362
  }
21363
21363
  );
21364
21364
  server.tool(
21365
- "kanban.done",
21365
+ "kanban_done",
21366
21366
  "Mark a kanban item as done with an optional result.",
21367
21367
  {
21368
21368
  id: external_exports.string().optional().describe("Item UUID (preferred)"),
@@ -21401,7 +21401,7 @@ server.tool(
21401
21401
  }
21402
21402
  );
21403
21403
  server.tool(
21404
- "status.standup",
21404
+ "status_standup",
21405
21405
  "Submit a daily standup update with yesterday, today, and blockers.",
21406
21406
  {
21407
21407
  yesterday: external_exports.string().describe("What was accomplished yesterday/last session"),
@@ -21434,7 +21434,7 @@ server.tool(
21434
21434
  }
21435
21435
  );
21436
21436
  server.tool(
21437
- "status.update",
21437
+ "status_update",
21438
21438
  "Report current task progress or status update.",
21439
21439
  {
21440
21440
  current_tasks: external_exports.string().describe("Summary of what you are currently working on")
@@ -21461,7 +21461,7 @@ server.tool(
21461
21461
  }
21462
21462
  );
21463
21463
  server.tool(
21464
- "drift.report",
21464
+ "drift_report",
21465
21465
  "Report configuration drift detected in local agent files (CHARTER.md, TOOLS.md, etc.). Compares local file hashes against API-side versions and reports any differences.",
21466
21466
  {
21467
21467
  drifted_files: external_exports.array(external_exports.string()).describe('List of filenames that have drifted from provisioned state (e.g. ["CHARTER.md", "TOOLS.md"])'),
@@ -21484,7 +21484,7 @@ server.tool(
21484
21484
  }
21485
21485
  );
21486
21486
  server.tool(
21487
- "token.refresh",
21487
+ "token_refresh",
21488
21488
  "Get a working OAuth access token for an integration. Returns the current cached token when it is still valid (the server-side cron rotates tokens every ~15 minutes) and forces a refresh only when the cached token is expiring or missing. Pass `integration_id` to force-refresh a specific integration after a 401/TokenExpired from the provider.",
21489
21489
  {
21490
21490
  integration_id: external_exports.string().optional().describe("Specific integration UUID to force-refresh (use after a 401 from the provider). If omitted, returns current tokens for all OAuth integrations, refreshing any that are within ~10 minutes of expiry.")
@@ -21578,7 +21578,7 @@ async function runAcpxCommand(args, cwd) {
21578
21578
  });
21579
21579
  }
21580
21580
  server.tool(
21581
- "acpx.prompt",
21581
+ "acpx_prompt",
21582
21582
  "Send a prompt to a coding agent (claude, codex, openclaw) via ACP. Uses persistent sessions scoped to the working directory.",
21583
21583
  {
21584
21584
  agent: external_exports.enum(["claude", "codex", "openclaw"]).describe("Coding agent to use"),
@@ -21603,7 +21603,7 @@ server.tool(
21603
21603
  }
21604
21604
  );
21605
21605
  server.tool(
21606
- "acpx.exec",
21606
+ "acpx_exec",
21607
21607
  "Run a one-shot task on a coding agent. Does not reuse sessions \u2014 fire and forget.",
21608
21608
  {
21609
21609
  agent: external_exports.enum(["claude", "codex", "openclaw"]).describe("Coding agent to use"),
@@ -21622,7 +21622,7 @@ server.tool(
21622
21622
  }
21623
21623
  );
21624
21624
  server.tool(
21625
- "acpx.spawn",
21625
+ "acpx_spawn",
21626
21626
  "Ensure an ACP session exists for a coding agent. Creates one if needed, reuses existing.",
21627
21627
  {
21628
21628
  agent: external_exports.enum(["claude", "codex", "openclaw"]).describe("Coding agent to use"),
@@ -21642,7 +21642,7 @@ server.tool(
21642
21642
  }
21643
21643
  );
21644
21644
  server.tool(
21645
- "acpx.cancel",
21645
+ "acpx_cancel",
21646
21646
  "Send a cooperative cancel to the running ACP session. Does not destroy state.",
21647
21647
  {
21648
21648
  agent: external_exports.enum(["claude", "codex", "openclaw"]).describe("Coding agent to cancel"),
@@ -21662,7 +21662,7 @@ server.tool(
21662
21662
  }
21663
21663
  );
21664
21664
  server.tool(
21665
- "acpx.status",
21665
+ "acpx_status",
21666
21666
  "Check the status of the current ACP session (running, idle, dead).",
21667
21667
  {
21668
21668
  agent: external_exports.enum(["claude", "codex", "openclaw"]).describe("Coding agent to check"),
@@ -21676,7 +21676,7 @@ server.tool(
21676
21676
  }
21677
21677
  );
21678
21678
  server.tool(
21679
- "acpx.close",
21679
+ "acpx_close",
21680
21680
  "Soft-close an ACP session. Terminates the process but preserves history.",
21681
21681
  {
21682
21682
  agent: external_exports.enum(["claude", "codex", "openclaw"]).describe("Coding agent session to close"),
@@ -21696,7 +21696,7 @@ server.tool(
21696
21696
  }
21697
21697
  );
21698
21698
  server.tool(
21699
- "schedule.list",
21699
+ "schedule_list",
21700
21700
  "List your scheduled tasks (cron jobs, reminders, recurring work).",
21701
21701
  {},
21702
21702
  async () => {
@@ -21716,7 +21716,7 @@ server.tool(
21716
21716
  }
21717
21717
  );
21718
21718
  server.tool(
21719
- "schedule.create",
21719
+ "schedule_create",
21720
21720
  "Create a new scheduled task. Supports cron expressions, intervals (every), and one-time reminders (at).",
21721
21721
  {
21722
21722
  name: external_exports.string().describe('Display name for the task (e.g. "Daily standup", "Check emails")'),
@@ -21752,10 +21752,10 @@ server.tool(
21752
21752
  }
21753
21753
  );
21754
21754
  server.tool(
21755
- "schedule.delete",
21755
+ "schedule_delete",
21756
21756
  "Delete a scheduled task by ID.",
21757
21757
  {
21758
- task_id: external_exports.string().describe("The task ID to delete (from schedule.list)")
21758
+ task_id: external_exports.string().describe("The task ID to delete (from schedule_list)")
21759
21759
  },
21760
21760
  async (params) => {
21761
21761
  await apiPost("/host/schedules/delete", {
@@ -21768,12 +21768,12 @@ server.tool(
21768
21768
  }
21769
21769
  );
21770
21770
  server.tool(
21771
- "schedule.update",
21771
+ "schedule_update",
21772
21772
  // Lead with user phrasings the agent should match against, then explain
21773
21773
  // the two modes. Order matters for tool selection.
21774
21774
  'Update an existing scheduled task. USE THIS TOOL when the user says: "change the schedule for X", "run the daily report at 10am instead of 9am", "pause the standup schedule", "move the weekly digest to Mondays", "make this report more concise", "add a section about X to the daily summary", "tell the Y schedule to also cover Z", "post these to the Customer Success channel from now on". Two modes: (1) DIRECT FIELD UPDATES \u2014 set cron_expression/interval/timezone/delivery_channel/delivery_to/enabled/name/prompt to change those values directly. (2) LLM DESCRIPTION REWRITE \u2014 pass description_feedback to have the platform rewrite the task prompt based on natural-language feedback (e.g. "make this more concise" or "add a section about the team backlog"). Default for LLM rewrites is preview-then-apply: call once to get the proposed rewrite, show the diff to the user, call again with auto_apply=true on confirmation. Direct field updates apply immediately. The two modes are mutually exclusive \u2014 one request is EITHER a field update OR a description rewrite.\n\nIMPORTANT \u2014 `delivery_to` requires a Slack channel ID (e.g. `C0123ABC456`), NOT a human channel name. If the user references a channel by its name ("the Customer Success channel"), call `mcp__slack__slack.list_channels` first with `query: "customer success"` to get the matching `id`, then pass that id as `delivery_to` here. Do not guess channel ids.',
21775
21775
  {
21776
- task_id: external_exports.string().describe("The task ID to update (from schedule.list)"),
21776
+ task_id: external_exports.string().describe("The task ID to update (from schedule_list)"),
21777
21777
  // Direct-field mode
21778
21778
  name: external_exports.string().optional().describe("New task display name"),
21779
21779
  schedule_kind: external_exports.enum(["cron", "every", "at"]).optional().describe(
@@ -21796,7 +21796,7 @@ server.tool(
21796
21796
  'Where results are delivered (e.g. "slack", "telegram", "email")'
21797
21797
  ),
21798
21798
  delivery_to: DeliveryTargetSchema.optional().describe(
21799
- "Structured delivery target (see schedule.create description for shape). Pass null-equivalent by omitting the field; pass a new structured target to change routing."
21799
+ "Structured delivery target (see schedule_create description for shape). Pass null-equivalent by omitting the field; pass a new structured target to change routing."
21800
21800
  ),
21801
21801
  enabled: external_exports.boolean().optional().describe(
21802
21802
  "Set to false to pause the schedule without deleting it"
@@ -21833,7 +21833,7 @@ server.tool(
21833
21833
  if (!data.applied) {
21834
21834
  lines.push("");
21835
21835
  lines.push("---");
21836
- lines.push("Show this diff to the user. If they confirm, call `schedule.update` again with the same `task_id` and `description_feedback` plus `auto_apply: true` to commit the change.");
21836
+ lines.push("Show this diff to the user. If they confirm, call `schedule_update` again with the same `task_id` and `description_feedback` plus `auto_apply: true` to commit the change.");
21837
21837
  }
21838
21838
  return { content: [{ type: "text", text: lines.join("\n") }] };
21839
21839
  }
@@ -21846,8 +21846,8 @@ server.tool(
21846
21846
  }
21847
21847
  );
21848
21848
  server.tool(
21849
- "knowledge.list",
21850
- "List team knowledge entries (ids, titles, source types). Use before knowledge.add to check if a fact is already saved, or before knowledge.update/delete to find the target id. Returns metadata only \u2014 fetch the full content via the webapp if you need to read an entry back.",
21849
+ "knowledge_list",
21850
+ "List team knowledge entries (ids, titles, source types). Use before knowledge_add to check if a fact is already saved, or before knowledge_update/delete to find the target id. Returns metadata only \u2014 fetch the full content via the webapp if you need to read an entry back.",
21851
21851
  {},
21852
21852
  async () => {
21853
21853
  const data = await apiPost("/host/my-knowledge", {
@@ -21864,8 +21864,8 @@ server.tool(
21864
21864
  }
21865
21865
  );
21866
21866
  server.tool(
21867
- "knowledge.add",
21868
- "Save a new durable fact or learning to the team knowledge base. Use when the user explicitly asks you to save/remember something, or when you discover information that will be useful on future runs (e.g. a tenant id, an API key name, a company policy). Do NOT use for tasks (use kanban.add) or trivial one-off answers. Returns the new entry id so you can target it with knowledge.update / knowledge.delete.",
21867
+ "knowledge_add",
21868
+ "Save a new durable fact or learning to the team knowledge base. Use when the user explicitly asks you to save/remember something, or when you discover information that will be useful on future runs (e.g. a tenant id, an API key name, a company policy). Do NOT use for tasks (use kanban_add) or trivial one-off answers. Returns the new entry id so you can target it with knowledge_update / knowledge_delete.",
21869
21869
  {
21870
21870
  title: external_exports.string().describe("Short title summarising the entry (max 200 chars)"),
21871
21871
  content: external_exports.string().describe("The knowledge content \u2014 the actual fact, learning, or reference material"),
@@ -21903,10 +21903,10 @@ server.tool(
21903
21903
  }
21904
21904
  );
21905
21905
  server.tool(
21906
- "knowledge.update",
21907
- "Edit an existing knowledge entry by id. Use to correct a saved fact or refine its wording. Only the fields you pass are changed; others are left alone. Call knowledge.list first if you need to find the target id.",
21906
+ "knowledge_update",
21907
+ "Edit an existing knowledge entry by id. Use to correct a saved fact or refine its wording. Only the fields you pass are changed; others are left alone. Call knowledge_list first if you need to find the target id.",
21908
21908
  {
21909
- id: external_exports.string().describe("Knowledge entry id (uuid) \u2014 from knowledge.list or knowledge.add"),
21909
+ id: external_exports.string().describe("Knowledge entry id (uuid) \u2014 from knowledge_list or knowledge_add"),
21910
21910
  title: external_exports.string().optional().describe("New title (max 200 chars). Regenerates the slug."),
21911
21911
  content: external_exports.string().optional().describe("Replacement content for the entry"),
21912
21912
  is_active: external_exports.boolean().optional().describe("Set false to soft-disable without deleting")
@@ -21914,7 +21914,7 @@ server.tool(
21914
21914
  async (params) => {
21915
21915
  if (params.title === void 0 && params.content === void 0 && params.is_active === void 0) {
21916
21916
  return {
21917
- content: [{ type: "text", text: "Error: knowledge.update needs at least one of title, content, or is_active." }],
21917
+ content: [{ type: "text", text: "Error: knowledge_update needs at least one of title, content, or is_active." }],
21918
21918
  isError: true
21919
21919
  };
21920
21920
  }
@@ -21940,8 +21940,8 @@ server.tool(
21940
21940
  }
21941
21941
  );
21942
21942
  server.tool(
21943
- "knowledge.delete",
21944
- "Remove a knowledge entry permanently. Prefer knowledge.update with is_active=false for temporary hides; use delete only when the fact was wrong or no longer applies.",
21943
+ "knowledge_delete",
21944
+ "Remove a knowledge entry permanently. Prefer knowledge_update with is_active=false for temporary hides; use delete only when the fact was wrong or no longer applies.",
21945
21945
  {
21946
21946
  id: external_exports.string().describe("Knowledge entry id (uuid) to delete")
21947
21947
  },
@@ -21961,8 +21961,8 @@ server.tool(
21961
21961
  }
21962
21962
  );
21963
21963
  server.tool(
21964
- "plugin.list",
21965
- `List the plugins currently installed on this agent, with their current typed context values and a preview of any freeform overrides. Use this when the user asks "what plugins do I have?", "show me the plugins", "what is the Coding plugin configured with?", or before calling plugin.improve to see what fields exist and what they're set to.`,
21964
+ "plugin_list",
21965
+ `List the plugins currently installed on this agent, with their current typed context values and a preview of any freeform overrides. Use this when the user asks "what plugins do I have?", "show me the plugins", "what is the Coding plugin configured with?", or before calling plugin_improve to see what fields exist and what they're set to.`,
21966
21966
  {},
21967
21967
  async () => {
21968
21968
  const data = await apiPost("/host/list-plugins", {
@@ -21994,12 +21994,12 @@ server.tool(
21994
21994
  }
21995
21995
  lines.push("");
21996
21996
  }
21997
- lines.push("To update any plugin's behavior, use the `plugin.improve` tool with the user's feedback.");
21997
+ lines.push("To update any plugin's behavior, use the `plugin_improve` tool with the user's feedback.");
21998
21998
  return { content: [{ type: "text", text: lines.join("\n") }] };
21999
21999
  }
22000
22000
  );
22001
22001
  server.tool(
22002
- "plugin.improve",
22002
+ "plugin_improve",
22003
22003
  // Description leads with the user phrasings the agent should match against,
22004
22004
  // then explains the safety rule. Order matters — Claude\'s tool selection
22005
22005
  // weights the first line of a description heavily.
@@ -22055,7 +22055,7 @@ server.tool(
22055
22055
  if (!data.applied) {
22056
22056
  lines.push("");
22057
22057
  lines.push("---");
22058
- lines.push("Show this diff to the user. If they confirm, call `plugin.improve` again with `auto_apply: true` and the same arguments to apply.");
22058
+ lines.push("Show this diff to the user. If they confirm, call `plugin_improve` again with `auto_apply: true` and the same arguments to apply.");
22059
22059
  } else {
22060
22060
  lines.push("");
22061
22061
  lines.push("Update applied. The manager will re-render the plugin's SKILL.md files on the next refresh cycle.");
@@ -22073,7 +22073,7 @@ var WidgetDefSchema = external_exports.object({
22073
22073
  refresh_cron: external_exports.string().optional().describe('Optional per-widget cron override (e.g. "0 6 * * *" for 6am daily).')
22074
22074
  });
22075
22075
  server.tool(
22076
- "dashboards.list",
22076
+ "dashboards_list",
22077
22077
  'List dashboards visible to your team. Use to answer "what dashboards exist", "show me the dashboards I built", or before authoring a new one to avoid duplicating an existing slug.',
22078
22078
  {
22079
22079
  status: external_exports.enum(["draft", "active", "archived"]).optional().describe("Filter by status. Defaults to 'active'.")
@@ -22095,8 +22095,8 @@ ${lines.join("\n")}` }] };
22095
22095
  }
22096
22096
  );
22097
22097
  server.tool(
22098
- "dashboards.show",
22099
- "Get full metadata for one dashboard by slug \u2014 title, description, scope, status, widget list, last refresh per widget. Use when investigating a specific dashboard or before editing it via dashboards.upsert.",
22098
+ "dashboards_show",
22099
+ "Get full metadata for one dashboard by slug \u2014 title, description, scope, status, widget list, last refresh per widget. Use when investigating a specific dashboard or before editing it via dashboards_upsert.",
22100
22100
  {
22101
22101
  slug: external_exports.string().describe("Dashboard slug, e.g. 'finance-overview'.")
22102
22102
  },
@@ -22128,7 +22128,7 @@ URL: ${url}` : "",
22128
22128
  }
22129
22129
  );
22130
22130
  server.tool(
22131
- "dashboards.upsert",
22131
+ "dashboards_upsert",
22132
22132
  // The description leads with the user phrasings the agent should match
22133
22133
  // against AND the explicit forbidden alternatives. Tool selection weights
22134
22134
  // the first sentence heavily, so we burn it on the right behavior.
@@ -22168,8 +22168,8 @@ URL: ${url}
22168
22168
  }
22169
22169
  );
22170
22170
  server.tool(
22171
- "dashboards.request_refresh",
22172
- "Mark widgets on a dashboard as needing refresh. Use this when authoring a new dashboard (to populate the first snapshot) or when you want widgets to update on the next dashboards.pending_refreshes call. Pass widget_id to flag just one widget; omit to flag every widget.",
22171
+ "dashboards_request_refresh",
22172
+ "Mark widgets on a dashboard as needing refresh. Use this when authoring a new dashboard (to populate the first snapshot) or when you want widgets to update on the next dashboards_pending_refreshes call. Pass widget_id to flag just one widget; omit to flag every widget.",
22173
22173
  {
22174
22174
  slug: external_exports.string(),
22175
22175
  widget_id: external_exports.string().optional().describe("Optional. Omit to flag all widgets on the dashboard."),
@@ -22189,8 +22189,8 @@ server.tool(
22189
22189
  }
22190
22190
  );
22191
22191
  server.tool(
22192
- "dashboards.pending_refreshes",
22193
- "Return widgets that need refresh \u2014 by default ONLY those flagged pending (user clicked \u21BB in the iframe, or someone called request_refresh). Pass slug=<dashboard slug> to instead get every widget on that dashboard so you can proactively refresh after authoring or when the user asks 'update the X dashboard'. Pass include_all=true with no slug to get every flagged-or-not widget across all your dashboards. Each entry: {slug, widget_id, kind, prompt, schema, requested_at}. Loop over them, read the prompt, gather data with your other tools, match the schema, and call dashboards.persist_widget for each.",
22192
+ "dashboards_pending_refreshes",
22193
+ "Return widgets that need refresh \u2014 by default ONLY those flagged pending (user clicked \u21BB in the iframe, or someone called request_refresh). Pass slug=<dashboard slug> to instead get every widget on that dashboard so you can proactively refresh after authoring or when the user asks 'update the X dashboard'. Pass include_all=true with no slug to get every flagged-or-not widget across all your dashboards. Each entry: {slug, widget_id, kind, prompt, schema, requested_at}. Loop over them, read the prompt, gather data with your other tools, match the schema, and call dashboards_persist_widget for each.",
22194
22194
  {
22195
22195
  slug: external_exports.string().optional().describe("Restrict to one dashboard by slug. When set, returns every widget on that dashboard \u2014 flagged or not \u2014 so you can refresh the whole thing without calling request_refresh first."),
22196
22196
  include_all: external_exports.boolean().optional().describe("When true (and no slug), returns every widget on every dashboard you own \u2014 flagged or not. Use sparingly; one refresh = one LLM call.")
@@ -22209,7 +22209,7 @@ server.tool(
22209
22209
  }
22210
22210
  );
22211
22211
  server.tool(
22212
- "dashboards.persist_widget",
22212
+ "dashboards_persist_widget",
22213
22213
  "Persist refreshed data for one widget. The platform validates `data` against the widget's stored JSON Schema server-side \u2014 invalid output is rejected. Call this once per widget after gathering data with your tools.",
22214
22214
  {
22215
22215
  slug: external_exports.string(),
@@ -22282,28 +22282,28 @@ async function forwardToolCall(toolName, args) {
22282
22282
  async function registerForwardedApiTools() {
22283
22283
  const apiTools = await discoverApiTools();
22284
22284
  const localToolNames = /* @__PURE__ */ new Set([
22285
- "kanban.list",
22286
- "kanban.add",
22287
- "kanban.move",
22288
- "kanban.update",
22289
- "kanban.progress",
22290
- "kanban.done",
22291
- "status.standup",
22292
- "status.update",
22293
- "drift.report",
22294
- "token.refresh",
22295
- "acpx.prompt",
22296
- "acpx.exec",
22297
- "acpx.spawn",
22298
- "acpx.cancel",
22299
- "knowledge.list",
22300
- "knowledge.search",
22301
- "knowledge.read",
22302
- "knowledge.add",
22303
- "knowledge.update",
22304
- "knowledge.delete",
22305
- "plugin.list",
22306
- "plugin.improve"
22285
+ "kanban_list",
22286
+ "kanban_add",
22287
+ "kanban_move",
22288
+ "kanban_update",
22289
+ "kanban_progress",
22290
+ "kanban_done",
22291
+ "status_standup",
22292
+ "status_update",
22293
+ "drift_report",
22294
+ "token_refresh",
22295
+ "acpx_prompt",
22296
+ "acpx_exec",
22297
+ "acpx_spawn",
22298
+ "acpx_cancel",
22299
+ "knowledge_list",
22300
+ "knowledge_search",
22301
+ "knowledge_read",
22302
+ "knowledge_add",
22303
+ "knowledge_update",
22304
+ "knowledge_delete",
22305
+ "plugin_list",
22306
+ "plugin_improve"
22307
22307
  ]);
22308
22308
  let registered = 0;
22309
22309
  let skipped = 0;
@@ -22335,7 +22335,7 @@ async function registerForwardedApiTools() {
22335
22335
  return { registered, skipped };
22336
22336
  }
22337
22337
  server.tool(
22338
- "projects.list",
22338
+ "projects_list",
22339
22339
  "List artefacts (websites, slide decks, mockups) this agent has previously published. Call this BEFORE re-publishing to find and modify existing work instead of creating duplicates.",
22340
22340
  {
22341
22341
  publisher: external_exports.string().optional().describe('Filter by publisher (e.g. "here-now")'),
@@ -22361,10 +22361,10 @@ server.tool(
22361
22361
  }
22362
22362
  );
22363
22363
  server.tool(
22364
- "projects.get",
22365
- "Fetch metadata for a single published project. Returns the project record without the source HTML \u2014 call projects.get_source_chunk to retrieve the HTML in chunks when you need to modify it.",
22364
+ "projects_get",
22365
+ "Fetch metadata for a single published project. Returns the project record without the source HTML \u2014 call projects_get_source_chunk to retrieve the HTML in chunks when you need to modify it.",
22366
22366
  {
22367
- project_id: external_exports.string().uuid().describe("The project id returned by projects.list")
22367
+ project_id: external_exports.string().uuid().describe("The project id returned by projects_list")
22368
22368
  },
22369
22369
  async (params) => {
22370
22370
  const data = await apiPost(`/host/published-project/${encodeURIComponent(params.project_id)}`, {
@@ -22379,16 +22379,16 @@ server.tool(
22379
22379
  `Published: ${p.published_at}`,
22380
22380
  p.expires_at ? `Expires: ${p.expires_at}` : null,
22381
22381
  `Last modified: ${p.last_modified_at}`,
22382
- p.has_source_html ? `Source HTML: ${p.source_html_length} bytes (call projects.get_source_chunk to fetch)` : "(source HTML not preserved for this project)"
22382
+ p.has_source_html ? `Source HTML: ${p.source_html_length} bytes (call projects_get_source_chunk to fetch)` : "(source HTML not preserved for this project)"
22383
22383
  ].filter((s) => s !== null).join("\n");
22384
22384
  return { content: [{ type: "text", text: header }] };
22385
22385
  }
22386
22386
  );
22387
22387
  server.tool(
22388
- "projects.get_source_chunk",
22389
- "Fetch a chunk of source HTML for a published project. Use this to assemble the HTML for modification when projects.get reports a non-zero source_html_length.",
22388
+ "projects_get_source_chunk",
22389
+ "Fetch a chunk of source HTML for a published project. Use this to assemble the HTML for modification when projects_get reports a non-zero source_html_length.",
22390
22390
  {
22391
- project_id: external_exports.string().uuid().describe("The project id returned by projects.list"),
22391
+ project_id: external_exports.string().uuid().describe("The project id returned by projects_list"),
22392
22392
  offset: external_exports.number().int().min(0).describe("Byte offset into the source HTML to start from"),
22393
22393
  limit: external_exports.number().int().min(1).max(65536).optional().describe("Max bytes to return (default 32KB, max 64KB)")
22394
22394
  },
@@ -14813,6 +14813,53 @@ function isMode(value) {
14813
14813
  return value === "thinking" || value === "working" || value === "waiting";
14814
14814
  }
14815
14815
 
14816
+ // src/impersonation.ts
14817
+ var ENV_VAR = "AGT_ACT_AS_AGENT_ID";
14818
+ var OVERRIDE_ENV_VAR = "ENABLE_IMPERSONATION_CHANNELS";
14819
+ function isImpersonating() {
14820
+ return impersonatedAgentId() !== null;
14821
+ }
14822
+ function impersonatedAgentId() {
14823
+ const raw = process.env[ENV_VAR];
14824
+ if (typeof raw !== "string") return null;
14825
+ const trimmed = raw.trim();
14826
+ return trimmed.length > 0 ? trimmed : null;
14827
+ }
14828
+ function channelsEnabledOverride() {
14829
+ const raw = process.env[OVERRIDE_ENV_VAR];
14830
+ if (typeof raw !== "string") return false;
14831
+ const trimmed = raw.trim().toLowerCase();
14832
+ return trimmed === "1" || trimmed === "true" || trimmed === "yes" || trimmed === "on";
14833
+ }
14834
+ var IMPERSONATION_REFUSAL_CODE = "CHANNEL.IMPERSONATION_DISABLED";
14835
+ var IMPERSONATION_REFUSAL_MESSAGE = "Posting as an agent under impersonation is disabled in v0 \u2014 quit impersonation with `agt impersonate exit` to send as yourself.";
14836
+ function buildImpersonationRefusal(toolName) {
14837
+ return {
14838
+ content: [
14839
+ {
14840
+ type: "text",
14841
+ text: JSON.stringify({
14842
+ error: {
14843
+ code: IMPERSONATION_REFUSAL_CODE,
14844
+ message: IMPERSONATION_REFUSAL_MESSAGE,
14845
+ tool: toolName
14846
+ }
14847
+ })
14848
+ }
14849
+ ],
14850
+ isError: true
14851
+ };
14852
+ }
14853
+
14854
+ // src/channel-egress-tools.ts
14855
+ var SLACK_EGRESS_TOOLS = /* @__PURE__ */ new Set([
14856
+ "slack.reply",
14857
+ "slack.react",
14858
+ "slack.send_structured",
14859
+ "slack.ask_user",
14860
+ "slack.upload_file"
14861
+ ]);
14862
+
14816
14863
  // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.27.1_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
14817
14864
  import process2 from "process";
14818
14865
 
@@ -16909,6 +16956,9 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16909
16956
  }));
16910
16957
  mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
16911
16958
  const { name, arguments: args } = req.params;
16959
+ if (isImpersonating() && !channelsEnabledOverride() && SLACK_EGRESS_TOOLS.has(name)) {
16960
+ return buildImpersonationRefusal(name);
16961
+ }
16912
16962
  const progressResult = await progressRegistry.handle(name, args ?? {});
16913
16963
  if (progressResult !== void 0) return progressResult;
16914
16964
  if (name === "slack.reply") {
@@ -15810,6 +15810,52 @@ function createTelegramProgressFlush(opts) {
15810
15810
  };
15811
15811
  }
15812
15812
 
15813
+ // src/impersonation.ts
15814
+ var ENV_VAR = "AGT_ACT_AS_AGENT_ID";
15815
+ var OVERRIDE_ENV_VAR = "ENABLE_IMPERSONATION_CHANNELS";
15816
+ function isImpersonating() {
15817
+ return impersonatedAgentId() !== null;
15818
+ }
15819
+ function impersonatedAgentId() {
15820
+ const raw = process.env[ENV_VAR];
15821
+ if (typeof raw !== "string") return null;
15822
+ const trimmed = raw.trim();
15823
+ return trimmed.length > 0 ? trimmed : null;
15824
+ }
15825
+ function channelsEnabledOverride() {
15826
+ const raw = process.env[OVERRIDE_ENV_VAR];
15827
+ if (typeof raw !== "string") return false;
15828
+ const trimmed = raw.trim().toLowerCase();
15829
+ return trimmed === "1" || trimmed === "true" || trimmed === "yes" || trimmed === "on";
15830
+ }
15831
+ var IMPERSONATION_REFUSAL_CODE = "CHANNEL.IMPERSONATION_DISABLED";
15832
+ var IMPERSONATION_REFUSAL_MESSAGE = "Posting as an agent under impersonation is disabled in v0 \u2014 quit impersonation with `agt impersonate exit` to send as yourself.";
15833
+ function buildImpersonationRefusal(toolName) {
15834
+ return {
15835
+ content: [
15836
+ {
15837
+ type: "text",
15838
+ text: JSON.stringify({
15839
+ error: {
15840
+ code: IMPERSONATION_REFUSAL_CODE,
15841
+ message: IMPERSONATION_REFUSAL_MESSAGE,
15842
+ tool: toolName
15843
+ }
15844
+ })
15845
+ }
15846
+ ],
15847
+ isError: true
15848
+ };
15849
+ }
15850
+
15851
+ // src/channel-egress-tools.ts
15852
+ var TELEGRAM_EGRESS_TOOLS = /* @__PURE__ */ new Set([
15853
+ "telegram.reply",
15854
+ "telegram.send_message",
15855
+ "telegram.react",
15856
+ "channel_request_input"
15857
+ ]);
15858
+
15813
15859
  // src/mcp-spawn-lock.ts
15814
15860
  import {
15815
15861
  existsSync,
@@ -16519,7 +16565,7 @@ var mcp = new Server(
16519
16565
  "CRITICAL: every response to a Telegram <channel> tag MUST go through telegram.reply with the chat_id from the tag. Text in your session WITHOUT a telegram.reply call never reaches the user.",
16520
16566
  'Messages from Telegram arrive as <channel source="telegram" chat_id="..." user="..." user_name="..." message_id="...">. Pass reply_to_message_id from the tag so the response lands as a quote-reply in busy chats.',
16521
16567
  "Inbound attachments: <channel> `files` is a JSON-serialised array \u2014 JSON.parse it. If an entry has `path`, the image is already downloaded \u2014 Read it directly, do NOT call telegram.download_attachment. Use that tool only for entries with `file_id` but NO `path` (PDF, docx, voice, audio, video, animations): pass file_id + chat_id verbatim, then Read the returned path. Single-image messages also get a top-level `image_path`. Caption arrives as channel content. Don't surface internal file-handling errors that don't affect the answer.",
16522
- 'For work >30s follow CLAUDE.md kanban flow: kanban.add \u2192 reply "On it \u2014 tracking here: <kanban URL>" \u2192 move to in_progress \u2192 do the work \u2192 reply with the result. Simple lookups skip kanban but still reply.',
16568
+ 'For work >30s follow CLAUDE.md kanban flow: kanban_add \u2192 reply "On it \u2014 tracking here: <kanban URL>" \u2192 move to in_progress \u2192 do the work \u2192 reply with the result. Simple lookups skip kanban but still reply.',
16523
16569
  "Address users by user_name; user is the numeric Telegram ID. Resolve ambiguous times against your own Timezone from CLAUDE.md \u2014 do not ask.",
16524
16570
  "Reaction taxonomy (use telegram.react sparingly \u2014 prefer telegram.reply): \u{1F440} = ack (already auto-added on inbound, do not duplicate); \u{1F44D} or \u{1F389} = success. NEVER react to signal failure. On failure, telegram.reply with one sentence explaining what went wrong. Free-tier emoji: \u{1F44D} \u{1F44E} \u2764 \u{1F525} \u{1F389} \u{1F914} \u{1F92F} \u{1F64F} \u{1F44C} \u{1F440} \u{1F4AF} \u270D \u{1FAE1} \u{1F192} \u{1F973} \u{1F494}."
16525
16571
  ].join(" ")
@@ -16676,6 +16722,9 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
16676
16722
  }));
16677
16723
  mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
16678
16724
  const { name, arguments: args } = req.params;
16725
+ if (isImpersonating() && !channelsEnabledOverride() && TELEGRAM_EGRESS_TOOLS.has(name)) {
16726
+ return buildImpersonationRefusal(name);
16727
+ }
16679
16728
  const progressResult = await progressRegistry.handle(name, args ?? {});
16680
16729
  if (progressResult !== void 0) return progressResult;
16681
16730
  if (name === "channel_request_input") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.27.8",
3
+ "version": "0.27.10",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {