slack-axi 0.1.0

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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/bin/slack-axi.d.ts +2 -0
  4. package/dist/bin/slack-axi.js +4 -0
  5. package/dist/bin/slack-axi.js.map +1 -0
  6. package/dist/src/auth/setup.d.ts +39 -0
  7. package/dist/src/auth/setup.js +119 -0
  8. package/dist/src/auth/setup.js.map +1 -0
  9. package/dist/src/cli.d.ts +2 -0
  10. package/dist/src/cli.js +66 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/commands/auth.d.ts +2 -0
  13. package/dist/src/commands/auth.js +192 -0
  14. package/dist/src/commands/auth.js.map +1 -0
  15. package/dist/src/commands/cache.d.ts +2 -0
  16. package/dist/src/commands/cache.js +25 -0
  17. package/dist/src/commands/cache.js.map +1 -0
  18. package/dist/src/commands/catchup.d.ts +2 -0
  19. package/dist/src/commands/catchup.js +171 -0
  20. package/dist/src/commands/catchup.js.map +1 -0
  21. package/dist/src/commands/channels.d.ts +6 -0
  22. package/dist/src/commands/channels.js +147 -0
  23. package/dist/src/commands/channels.js.map +1 -0
  24. package/dist/src/commands/cite.d.ts +2 -0
  25. package/dist/src/commands/cite.js +38 -0
  26. package/dist/src/commands/cite.js.map +1 -0
  27. package/dist/src/commands/doctor.d.ts +2 -0
  28. package/dist/src/commands/doctor.js +46 -0
  29. package/dist/src/commands/doctor.js.map +1 -0
  30. package/dist/src/commands/home.d.ts +6 -0
  31. package/dist/src/commands/home.js +37 -0
  32. package/dist/src/commands/home.js.map +1 -0
  33. package/dist/src/commands/read.d.ts +4 -0
  34. package/dist/src/commands/read.js +178 -0
  35. package/dist/src/commands/read.js.map +1 -0
  36. package/dist/src/commands/search.d.ts +2 -0
  37. package/dist/src/commands/search.js +135 -0
  38. package/dist/src/commands/search.js.map +1 -0
  39. package/dist/src/commands/setup.d.ts +2 -0
  40. package/dist/src/commands/setup.js +21 -0
  41. package/dist/src/commands/setup.js.map +1 -0
  42. package/dist/src/commands/write.d.ts +4 -0
  43. package/dist/src/commands/write.js +148 -0
  44. package/dist/src/commands/write.js.map +1 -0
  45. package/dist/src/config.d.ts +71 -0
  46. package/dist/src/config.js +184 -0
  47. package/dist/src/config.js.map +1 -0
  48. package/dist/src/flags.d.ts +11 -0
  49. package/dist/src/flags.js +21 -0
  50. package/dist/src/flags.js.map +1 -0
  51. package/dist/src/meta.d.ts +3 -0
  52. package/dist/src/meta.js +17 -0
  53. package/dist/src/meta.js.map +1 -0
  54. package/dist/src/output.d.ts +25 -0
  55. package/dist/src/output.js +52 -0
  56. package/dist/src/output.js.map +1 -0
  57. package/dist/src/session.d.ts +17 -0
  58. package/dist/src/session.js +25 -0
  59. package/dist/src/session.js.map +1 -0
  60. package/dist/src/slack/cache.d.ts +40 -0
  61. package/dist/src/slack/cache.js +161 -0
  62. package/dist/src/slack/cache.js.map +1 -0
  63. package/dist/src/slack/client.d.ts +21 -0
  64. package/dist/src/slack/client.js +39 -0
  65. package/dist/src/slack/client.js.map +1 -0
  66. package/dist/src/slack/errors.d.ts +14 -0
  67. package/dist/src/slack/errors.js +54 -0
  68. package/dist/src/slack/errors.js.map +1 -0
  69. package/dist/src/slack/format.d.ts +8 -0
  70. package/dist/src/slack/format.js +20 -0
  71. package/dist/src/slack/format.js.map +1 -0
  72. package/dist/src/slack/permalink.d.ts +7 -0
  73. package/dist/src/slack/permalink.js +10 -0
  74. package/dist/src/slack/permalink.js.map +1 -0
  75. package/dist/src/slack/resolve.d.ts +22 -0
  76. package/dist/src/slack/resolve.js +118 -0
  77. package/dist/src/slack/resolve.js.map +1 -0
  78. package/dist/src/slack/scopes.d.ts +7 -0
  79. package/dist/src/slack/scopes.js +23 -0
  80. package/dist/src/slack/scopes.js.map +1 -0
  81. package/dist/src/slack/threads.d.ts +24 -0
  82. package/dist/src/slack/threads.js +63 -0
  83. package/dist/src/slack/threads.js.map +1 -0
  84. package/dist/src/slack/time.d.ts +46 -0
  85. package/dist/src/slack/time.js +156 -0
  86. package/dist/src/slack/time.js.map +1 -0
  87. package/dist/src/slack/ts.d.ts +5 -0
  88. package/dist/src/slack/ts.js +14 -0
  89. package/dist/src/slack/ts.js.map +1 -0
  90. package/package.json +41 -0
@@ -0,0 +1,39 @@
1
+ import { WebClient } from "@slack/web-api";
2
+ /** Build a Slack Web API client for a token. The SDK handles tiered rate-limit retry. */
3
+ export function webClient(token) {
4
+ return new WebClient(token, { retryConfig: { retries: 3 } });
5
+ }
6
+ /**
7
+ * Validate a token via auth.test and capture granted scopes.
8
+ *
9
+ * Uses a direct fetch (not WebClient) because the granted scopes are returned in the
10
+ * `x-oauth-scopes` response header, which we want alongside the body in a single call —
11
+ * the reliable source for scope-coverage checks (see specs/behaviors/auth-and-workspaces.md).
12
+ * Throws the raw Slack error code (string) on failure for the caller to translate.
13
+ */
14
+ export async function validateToken(token) {
15
+ const res = await fetch("https://slack.com/api/auth.test", {
16
+ method: "POST",
17
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-www-form-urlencoded" },
18
+ });
19
+ const scopes = (res.headers.get("x-oauth-scopes") ?? "")
20
+ .split(",")
21
+ .map((s) => s.trim())
22
+ .filter(Boolean);
23
+ const body = (await res.json());
24
+ if (!body.ok) {
25
+ // Throw the bare Slack error code; callers translate via slackErrorToAxi.
26
+ throw Object.assign(new Error(body.error ?? "invalid_auth"), {
27
+ data: { error: body.error ?? "invalid_auth" },
28
+ });
29
+ }
30
+ return {
31
+ teamId: body.team_id ?? "",
32
+ teamName: body.team ?? "",
33
+ userId: body.user_id ?? "",
34
+ userName: body.user,
35
+ url: body.url,
36
+ scopes,
37
+ };
38
+ }
39
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/slack/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAY3C,yFAAyF;AACzF,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAO,IAAI,SAAS,CAAC,KAAK,EAAE,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAa;IAC/C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,iCAAiC,EAAE;QACzD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;KACnG,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;SACrD,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;IAEnB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAQ7B,CAAC;IAEF,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACb,0EAA0E;QAC1E,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,cAAc,CAAC,EAAE;YAC3D,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,cAAc,EAAE;SAC9C,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,MAAM,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE;QAC1B,QAAQ,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE;QACzB,MAAM,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE;QAC1B,QAAQ,EAAE,IAAI,CAAC,IAAI;QACnB,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,MAAM;KACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ /**
3
+ * Translate a Slack API error code into a structured AxiError with an actionable suggestion.
4
+ * See specs/behaviors/errors.md. Raw Slack/SDK payloads never reach the agent.
5
+ */
6
+ export declare function slackErrorToAxi(code: string, context?: {
7
+ team?: string;
8
+ channel?: string;
9
+ }): AxiError;
10
+ /** Coerce an unknown thrown value (incl. @slack/web-api errors) into an AxiError. */
11
+ export declare function toAxiError(err: unknown, context?: {
12
+ team?: string;
13
+ channel?: string;
14
+ }): AxiError;
@@ -0,0 +1,54 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ /**
3
+ * Translate a Slack API error code into a structured AxiError with an actionable suggestion.
4
+ * See specs/behaviors/errors.md. Raw Slack/SDK payloads never reach the agent.
5
+ */
6
+ export function slackErrorToAxi(code, context = {}) {
7
+ const teamFlag = context.team ? ` --team ${context.team}` : "";
8
+ switch (code) {
9
+ case "invalid_auth":
10
+ case "token_revoked":
11
+ case "token_expired":
12
+ case "account_inactive":
13
+ return new AxiError("Slack token is invalid or revoked", "AUTH_INVALID", [`Run \`slack-axi auth login${teamFlag} --token xoxp-...\` to store a valid token`]);
14
+ case "not_authed":
15
+ return new AxiError("No Slack token was supplied", "NO_TOKEN", ["Set SLACK_AXI_TOKEN or run `slack-axi auth login`"]);
16
+ case "missing_scope":
17
+ return new AxiError("Token is missing a required scope", "SCOPE_MISSING", ["Re-run `slack-axi auth setup`, add the missing scope, and re-install the app"]);
18
+ case "channel_not_found":
19
+ return new AxiError(context.channel ? `Channel '${context.channel}' not found` : "Channel not found", "CHANNEL_NOT_FOUND", [
20
+ "Run `slack-axi channels` to list your channels",
21
+ "Or `slack-axi search channels <query>` to find one by name",
22
+ ]);
23
+ case "not_in_channel":
24
+ return new AxiError(context.channel ? `You are not a member of '${context.channel}'` : "Not a member of that channel", "NOT_IN_CHANNEL", ["Join the channel in Slack, or use `slack-axi channels --all` to confirm the id"]);
25
+ case "thread_not_found":
26
+ case "message_not_found":
27
+ return new AxiError("Message or thread not found", "MESSAGE_NOT_FOUND", ["Check the ts; run `slack-axi read <channel>` to find the message"]);
28
+ case "ratelimited":
29
+ return new AxiError("Slack rate limit exceeded after retries", "RATE_LIMITED", ["Wait a moment and retry; narrow the time window or lower --limit"]);
30
+ default:
31
+ return new AxiError(`Slack API error: ${code}`, "SLACK_ERROR", ["Run `slack-axi doctor` to check auth and scopes"]);
32
+ }
33
+ }
34
+ /** Coerce an unknown thrown value (incl. @slack/web-api errors) into an AxiError. */
35
+ export function toAxiError(err, context = {}) {
36
+ if (err instanceof AxiError)
37
+ return err;
38
+ const code = extractSlackErrorCode(err);
39
+ if (code)
40
+ return slackErrorToAxi(code, context);
41
+ const message = err instanceof Error ? err.message : String(err);
42
+ return new AxiError(`Unexpected error: ${message}`, "INTERNAL_ERROR", [
43
+ "Run `slack-axi doctor` to check auth and runtime health",
44
+ ]);
45
+ }
46
+ function extractSlackErrorCode(err) {
47
+ if (!err || typeof err !== "object")
48
+ return undefined;
49
+ const data = err.data;
50
+ if (data && typeof data.error === "string")
51
+ return data.error;
52
+ return undefined;
53
+ }
54
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/slack/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,UAA+C,EAAE;IAC7F,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/D,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,cAAc,CAAC;QACpB,KAAK,eAAe,CAAC;QACrB,KAAK,eAAe,CAAC;QACrB,KAAK,kBAAkB;YACrB,OAAO,IAAI,QAAQ,CACjB,mCAAmC,EACnC,cAAc,EACd,CAAC,6BAA6B,QAAQ,4CAA4C,CAAC,CACpF,CAAC;QACJ,KAAK,YAAY;YACf,OAAO,IAAI,QAAQ,CACjB,6BAA6B,EAC7B,UAAU,EACV,CAAC,mDAAmD,CAAC,CACtD,CAAC;QACJ,KAAK,eAAe;YAClB,OAAO,IAAI,QAAQ,CACjB,mCAAmC,EACnC,eAAe,EACf,CAAC,8EAA8E,CAAC,CACjF,CAAC;QACJ,KAAK,mBAAmB;YACtB,OAAO,IAAI,QAAQ,CACjB,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,OAAO,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC,mBAAmB,EAChF,mBAAmB,EACnB;gBACE,gDAAgD;gBAChD,4DAA4D;aAC7D,CACF,CAAC;QACJ,KAAK,gBAAgB;YACnB,OAAO,IAAI,QAAQ,CACjB,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,4BAA4B,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,8BAA8B,EACjG,gBAAgB,EAChB,CAAC,gFAAgF,CAAC,CACnF,CAAC;QACJ,KAAK,kBAAkB,CAAC;QACxB,KAAK,mBAAmB;YACtB,OAAO,IAAI,QAAQ,CACjB,6BAA6B,EAC7B,mBAAmB,EACnB,CAAC,kEAAkE,CAAC,CACrE,CAAC;QACJ,KAAK,aAAa;YAChB,OAAO,IAAI,QAAQ,CACjB,yCAAyC,EACzC,cAAc,EACd,CAAC,kEAAkE,CAAC,CACrE,CAAC;QACJ;YACE,OAAO,IAAI,QAAQ,CACjB,oBAAoB,IAAI,EAAE,EAC1B,aAAa,EACb,CAAC,iDAAiD,CAAC,CACpD,CAAC;IACN,CAAC;AACH,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,GAAY,EAAE,UAA+C,EAAE;IACxF,IAAI,GAAG,YAAY,QAAQ;QAAE,OAAO,GAAG,CAAC;IACxC,MAAM,IAAI,GAAG,qBAAqB,CAAC,GAAG,CAAC,CAAC;IACxC,IAAI,IAAI;QAAE,OAAO,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,OAAO,IAAI,QAAQ,CAAC,qBAAqB,OAAO,EAAE,EAAE,gBAAgB,EAAE;QACpE,yDAAyD;KAC1D,CAAC,CAAC;AACL,CAAC;AAED,SAAS,qBAAqB,CAAC,GAAY;IACzC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACtD,MAAM,IAAI,GAAI,GAAsC,CAAC,IAAI,CAAC;IAC1D,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC;IAC9D,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { UserMeta } from "./cache.js";
2
+ /** A human label for a user id from a pre-loaded users map (display → real → handle → id). */
3
+ export declare function userName(id: string, users: Record<string, UserMeta>): string;
4
+ /**
5
+ * Resolve Slack markup to readable text: `<@U|name>` mentions, `<#C|name>` channels, `<url|label>`
6
+ * links, and basic entity decoding. Used by `read`, `thread`, and `search` output.
7
+ */
8
+ export declare function formatText(text: string, users: Record<string, UserMeta>): string;
@@ -0,0 +1,20 @@
1
+ /** A human label for a user id from a pre-loaded users map (display → real → handle → id). */
2
+ export function userName(id, users) {
3
+ const u = users[id];
4
+ return u ? u.display_name || u.real_name || u.name || id : id;
5
+ }
6
+ /**
7
+ * Resolve Slack markup to readable text: `<@U|name>` mentions, `<#C|name>` channels, `<url|label>`
8
+ * links, and basic entity decoding. Used by `read`, `thread`, and `search` output.
9
+ */
10
+ export function formatText(text, users) {
11
+ return text
12
+ .replace(/<@(U[A-Z0-9]+)(?:\|([^>]+))?>/g, (_m, id, name) => `@${name || userName(id, users)}`)
13
+ .replace(/<#C[A-Z0-9]+\|([^>]+)>/g, (_m, name) => `#${name}`)
14
+ .replace(/<(https?:[^|>]+)\|([^>]+)>/g, (_m, _url, label) => label)
15
+ .replace(/<(https?:[^>]+)>/g, (_m, url) => url)
16
+ .replace(/&amp;/g, "&")
17
+ .replace(/&lt;/g, "<")
18
+ .replace(/&gt;/g, ">");
19
+ }
20
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../../src/slack/format.ts"],"names":[],"mappings":"AAEA,8FAA8F;AAC9F,MAAM,UAAU,QAAQ,CAAC,EAAU,EAAE,KAA+B;IAClE,MAAM,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC;IACpB,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAChE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,KAA+B;IACtE,OAAO,IAAI;SACR,OAAO,CAAC,gCAAgC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,IAAI,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC;SAC9F,OAAO,CAAC,yBAAyB,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC;SAC5D,OAAO,CAAC,6BAA6B,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC;SAClE,OAAO,CAAC,mBAAmB,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC;SAC9C,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Session } from "../session.js";
2
+ /**
3
+ * The canonical permalink for a message, via chat.getPermalink (handles thread replies and the
4
+ * workspace subdomain automatically). slack-axi owns this — the agent never assembles a permalink.
5
+ * See specs/behaviors/permalinks.md.
6
+ */
7
+ export declare function getPermalink(session: Session, channel: string, ts: string): Promise<string>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The canonical permalink for a message, via chat.getPermalink (handles thread replies and the
3
+ * workspace subdomain automatically). slack-axi owns this — the agent never assembles a permalink.
4
+ * See specs/behaviors/permalinks.md.
5
+ */
6
+ export async function getPermalink(session, channel, ts) {
7
+ const res = await session.client.chat.getPermalink({ channel, message_ts: ts });
8
+ return typeof res.permalink === "string" ? res.permalink : "";
9
+ }
10
+ //# sourceMappingURL=permalink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permalink.js","sourceRoot":"","sources":["../../../src/slack/permalink.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAgB,EAAE,OAAe,EAAE,EAAU;IAC9E,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC;IAChF,OAAO,OAAO,GAAG,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;AAChE,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { Session } from "../session.js";
2
+ import { type ChannelMeta } from "./cache.js";
3
+ export declare function looksLikeId(arg: string): boolean;
4
+ /** Strip a leading `#` and surrounding whitespace from a channel argument. */
5
+ export declare function normalizeChannelArg(arg: string): string;
6
+ /**
7
+ * Resolve `#name`, bare `name`, or an id to a ChannelMeta. Cache-backed; on a name miss it refreshes
8
+ * (member channels, then the whole workspace) before giving up with fuzzy suggestions. See
9
+ * specs/behaviors/resolution-and-caching.md.
10
+ */
11
+ export declare function resolveChannel(session: Session, arg: string): Promise<ChannelMeta>;
12
+ /** Rank cached channels by name similarity to a query (substring/prefix), best first. */
13
+ export declare function fuzzyChannelMatches(channels: ChannelMeta[], query: string, limit: number): ChannelMeta[];
14
+ /** A human label for a user id (display name → real name → handle → id). Cache-backed. */
15
+ export declare function userLabel(session: Session, id: string): Promise<string>;
16
+ /** A human label for a channel: `#name` for channels, `@user` for a DM, participants for a group DM. */
17
+ export declare function channelLabel(session: Session, meta: ChannelMeta): Promise<string>;
18
+ /**
19
+ * Slack names group DMs `mpdm-<handle>--<handle>--<handle>-1`, so participants are derivable from the
20
+ * name with no API call. Falls back to the raw name if the pattern doesn't match.
21
+ */
22
+ export declare function parseMpimName(name: string): string;
@@ -0,0 +1,118 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { cachedChannels, cachedUser, ensureUsers, fetchChannelInfo, getCachedChannel, getChannels, refreshAllChannels, } from "./cache.js";
3
+ /** Slack channel ids start with C (public), G (private/mpim), or D (im), then uppercase alphanumerics. */
4
+ const ID_RE = /^[CGD][A-Z0-9]{6,}$/;
5
+ export function looksLikeId(arg) {
6
+ return ID_RE.test(arg);
7
+ }
8
+ /** Strip a leading `#` and surrounding whitespace from a channel argument. */
9
+ export function normalizeChannelArg(arg) {
10
+ return arg.trim().replace(/^#/, "");
11
+ }
12
+ /**
13
+ * Resolve `#name`, bare `name`, or an id to a ChannelMeta. Cache-backed; on a name miss it refreshes
14
+ * (member channels, then the whole workspace) before giving up with fuzzy suggestions. See
15
+ * specs/behaviors/resolution-and-caching.md.
16
+ */
17
+ export async function resolveChannel(session, arg) {
18
+ const raw = normalizeChannelArg(arg);
19
+ if (looksLikeId(raw)) {
20
+ const cached = getCachedChannel(session.teamId, raw);
21
+ if (cached)
22
+ return cached;
23
+ try {
24
+ return await fetchChannelInfo(session, raw);
25
+ }
26
+ catch {
27
+ throw new AxiError(`Channel id '${raw}' not found or inaccessible`, "CHANNEL_NOT_FOUND", [
28
+ "Run `slack-axi channels` to list your channels",
29
+ ]);
30
+ }
31
+ }
32
+ // Name lookup, refreshing the cache on a miss before failing.
33
+ const target = raw.toLowerCase();
34
+ let channels = await getChannels(session);
35
+ let hit = channels.find((c) => c.name.toLowerCase() === target);
36
+ if (!hit) {
37
+ channels = await refreshAllChannels(session);
38
+ channels = cachedChannels(session.teamId);
39
+ hit = channels.find((c) => c.name.toLowerCase() === target);
40
+ }
41
+ if (hit)
42
+ return hit;
43
+ throw new AxiError(`Channel '${raw}' not found`, "CHANNEL_NOT_FOUND", [
44
+ ...suggestionsFor(channels, target),
45
+ "Run `slack-axi channels` to list your channels, or `--all` to include unjoined ones",
46
+ ]);
47
+ }
48
+ function suggestionsFor(channels, target) {
49
+ const matches = suggestChannels(channels, target, 5);
50
+ return matches.length > 0 ? [`Did you mean: ${matches.map((c) => `#${c.name} (${c.id})`).join(", ")}`] : [];
51
+ }
52
+ function commonPrefixLen(a, b) {
53
+ const n = Math.min(a.length, b.length);
54
+ let i = 0;
55
+ while (i < n && a[i] === b[i])
56
+ i++;
57
+ return i;
58
+ }
59
+ /**
60
+ * Lenient "did you mean" ranking for a not-found name: substring either direction scores highest,
61
+ * otherwise shared-prefix length. Surfaces near-misses (`bid-rtd-xyz` → `bid-rtd-*`) that a strict
62
+ * substring filter would miss.
63
+ */
64
+ function suggestChannels(channels, query, limit) {
65
+ const q = query.toLowerCase();
66
+ const scored = channels
67
+ .filter((c) => c.name)
68
+ .map((c) => {
69
+ const name = c.name.toLowerCase();
70
+ const contains = name.includes(q) || q.includes(name);
71
+ const score = (contains ? 1000 : 0) + commonPrefixLen(name, q);
72
+ return { c, score };
73
+ })
74
+ .filter((s) => s.score >= 3)
75
+ .sort((a, b) => b.score - a.score || a.c.name.length - b.c.name.length || a.c.name.localeCompare(b.c.name));
76
+ return scored.slice(0, limit).map((s) => s.c);
77
+ }
78
+ /** Rank cached channels by name similarity to a query (substring/prefix), best first. */
79
+ export function fuzzyChannelMatches(channels, query, limit) {
80
+ const q = query.toLowerCase();
81
+ return channels
82
+ .filter((c) => c.name && c.name.toLowerCase().includes(q))
83
+ .sort((a, b) => {
84
+ const ap = a.name.toLowerCase().startsWith(q) ? 0 : 1;
85
+ const bp = b.name.toLowerCase().startsWith(q) ? 0 : 1;
86
+ return ap - bp || a.name.length - b.name.length || a.name.localeCompare(b.name);
87
+ })
88
+ .slice(0, limit);
89
+ }
90
+ /** A human label for a user id (display name → real name → handle → id). Cache-backed. */
91
+ export async function userLabel(session, id) {
92
+ await ensureUsers(session);
93
+ const u = cachedUser(session.teamId, id);
94
+ if (!u)
95
+ return id;
96
+ return u.display_name || u.real_name || u.name || id;
97
+ }
98
+ /** A human label for a channel: `#name` for channels, `@user` for a DM, participants for a group DM. */
99
+ export async function channelLabel(session, meta) {
100
+ if (meta.type === "im") {
101
+ const who = meta.user ? await userLabel(session, meta.user) : "unknown";
102
+ return `@${who}`;
103
+ }
104
+ if (meta.type === "mpim")
105
+ return parseMpimName(meta.name);
106
+ return `#${meta.name}`;
107
+ }
108
+ /**
109
+ * Slack names group DMs `mpdm-<handle>--<handle>--<handle>-1`, so participants are derivable from the
110
+ * name with no API call. Falls back to the raw name if the pattern doesn't match.
111
+ */
112
+ export function parseMpimName(name) {
113
+ const m = name.match(/^mpdm-(.+)-\d+$/);
114
+ if (!m)
115
+ return name || "(group dm)";
116
+ return m[1].split("--").join(", ");
117
+ }
118
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/slack/resolve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /** User-token scopes slack-axi needs. See specs/behaviors/auth-and-workspaces.md. */
2
+ export declare const READ_SCOPES: readonly ["channels:read", "groups:read", "im:read", "mpim:read", "channels:history", "groups:history", "im:history", "mpim:history", "search:read", "users:read", "reactions:read"];
3
+ /** Scopes needed for reactions + drafts (read + safe-draft capability set). */
4
+ export declare const WRITE_SCOPES: readonly ["reactions:write", "chat:write"];
5
+ export declare const REQUIRED_SCOPES: string[];
6
+ /** Granted scopes missing from the required set. */
7
+ export declare function missingScopes(granted: string[]): string[];
@@ -0,0 +1,23 @@
1
+ /** User-token scopes slack-axi needs. See specs/behaviors/auth-and-workspaces.md. */
2
+ export const READ_SCOPES = [
3
+ "channels:read",
4
+ "groups:read",
5
+ "im:read",
6
+ "mpim:read",
7
+ "channels:history",
8
+ "groups:history",
9
+ "im:history",
10
+ "mpim:history",
11
+ "search:read",
12
+ "users:read",
13
+ "reactions:read",
14
+ ];
15
+ /** Scopes needed for reactions + drafts (read + safe-draft capability set). */
16
+ export const WRITE_SCOPES = ["reactions:write", "chat:write"];
17
+ export const REQUIRED_SCOPES = [...READ_SCOPES, ...WRITE_SCOPES];
18
+ /** Granted scopes missing from the required set. */
19
+ export function missingScopes(granted) {
20
+ const have = new Set(granted);
21
+ return REQUIRED_SCOPES.filter((scope) => !have.has(scope));
22
+ }
23
+ //# sourceMappingURL=scopes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scopes.js","sourceRoot":"","sources":["../../../src/slack/scopes.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAErF,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,eAAe;IACf,aAAa;IACb,SAAS;IACT,WAAW;IACX,kBAAkB;IAClB,gBAAgB;IAChB,YAAY;IACZ,cAAc;IACd,aAAa;IACb,YAAY;IACZ,gBAAgB;CACR,CAAC;AAEX,+EAA+E;AAC/E,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,iBAAiB,EAAE,YAAY,CAAU,CAAC;AAEvE,MAAM,CAAC,MAAM,eAAe,GAAa,CAAC,GAAG,WAAW,EAAE,GAAG,YAAY,CAAC,CAAC;AAE3E,oDAAoD;AACpD,MAAM,UAAU,aAAa,CAAC,OAAiB;IAC7C,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IAC9B,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AAC7D,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { Session } from "../session.js";
2
+ /** A normalized Slack message (top-level or reply). */
3
+ export interface Msg {
4
+ ts: string;
5
+ user?: string;
6
+ botId?: string;
7
+ subtype?: string;
8
+ text: string;
9
+ replyCount: number;
10
+ threadTs?: string;
11
+ latestReply?: string;
12
+ }
13
+ export declare function isBot(msg: Msg): boolean;
14
+ /**
15
+ * All top-level messages in the half-open window `[oldestMs, endMs)`, paginated to completion, returned
16
+ * oldest→newest regardless of the API's native page order. The upper bound is exclusive (`latest` is
17
+ * one microsecond before `endMs`) so adjacent windows/batches tile with no gap and no overlap.
18
+ * See specs/behaviors/time-and-completeness.md.
19
+ */
20
+ export declare function fetchWindow(session: Session, channelId: string, oldestMs: number, endMs: number): Promise<Msg[]>;
21
+ /** A whole thread (parent + replies), paginated to completion, oldest→newest. */
22
+ export declare function fetchThread(session: Session, channelId: string, parentTs: string): Promise<Msg[]>;
23
+ /** Replies to a thread, excluding the parent. */
24
+ export declare function fetchReplies(session: Session, channelId: string, parentTs: string): Promise<Msg[]>;
@@ -0,0 +1,63 @@
1
+ import { epochMsToTs, tsExclusiveBefore } from "./time.js";
2
+ export function isBot(msg) {
3
+ return Boolean(msg.botId) || msg.subtype === "bot_message";
4
+ }
5
+ function toMsg(raw) {
6
+ return {
7
+ ts: String(raw.ts),
8
+ ...(typeof raw.user === "string" ? { user: raw.user } : {}),
9
+ ...(typeof raw.bot_id === "string" ? { botId: raw.bot_id } : {}),
10
+ ...(typeof raw.subtype === "string" ? { subtype: raw.subtype } : {}),
11
+ text: typeof raw.text === "string" ? raw.text : "",
12
+ replyCount: typeof raw.reply_count === "number" ? raw.reply_count : 0,
13
+ ...(typeof raw.thread_ts === "string" ? { threadTs: raw.thread_ts } : {}),
14
+ ...(typeof raw.latest_reply === "string" ? { latestReply: raw.latest_reply } : {}),
15
+ };
16
+ }
17
+ const byTsAsc = (a, b) => Number.parseFloat(a.ts) - Number.parseFloat(b.ts);
18
+ /**
19
+ * All top-level messages in the half-open window `[oldestMs, endMs)`, paginated to completion, returned
20
+ * oldest→newest regardless of the API's native page order. The upper bound is exclusive (`latest` is
21
+ * one microsecond before `endMs`) so adjacent windows/batches tile with no gap and no overlap.
22
+ * See specs/behaviors/time-and-completeness.md.
23
+ */
24
+ export async function fetchWindow(session, channelId, oldestMs, endMs) {
25
+ const messages = [];
26
+ let cursor;
27
+ do {
28
+ const res = await session.client.conversations.history({
29
+ channel: channelId,
30
+ oldest: epochMsToTs(oldestMs),
31
+ latest: tsExclusiveBefore(endMs),
32
+ inclusive: true,
33
+ limit: 200,
34
+ ...(cursor ? { cursor } : {}),
35
+ });
36
+ for (const m of res.messages ?? [])
37
+ messages.push(toMsg(m));
38
+ cursor = res.has_more ? res.response_metadata?.next_cursor || undefined : undefined;
39
+ } while (cursor);
40
+ return messages.sort(byTsAsc);
41
+ }
42
+ /** A whole thread (parent + replies), paginated to completion, oldest→newest. */
43
+ export async function fetchThread(session, channelId, parentTs) {
44
+ const messages = [];
45
+ let cursor;
46
+ do {
47
+ const res = await session.client.conversations.replies({
48
+ channel: channelId,
49
+ ts: parentTs,
50
+ limit: 200,
51
+ ...(cursor ? { cursor } : {}),
52
+ });
53
+ for (const m of res.messages ?? [])
54
+ messages.push(toMsg(m));
55
+ cursor = res.has_more ? res.response_metadata?.next_cursor || undefined : undefined;
56
+ } while (cursor);
57
+ return messages.sort(byTsAsc);
58
+ }
59
+ /** Replies to a thread, excluding the parent. */
60
+ export async function fetchReplies(session, channelId, parentTs) {
61
+ return (await fetchThread(session, channelId, parentTs)).filter((m) => m.ts !== parentTs);
62
+ }
63
+ //# sourceMappingURL=threads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"threads.js","sourceRoot":"","sources":["../../../src/slack/threads.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Human time in, Slack ts out — the year-correctness guard. The agent never produces an epoch; we
3
+ * parse spans/dates in a timezone (Eastern by default) and echo the resolved window back with explicit
4
+ * year. See specs/behaviors/time-and-completeness.md. All functions are pure (now is injected).
5
+ */
6
+ export declare const DEFAULT_TZ = "America/New_York";
7
+ export interface ResolvedWindow {
8
+ /** Inclusive lower bound, epoch ms. */
9
+ oldestMs: number;
10
+ /** **Exclusive** upper bound, epoch ms — the window is the half-open `[oldestMs, endMs)`. */
11
+ endMs: number;
12
+ tz: string;
13
+ }
14
+ /** Parse a relative span like `90m`, `24h`, `7d`, `2w` into milliseconds; undefined if it doesn't match. */
15
+ export declare function parseSpanMs(input: string): number | undefined;
16
+ /** Resolve `--since` / `--from` / `--to` into an explicit window. Defaults to the last 7 days. */
17
+ export declare function resolveWindow(opts: {
18
+ since?: string;
19
+ from?: string;
20
+ to?: string;
21
+ tz?: string;
22
+ }, now: number): ResolvedWindow;
23
+ /**
24
+ * Tile a window into half-open `[start, end)` batches of `everyMs`. Adjacent batches share a boundary
25
+ * instant (one batch's `endMs` == the next's `startMs`), so the set has no gap and no overlap; the
26
+ * final batch's `endMs` is exactly the window's end. See specs/commands/catchup.md.
27
+ */
28
+ export declare function batchWindows(oldestMs: number, endMs: number, everyMs: number): Array<{
29
+ startMs: number;
30
+ endMs: number;
31
+ }>;
32
+ /** `YYYY-MM-DD` in tz. */
33
+ export declare function formatDate(epochMs: number, tz: string): string;
34
+ /** `HH:MM` in tz (24h). */
35
+ export declare function formatTime(epochMs: number, tz: string): string;
36
+ /** `YYYY-MM-DD HH:MM` in tz — used in the resolved-range echo so the year is always explicit. */
37
+ export declare function formatDateTime(epochMs: number, tz: string): string;
38
+ /** The resolved-range header value, e.g. `2026-05-30 09:14 → 2026-06-06 09:14 (America/New_York)`.
39
+ * endMs is exclusive, so the displayed upper bound is `endMs − 1` (the last instant actually covered). */
40
+ export declare function formatRange(w: ResolvedWindow): string;
41
+ /** Slack ts (`1717589640.123456`) → epoch ms. */
42
+ export declare function tsToEpochMs(ts: string): number;
43
+ /** epoch ms → Slack ts string (seconds.microseconds), exact integer math. */
44
+ export declare function epochMsToTs(epochMs: number): string;
45
+ /** Slack ts one microsecond *before* an instant — the inclusive `latest` for a half-open `[…, endMs)`. */
46
+ export declare function tsExclusiveBefore(endMs: number): string;