@integrity-labs/agt-cli 0.28.917 → 0.28.919

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/mcp/index.js CHANGED
@@ -45752,6 +45752,50 @@ function describeFeatureRequestResult(resp) {
45752
45752
  isError: true
45753
45753
  };
45754
45754
  }
45755
+ var FEATURE_REQUEST_LIST_STATES = [
45756
+ "open",
45757
+ "in_progress",
45758
+ "resolved",
45759
+ "closed",
45760
+ "unknown"
45761
+ ];
45762
+ var DESCRIPTION_NOT_RECORDED = "(body not recorded: this request was filed before request_feature_list existed; only the title is known)";
45763
+ function describeFeatureRequestList(resp, stateFilter) {
45764
+ if (resp.status !== "listed" || !Array.isArray(resp.requests)) {
45765
+ return {
45766
+ text: `Could not list feature requests${resp.message ? `: ${resp.message}` : ""}. Do not assume nothing was filed; tell your operator.`,
45767
+ isError: true
45768
+ };
45769
+ }
45770
+ const rows = resp.requests;
45771
+ const total = resp.total ?? rows.length;
45772
+ const notes = [];
45773
+ if (resp.state_source === "unavailable") {
45774
+ notes.push(
45775
+ 'Status could not be read from the support tracker, so every state below is "unknown". Do not read unknown as open or resolved.'
45776
+ );
45777
+ }
45778
+ if (resp.truncated) {
45779
+ notes.push(
45780
+ `Only the newest ${resp.scanned ?? rows.length} of ${total} requests were examined; older ones are not listed${stateFilter ? " and were not checked against the filter" : ""}.`
45781
+ );
45782
+ }
45783
+ if (rows.length === 0) {
45784
+ const head = total === 0 ? "Your team has no feature requests on record." : `None of your team's ${resp.scanned ?? total} most recent feature requests are in state "${stateFilter}".`;
45785
+ return { text: [head, ...notes].join("\n"), isError: false };
45786
+ }
45787
+ const header = `${rows.length} feature request${rows.length === 1 ? "" : "s"} filed by your team${stateFilter ? ` in state "${stateFilter}"` : ""} (newest first):`;
45788
+ const body = rows.map((r) => {
45789
+ const kind = [r.feature_type, r.provider].filter(Boolean).join(", ");
45790
+ const who = r.filed_by_me ? "by you" : "by a teammate";
45791
+ const day = r.created_at ? r.created_at.slice(0, 10) : "unknown date";
45792
+ return [
45793
+ `- [${r.state}] ${r.title}${kind ? ` (${kind})` : ""}, filed ${day} ${who}, id: ${r.id}`,
45794
+ ` ${r.description ?? DESCRIPTION_NOT_RECORDED}`
45795
+ ].join("\n");
45796
+ });
45797
+ return { text: [header, ...notes, ...body].join("\n"), isError: false };
45798
+ }
45755
45799
 
45756
45800
  // src/recruit-agent.ts
45757
45801
  function describeRecruitAgentResult(resp) {
@@ -47586,6 +47630,47 @@ server.tool(
47586
47630
  return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
47587
47631
  }
47588
47632
  );
47633
+ server.tool(
47634
+ "request_feature_list",
47635
+ 'List the feature requests your TEAM has already sent to Ninjafy support with request_feature, newest first. Call this BEFORE request_feature so you do not file a duplicate: each entry shows the full request body as it was submitted (never a preview), its current state (open, in_progress, resolved, closed, or unknown), and whether you or a teammate filed it. "closed" means it was declined or merged into another request; "unknown" means the status could not be read, not that it is open. Requests filed before this tool existed show their title but no body. Support ticket numbers and links are internal and are never shown. A request whose record failed to save when it was filed cannot appear here, so an empty list is strong evidence, not proof, that nothing was asked.',
47636
+ {
47637
+ state: external_exports.enum(FEATURE_REQUEST_LIST_STATES).optional().describe('Only return requests in this state, e.g. "open" to see what is still outstanding. Omit for all.')
47638
+ },
47639
+ async (params) => {
47640
+ if (!AGT_AGENT_CODE_NAME) {
47641
+ return {
47642
+ content: [{ type: "text", text: "Error: AGT_AGENT_CODE_NAME not configured." }],
47643
+ isError: true
47644
+ };
47645
+ }
47646
+ let resp;
47647
+ try {
47648
+ resp = await apiPost(
47649
+ "/host/request-feature/list",
47650
+ {
47651
+ agent_code_name: AGT_AGENT_CODE_NAME,
47652
+ ...params.state ? { state: params.state } : {}
47653
+ },
47654
+ false,
47655
+ 15e3,
47656
+ "request_feature_list",
47657
+ true
47658
+ );
47659
+ } catch (err) {
47660
+ return {
47661
+ content: [
47662
+ {
47663
+ type: "text",
47664
+ text: `Could not reach the feature-request service (${err instanceof Error ? err.message : String(err)}). Do not assume nothing was filed.`
47665
+ }
47666
+ ],
47667
+ isError: true
47668
+ };
47669
+ }
47670
+ const { text, isError } = describeFeatureRequestList(resp, params.state);
47671
+ return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
47672
+ }
47673
+ );
47589
47674
  server.tool(
47590
47675
  "list_agent_roles",
47591
47676
  "List the predefined agent roles available to your organization. Call this BEFORE recruit_agent, and go through the results with the person who asked, so the new teammate is created from a curated role rather than a label you invented. Each role carries a vetted description, a risk tier, scheduled tasks and bundled skills that are provisioned automatically when the role is used \u2014 a made-up role label gets none of that. Pass the chosen role_id to recruit_agent.",
@@ -49452,6 +49537,9 @@ var LOCAL_TOOL_NAMES = /* @__PURE__ */ new Set([
49452
49537
  // this name, so the forwarding-skip is a no-op; listed here to keep the
49453
49538
  // lockstep guard green.
49454
49539
  "request_feature",
49540
+ // ENG-10427: the read half of request_feature (always registered). No API tool
49541
+ // shares this name; listed here to keep the lockstep guard green.
49542
+ "request_feature_list",
49455
49543
  // ENG-7495: agent-requested integration reconnect (always registered). No
49456
49544
  // API tool shares this name; listed here to keep the lockstep guard green.
49457
49545
  "request_reconnect",
@@ -30525,6 +30525,9 @@ function decideSenderPolicyForward(evt, policy) {
30525
30525
  }
30526
30526
 
30527
30527
  // ../core/dist/channels/governance/sender-policy-decline.js
30528
+ function teamOnlyReason(allowlist) {
30529
+ return allowlist && allowlist.length > 0 ? "mode_team_only" : "mode_team_only_no_principals";
30530
+ }
30528
30531
  function classifySlackPolicyBlock(evt, policy) {
30529
30532
  if (policy.internalOnly && (!policy.homeTeamId || evt.team !== policy.homeTeamId)) {
30530
30533
  return "internal_only";
@@ -30533,7 +30536,7 @@ function classifySlackPolicyBlock(evt, policy) {
30533
30536
  case "manager_only":
30534
30537
  return "mode_manager_only";
30535
30538
  case "team_only":
30536
- return "mode_team_only";
30539
+ return teamOnlyReason(policy.teamPrincipalSlackUserIds);
30537
30540
  case "team_agents_only":
30538
30541
  return "mode_team_agents_only";
30539
30542
  case "agents_only":
@@ -30550,12 +30553,21 @@ function politeDeclineCopy(reason) {
30550
30553
  return "Sorry, I only respond to my manager in this channel.";
30551
30554
  case "mode_team_only":
30552
30555
  return "Sorry, this agent only replies to its team members.";
30556
+ case "mode_team_only_no_principals":
30557
+ return "Sorry, I can't reply to anyone in this channel yet: none of my team members' accounts here are linked to me. An admin needs to link them first.";
30553
30558
  case "mode_team_agents_only":
30554
30559
  return "Sorry, I only respond to agents on my team in this channel.";
30555
30560
  case "mode_agents_only":
30556
30561
  return "Sorry, I only respond to other agents in this channel.";
30557
30562
  }
30558
30563
  }
30564
+ function isSlackDeclineAddressed(input) {
30565
+ if (input.eventType === "app_mention")
30566
+ return true;
30567
+ if (typeof input.channelId === "string" && input.channelId.startsWith("D"))
30568
+ return true;
30569
+ return !!input.botUserId && typeof input.text === "string" && input.text.includes(`<@${input.botUserId}>`);
30570
+ }
30559
30571
  function decideDeclineReply(input) {
30560
30572
  const last = input.cache.get(input.key);
30561
30573
  if (last !== void 0) {
@@ -30581,6 +30593,10 @@ function readDeclineCooldownMs(envVarName, defaultSeconds = 1800) {
30581
30593
  }
30582
30594
 
30583
30595
  // ../core/dist/channels/governance/inbound-access.js
30596
+ function isSameSlackWorkspace(input) {
30597
+ const home = (input.provisionedHomeTeamId && input.provisionedHomeTeamId.length > 0 ? input.provisionedHomeTeamId : null) ?? input.authTestHomeTeamId;
30598
+ return !!home && typeof input.senderTeamId === "string" && input.senderTeamId === home;
30599
+ }
30584
30600
  function decideInboundAccess(input) {
30585
30601
  if (!input.content.forward) {
30586
30602
  return { kind: "drop", reason: `content:${input.content.reason}` };
@@ -46853,12 +46869,16 @@ var SLACK_AUTH_FAILURE_CODES = /* @__PURE__ */ new Set([
46853
46869
  "account_inactive",
46854
46870
  "not_authed"
46855
46871
  ]);
46872
+ var authTestHomeTeamId = null;
46856
46873
  async function getBotUserId() {
46857
46874
  try {
46858
46875
  const res = await fetch("https://slack.com/api/auth.test", {
46859
46876
  headers: { Authorization: `Bearer ${BOT_TOKEN}` }
46860
46877
  });
46861
46878
  const data = await res.json();
46879
+ if (data.ok && typeof data.team_id === "string" && data.team_id.length > 0) {
46880
+ authTestHomeTeamId = data.team_id;
46881
+ }
46862
46882
  if (data.ok) return { id: data.user_id ?? null, authFailed: false };
46863
46883
  return { id: null, authFailed: !!data.error && SLACK_AUTH_FAILURE_CODES.has(data.error) };
46864
46884
  } catch {
@@ -49081,8 +49101,11 @@ async function connectSocketMode() {
49081
49101
  const isWatchDm = evt.channel?.startsWith("D") ?? false;
49082
49102
  const isWatchCommand = !isBot && watchArgsFromText(strippedText, AGENT_CODE_NAME, { allowBare: isWatchDm }) !== null;
49083
49103
  const command = isBot ? void 0 : isHelpCommand ? { command: "help", authorized: true } : isWatchCommand ? { command: "watch", authorized: isRestartSenderAllowed(getEffectiveAllowedUsers(), evt.user) } : isRestartCommand ? { command: "restart", authorized: isRestartSenderAllowed(getEffectiveAllowedUsers(), evt.user) } : void 0;
49084
- const slackHomeTeamId = process.env.SLACK_HOME_TEAM_ID;
49085
- const sameOrg = !!slackHomeTeamId && evt.team === slackHomeTeamId;
49104
+ const sameOrg = isSameSlackWorkspace({
49105
+ senderTeamId: evt.team,
49106
+ provisionedHomeTeamId: process.env.SLACK_HOME_TEAM_ID,
49107
+ authTestHomeTeamId
49108
+ });
49086
49109
  const access = decideInboundAccess({
49087
49110
  content: filterDecision.forward ? { forward: true } : { forward: false, reason: filterDecision.reason },
49088
49111
  // The agent's own user-echo was already skipped above; peer 'self'
@@ -49125,7 +49148,12 @@ async function connectSocketMode() {
49125
49148
  if (hint) process.stderr.write(`${hint}
49126
49149
  `);
49127
49150
  }
49128
- if (access.decline !== void 0 && policyBlockReason) {
49151
+ if (access.decline !== void 0 && policyBlockReason && isSlackDeclineAddressed({
49152
+ eventType: evt.type,
49153
+ channelId: evt.channel,
49154
+ text: evt.text,
49155
+ botUserId
49156
+ })) {
49129
49157
  await maybeSendSenderPolicyDecline({
49130
49158
  channel: evt.channel,
49131
49159
  senderId: evt.user,
@@ -14863,6 +14863,9 @@ function decideSenderPolicyForwardTeams(activity, policy) {
14863
14863
  }
14864
14864
 
14865
14865
  // ../core/dist/channels/governance/sender-policy-decline.js
14866
+ function teamOnlyReason(allowlist) {
14867
+ return allowlist && allowlist.length > 0 ? "mode_team_only" : "mode_team_only_no_principals";
14868
+ }
14866
14869
  function classifyTeamsPolicyBlock(activity, policy) {
14867
14870
  const senderTenantId = activity.channelData?.tenant?.id ?? activity.conversation?.tenantId;
14868
14871
  if (policy.internalOnly && (!policy.homeTenantId || policy.homeTenantId === "common" || senderTenantId !== policy.homeTenantId)) {
@@ -14872,7 +14875,7 @@ function classifyTeamsPolicyBlock(activity, policy) {
14872
14875
  case "manager_only":
14873
14876
  return "mode_manager_only";
14874
14877
  case "team_only":
14875
- return "mode_team_only";
14878
+ return teamOnlyReason(policy.teamPrincipalAadObjectIds);
14876
14879
  case "team_agents_only":
14877
14880
  return "mode_team_agents_only";
14878
14881
  case "agents_only":
@@ -14889,6 +14892,8 @@ function politeDeclineCopy(reason) {
14889
14892
  return "Sorry, I only respond to my manager in this channel.";
14890
14893
  case "mode_team_only":
14891
14894
  return "Sorry, this agent only replies to its team members.";
14895
+ case "mode_team_only_no_principals":
14896
+ return "Sorry, I can't reply to anyone in this channel yet: none of my team members' accounts here are linked to me. An admin needs to link them first.";
14892
14897
  case "mode_team_agents_only":
14893
14898
  return "Sorry, I only respond to agents on my team in this channel.";
14894
14899
  case "mode_agents_only":
@@ -30680,6 +30680,8 @@ function politeDeclineCopy(reason) {
30680
30680
  return "Sorry, I only respond to my manager in this channel.";
30681
30681
  case "mode_team_only":
30682
30682
  return "Sorry, this agent only replies to its team members.";
30683
+ case "mode_team_only_no_principals":
30684
+ return "Sorry, I can't reply to anyone in this channel yet: none of my team members' accounts here are linked to me. An admin needs to link them first.";
30683
30685
  case "mode_team_agents_only":
30684
30686
  return "Sorry, I only respond to agents on my team in this channel.";
30685
30687
  case "mode_agents_only":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.28.917",
3
+ "version": "0.28.919",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {