@integrity-labs/agt-cli 0.28.150 → 0.28.152

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.
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __augmentedCreateRequire } from 'module';
3
+ globalThis.require ??= __augmentedCreateRequire(import.meta.url);
4
+
5
+ // src/remote-oauth-proxy.ts
6
+ import { createInterface } from "readline";
7
+ import { readFileSync } from "fs";
8
+ import { fileURLToPath } from "url";
9
+ import { resolve as resolvePath } from "path";
10
+ var URL_ENV = "AGT_REMOTE_MCP_URL";
11
+ var TOKEN_FILE_ENV = "AGT_REMOTE_MCP_TOKEN_FILE";
12
+ var TOKEN_VAR_ENV = "AGT_REMOTE_MCP_TOKEN_VAR";
13
+ var REMOTE_FETCH_TIMEOUT_MS = 3e4;
14
+ var remoteUrl = process.env[URL_ENV] ?? "";
15
+ var tokenFile = process.env[TOKEN_FILE_ENV] ?? "";
16
+ var tokenVar = process.env[TOKEN_VAR_ENV] ?? "";
17
+ var label = process.env["AGT_REMOTE_MCP_LABEL"] || tokenVar || "remote-oauth";
18
+ function logErr(msg) {
19
+ process.stderr.write(`[remote-oauth-proxy:${label}] ${msg}
20
+ `);
21
+ }
22
+ function readCurrentToken(file, varName) {
23
+ let raw;
24
+ try {
25
+ raw = readFileSync(file, "utf8");
26
+ } catch {
27
+ return null;
28
+ }
29
+ let value = null;
30
+ for (const line of raw.split("\n")) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed || trimmed.startsWith("#")) continue;
33
+ const eq = trimmed.indexOf("=");
34
+ if (eq < 0) continue;
35
+ const key = trimmed.slice(0, eq).replace(/^export\s+/, "").trim();
36
+ if (key !== varName) continue;
37
+ let v = trimmed.slice(eq + 1).trim();
38
+ if (v.length >= 2 && (v[0] === '"' && v[v.length - 1] === '"' || v[0] === "'" && v[v.length - 1] === "'")) {
39
+ v = v.slice(1, -1);
40
+ }
41
+ value = v;
42
+ }
43
+ return value && value.length > 0 ? value : null;
44
+ }
45
+ function jsonRpcError(id, code, message) {
46
+ return JSON.stringify({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
47
+ }
48
+ function extractJsonRpcMessages(contentType, body) {
49
+ const ct = (contentType || "").toLowerCase();
50
+ const out = [];
51
+ if (ct.includes("text/event-stream")) {
52
+ for (const line of body.split("\n")) {
53
+ const t = line.trimEnd();
54
+ if (!t.startsWith("data:")) continue;
55
+ const payload = t.slice(5).trim();
56
+ if (!payload || payload === "[DONE]") continue;
57
+ try {
58
+ JSON.parse(payload);
59
+ out.push(payload);
60
+ } catch {
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+ const trimmed = body.trim();
66
+ if (!trimmed) return out;
67
+ try {
68
+ JSON.parse(trimmed);
69
+ out.push(trimmed);
70
+ } catch {
71
+ }
72
+ return out;
73
+ }
74
+ async function forward(line, id, isNotification) {
75
+ const token = readCurrentToken(tokenFile, tokenVar);
76
+ if (!token) {
77
+ logErr(`no token in ${tokenFile} (var ${tokenVar})`);
78
+ return isNotification ? [] : [jsonRpcError(id, -32e3, `${label}: no current access token available (reconnect the integration)`)];
79
+ }
80
+ let res;
81
+ try {
82
+ res = await fetch(remoteUrl, {
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ Accept: "application/json, text/event-stream",
87
+ Authorization: `Bearer ${token}`
88
+ },
89
+ body: line,
90
+ signal: AbortSignal.timeout(REMOTE_FETCH_TIMEOUT_MS)
91
+ });
92
+ } catch (err) {
93
+ logErr(`fetch failed: ${err.message}`);
94
+ return isNotification ? [] : [jsonRpcError(id, -32001, `${label}: transport error contacting MCP server`)];
95
+ }
96
+ const ct = res.headers.get("content-type") ?? "";
97
+ let body = "";
98
+ try {
99
+ body = await res.text();
100
+ } catch {
101
+ body = "";
102
+ }
103
+ if (isNotification) return [];
104
+ const messages = extractJsonRpcMessages(ct, body);
105
+ if (messages.length > 0) return messages;
106
+ const detail = body.slice(0, 300).replace(/\s+/g, " ").trim();
107
+ return [jsonRpcError(id, -32002, `${label}: MCP server returned HTTP ${res.status}${detail ? ` (${detail})` : ""}`)];
108
+ }
109
+ function isMissingConfig() {
110
+ if (!remoteUrl) return URL_ENV;
111
+ if (!tokenFile) return TOKEN_FILE_ENV;
112
+ if (!tokenVar) return TOKEN_VAR_ENV;
113
+ return null;
114
+ }
115
+ async function main() {
116
+ const missing = isMissingConfig();
117
+ if (missing) {
118
+ logErr(`missing required env ${missing}; exiting`);
119
+ process.exit(1);
120
+ }
121
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
122
+ let writeLock = Promise.resolve();
123
+ const writeReplies = (replies) => {
124
+ writeLock = writeLock.then(() => {
125
+ for (const reply of replies) process.stdout.write(reply + "\n");
126
+ });
127
+ return writeLock;
128
+ };
129
+ const inflight = /* @__PURE__ */ new Set();
130
+ rl.on("line", (line) => {
131
+ const text = line.trim();
132
+ if (!text) return;
133
+ let parsed;
134
+ try {
135
+ parsed = JSON.parse(text);
136
+ } catch {
137
+ logErr("dropping non-JSON stdin line");
138
+ return;
139
+ }
140
+ const isNotification = !("id" in parsed);
141
+ const id = parsed.id;
142
+ const task = forward(text, id, isNotification).then(writeReplies).catch((err) => {
143
+ logErr(`handler error: ${err.message}`);
144
+ });
145
+ inflight.add(task);
146
+ void task.finally(() => inflight.delete(task));
147
+ });
148
+ await new Promise((resolve) => rl.on("close", () => resolve()));
149
+ await Promise.all([...inflight]);
150
+ await writeLock;
151
+ }
152
+ var invokedDirectly = (() => {
153
+ try {
154
+ const entry = process.argv[1];
155
+ if (!entry) return false;
156
+ return fileURLToPath(import.meta.url) === resolvePath(entry);
157
+ } catch {
158
+ return false;
159
+ }
160
+ })();
161
+ if (invokedDirectly) {
162
+ void main();
163
+ }
164
+ export {
165
+ extractJsonRpcMessages,
166
+ main,
167
+ readCurrentToken
168
+ };
@@ -16399,7 +16399,26 @@ function buildChannelInfoResult(args) {
16399
16399
  return { ok: false, reason: "unknown", bot_user_handle: botUserHandle, raw_error: err };
16400
16400
  }
16401
16401
  const ch = infoRes.channel;
16402
- if (!ch || !ch.id || !ch.name) {
16402
+ if (!ch || !ch.id) {
16403
+ return { ok: false, reason: "unknown", bot_user_handle: botUserHandle, raw_error: "malformed_response" };
16404
+ }
16405
+ const isDm = ch.is_im === true || typeof ch.user === "string" && ch.user.length > 0;
16406
+ if (isDm) {
16407
+ const dmChannel = {
16408
+ id: ch.id,
16409
+ name: ch.user ?? ch.id,
16410
+ is_private: true,
16411
+ is_archived: ch.is_archived === true,
16412
+ is_member: true,
16413
+ kind: "dm",
16414
+ user: ch.user
16415
+ };
16416
+ if (dmChannel.is_archived) {
16417
+ return { ok: false, channel: dmChannel, bot_user_handle: botUserHandle, reason: "archived" };
16418
+ }
16419
+ return { ok: true, channel: dmChannel, bot_user_handle: botUserHandle };
16420
+ }
16421
+ if (!ch.name) {
16403
16422
  return { ok: false, reason: "unknown", bot_user_handle: botUserHandle, raw_error: "malformed_response" };
16404
16423
  }
16405
16424
  const channel = {
@@ -16417,6 +16436,17 @@ function buildChannelInfoResult(args) {
16417
16436
  }
16418
16437
  return { ok: true, channel, bot_user_handle: botUserHandle };
16419
16438
  }
16439
+ function buildDmUserInfo(usersInfoRes) {
16440
+ if (!usersInfoRes || usersInfoRes.ok === false || !usersInfoRes.user) return {};
16441
+ const u = usersInfoRes.user;
16442
+ const nonEmpty = (s) => typeof s === "string" && s.trim().length > 0 ? s : void 0;
16443
+ const user_name = nonEmpty(u.profile?.display_name) ?? nonEmpty(u.profile?.real_name) ?? nonEmpty(u.real_name);
16444
+ const user_handle = nonEmpty(u.name);
16445
+ const out = {};
16446
+ if (user_handle) out.user_handle = user_handle;
16447
+ if (user_name) out.user_name = user_name;
16448
+ return out;
16449
+ }
16420
16450
 
16421
16451
  // src/slack-peer-classifier.ts
16422
16452
  var CODE_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
@@ -19208,13 +19238,13 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
19208
19238
  },
19209
19239
  {
19210
19240
  name: "slack.channel_info",
19211
- description: 'Verify the bot can post to a specific Slack channel. Use this before relying on a channel ID for delivery (e.g. before submitting an AWS access request that will notify an approval channel) to avoid silent posting failures. Works for both public AND private channels \u2014 unlike slack.list_channels which is public-only. Returns `{ ok, channel: { id, name, is_private, is_archived, is_member }, bot_user_handle, reason? }`. When `ok=false`, branch on `reason`: "channel_not_found" or "not_in_channel" \u2192 tell the user to run `/invite @<bot_user_handle>` in that channel (the configured ID exists but your bot isn\'t a member, or the ID is from a workspace the bot isn\'t installed in); "archived" \u2192 the channel exists but is archived, ask an admin to point the integration at a non-archived channel; "auth_failed" \u2192 bot token is invalid, escalate to an operator (do not surface to the end user). Pass the raw C0... channel ID, not a human name (use slack.list_channels for name \u2192 ID).',
19241
+ description: 'Verify the bot can post to a specific Slack channel, OR resolve who is on the other side of a DM. Use this before relying on a channel ID for delivery (e.g. before submitting an AWS access request that will notify an approval channel) to avoid silent posting failures, and to confirm the human behind a DM channel id before claiming a message reached a named person. Works for public AND private channels (unlike slack.list_channels, which is public-only) and for DM (`D0...`) channels. Returns `{ ok, channel: { id, name, is_private, is_archived, is_member, kind }, bot_user_handle, reason? }`. For a DM, `kind` is "dm" and the result also carries `channel.user` (the partner\'s Slack user id) plus best-effort `channel.user_handle` and `channel.user_name` (the human\'s handle and display name); use these to VERIFY a DM\'s recipient rather than assuming whose DM it is. When `ok=false`, branch on `reason`: "channel_not_found" or "not_in_channel" \u2192 tell the user to run `/invite @<bot_user_handle>` in that channel (the configured ID exists but your bot isn\'t a member, or the ID is from a workspace the bot isn\'t installed in); "archived" \u2192 the channel exists but is archived, ask an admin to point the integration at a non-archived channel; "auth_failed" \u2192 bot token is invalid, escalate to an operator (do not surface to the end user). Pass the raw C0.../G0.../D0... channel ID, not a human name (use slack.list_channels for name \u2192 ID).',
19212
19242
  inputSchema: {
19213
19243
  type: "object",
19214
19244
  properties: {
19215
19245
  channel_id: {
19216
19246
  type: "string",
19217
- description: "Slack channel ID (C0..., G0... for legacy private). Required."
19247
+ description: "Slack channel ID: C0... (public), G0... (legacy private), or D0... (DM). Required."
19218
19248
  }
19219
19249
  },
19220
19250
  required: ["channel_id"]
@@ -19736,6 +19766,25 @@ async function handleChannelInfo(args) {
19736
19766
  infoRes: infoData,
19737
19767
  botUserHandle
19738
19768
  });
19769
+ if (result.channel?.kind === "dm" && result.channel.user) {
19770
+ const controller = new AbortController();
19771
+ const timeout = setTimeout(() => controller.abort(), 3e3);
19772
+ try {
19773
+ const usersRes = await fetch(
19774
+ `https://slack.com/api/users.info?user=${encodeURIComponent(result.channel.user)}`,
19775
+ { headers: { Authorization: `Bearer ${BOT_TOKEN}` }, signal: controller.signal }
19776
+ );
19777
+ const usersData = await usersRes.json();
19778
+ const { user_handle, user_name } = buildDmUserInfo(usersData);
19779
+ if (user_handle) result.channel.user_handle = user_handle;
19780
+ if (user_name) result.channel.user_name = user_name;
19781
+ if (user_name) result.channel.name = user_name;
19782
+ else if (user_handle) result.channel.name = user_handle;
19783
+ } catch {
19784
+ } finally {
19785
+ clearTimeout(timeout);
19786
+ }
19787
+ }
19739
19788
  return {
19740
19789
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
19741
19790
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@integrity-labs/agt-cli",
3
- "version": "0.28.150",
3
+ "version": "0.28.152",
4
4
  "description": "Augmented Team CLI — agent provisioning and management",
5
5
  "type": "module",
6
6
  "engines": {
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsup && npm run build:mcp-assets && npm run build:cli-assets",
26
- "build:mcp-assets": "mkdir -p dist/mcp && cp ../../packages/mcp/dist/index.js dist/mcp/index.js && cp ../../packages/mcp/dist/slack-channel.js dist/mcp/slack-channel.js && cp ../../packages/mcp/dist/direct-chat-channel.js dist/mcp/direct-chat-channel.js && cp ../../packages/mcp/dist/telegram-channel.js dist/mcp/telegram-channel.js && cp ../../packages/mcp/dist/teams-channel.js dist/mcp/teams-channel.js && cp ../../packages/mcp/dist/whatsapp-channel.js dist/mcp/whatsapp-channel.js && cp ../../packages/mcp/dist/whatsapp-link.js dist/mcp/whatsapp-link.js && cp ../../packages/augmented-admin-mcp/dist/index.js dist/mcp/augmented-admin.js",
26
+ "build:mcp-assets": "mkdir -p dist/mcp && cp ../../packages/mcp/dist/index.js dist/mcp/index.js && cp ../../packages/mcp/dist/slack-channel.js dist/mcp/slack-channel.js && cp ../../packages/mcp/dist/direct-chat-channel.js dist/mcp/direct-chat-channel.js && cp ../../packages/mcp/dist/telegram-channel.js dist/mcp/telegram-channel.js && cp ../../packages/mcp/dist/teams-channel.js dist/mcp/teams-channel.js && cp ../../packages/mcp/dist/whatsapp-channel.js dist/mcp/whatsapp-channel.js && cp ../../packages/mcp/dist/whatsapp-link.js dist/mcp/whatsapp-link.js && cp ../../packages/mcp/dist/remote-oauth-proxy.js dist/mcp/remote-oauth-proxy.js && cp ../../packages/augmented-admin-mcp/dist/index.js dist/mcp/augmented-admin.js",
27
27
  "build:cli-assets": "mkdir -p dist/assets && cp assets/impersonate-statusline.sh dist/assets/impersonate-statusline.sh && chmod +x dist/assets/impersonate-statusline.sh",
28
28
  "dev": "tsx watch src/bin/agt.ts",
29
29
  "test": "vitest run",