@sentry/junior-plugin-api 0.74.1 → 0.76.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.
@@ -0,0 +1,133 @@
1
+ import { z } from "zod";
2
+ export declare const nonBlankStringSchema: z.ZodString;
3
+ /** Runtime platform names supported by plugin public contracts. */
4
+ export declare const platformSchema: z.ZodEnum<{
5
+ slack: "slack";
6
+ local: "local";
7
+ }>;
8
+ /** Runtime source visibility visible to plugins. */
9
+ export declare const sourceTypeSchema: z.ZodEnum<{
10
+ pub: "pub";
11
+ priv: "priv";
12
+ }>;
13
+ /** Runtime-owned Slack address for routing future work or side effects. */
14
+ export declare const slackDestinationSchema: z.ZodObject<{
15
+ platform: z.ZodLiteral<"slack">;
16
+ teamId: z.ZodString;
17
+ channelId: z.ZodString;
18
+ }, z.core.$strict>;
19
+ /** Runtime-owned local CLI conversation address. */
20
+ export declare const localDestinationSchema: z.ZodObject<{
21
+ platform: z.ZodLiteral<"local">;
22
+ conversationId: z.ZodString;
23
+ }, z.core.$strict>;
24
+ /** Runtime-owned provider-neutral address for routing future work or side effects. */
25
+ export declare const destinationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
26
+ platform: z.ZodLiteral<"slack">;
27
+ teamId: z.ZodString;
28
+ channelId: z.ZodString;
29
+ }, z.core.$strict>, z.ZodObject<{
30
+ platform: z.ZodLiteral<"local">;
31
+ conversationId: z.ZodString;
32
+ }, z.core.$strict>], "platform">;
33
+ /** Runtime-owned Slack coordinates for the inbound invocation. */
34
+ export declare const slackSourceSchema: z.ZodObject<{
35
+ platform: z.ZodLiteral<"slack">;
36
+ teamId: z.ZodString;
37
+ channelId: z.ZodString;
38
+ type: z.ZodEnum<{
39
+ pub: "pub";
40
+ priv: "priv";
41
+ }>;
42
+ messageTs: z.ZodOptional<z.ZodString>;
43
+ threadTs: z.ZodOptional<z.ZodString>;
44
+ }, z.core.$strict>;
45
+ /** Runtime-owned local CLI coordinates for the inbound invocation. */
46
+ export declare const localSourceSchema: z.ZodObject<{
47
+ platform: z.ZodLiteral<"local">;
48
+ type: z.ZodLiteral<"priv">;
49
+ conversationId: z.ZodString;
50
+ }, z.core.$strict>;
51
+ /** Runtime-owned provider-neutral coordinates for the inbound invocation. */
52
+ export declare const sourceSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
53
+ platform: z.ZodLiteral<"slack">;
54
+ teamId: z.ZodString;
55
+ channelId: z.ZodString;
56
+ type: z.ZodEnum<{
57
+ pub: "pub";
58
+ priv: "priv";
59
+ }>;
60
+ messageTs: z.ZodOptional<z.ZodString>;
61
+ threadTs: z.ZodOptional<z.ZodString>;
62
+ }, z.core.$strict>, z.ZodObject<{
63
+ platform: z.ZodLiteral<"local">;
64
+ type: z.ZodLiteral<"priv">;
65
+ conversationId: z.ZodString;
66
+ }, z.core.$strict>], "platform">;
67
+ /** Stable user credential subject shape accepted from plugins. */
68
+ export declare const pluginCredentialSubjectSchema: z.ZodObject<{
69
+ type: z.ZodLiteral<"user">;
70
+ userId: z.ZodString;
71
+ allowedWhen: z.ZodLiteral<"private-direct-conversation">;
72
+ }, z.core.$strict>;
73
+ export declare const slackRequesterSchema: z.ZodObject<{
74
+ platform: z.ZodLiteral<"slack">;
75
+ teamId: z.ZodString;
76
+ email: z.ZodOptional<z.ZodString>;
77
+ fullName: z.ZodOptional<z.ZodString>;
78
+ userId: z.ZodString;
79
+ userName: z.ZodOptional<z.ZodString>;
80
+ }, z.core.$strict>;
81
+ export declare const localRequesterSchema: z.ZodObject<{
82
+ platform: z.ZodLiteral<"local">;
83
+ email: z.ZodOptional<z.ZodString>;
84
+ fullName: z.ZodOptional<z.ZodString>;
85
+ userId: z.ZodString;
86
+ userName: z.ZodOptional<z.ZodString>;
87
+ }, z.core.$strict>;
88
+ /** Runtime-provided requester identity visible to plugin hooks. */
89
+ export declare const requesterSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
90
+ platform: z.ZodLiteral<"slack">;
91
+ teamId: z.ZodString;
92
+ email: z.ZodOptional<z.ZodString>;
93
+ fullName: z.ZodOptional<z.ZodString>;
94
+ userId: z.ZodString;
95
+ userName: z.ZodOptional<z.ZodString>;
96
+ }, z.core.$strict>, z.ZodObject<{
97
+ platform: z.ZodLiteral<"local">;
98
+ email: z.ZodOptional<z.ZodString>;
99
+ fullName: z.ZodOptional<z.ZodString>;
100
+ userId: z.ZodString;
101
+ userName: z.ZodOptional<z.ZodString>;
102
+ }, z.core.$strict>], "platform">;
103
+ /** Plugin dispatch request accepted by Junior core. */
104
+ export declare const dispatchOptionsSchema: z.ZodObject<{
105
+ idempotencyKey: z.ZodPipe<z.ZodString, z.ZodString>;
106
+ credentialSubject: z.ZodOptional<z.ZodObject<{
107
+ type: z.ZodLiteral<"user">;
108
+ userId: z.ZodString;
109
+ allowedWhen: z.ZodLiteral<"private-direct-conversation">;
110
+ }, z.core.$strict>>;
111
+ destination: z.ZodObject<{
112
+ platform: z.ZodLiteral<"slack">;
113
+ teamId: z.ZodString;
114
+ channelId: z.ZodString;
115
+ }, z.core.$strict>;
116
+ input: z.ZodPipe<z.ZodString, z.ZodString>;
117
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
118
+ source: z.ZodDiscriminatedUnion<[z.ZodObject<{
119
+ platform: z.ZodLiteral<"slack">;
120
+ teamId: z.ZodString;
121
+ channelId: z.ZodString;
122
+ type: z.ZodEnum<{
123
+ pub: "pub";
124
+ priv: "priv";
125
+ }>;
126
+ messageTs: z.ZodOptional<z.ZodString>;
127
+ threadTs: z.ZodOptional<z.ZodString>;
128
+ }, z.core.$strict>, z.ZodObject<{
129
+ platform: z.ZodLiteral<"local">;
130
+ type: z.ZodLiteral<"priv">;
131
+ conversationId: z.ZodString;
132
+ }, z.core.$strict>], "platform">;
133
+ }, z.core.$strict>;
@@ -0,0 +1,10 @@
1
+ export interface PluginState {
2
+ delete(key: string): Promise<void>;
3
+ get<T = unknown>(key: string): Promise<T | undefined>;
4
+ set(key: string, value: unknown, ttlMs?: number): Promise<void>;
5
+ setIfNotExists(key: string, value: unknown, ttlMs?: number): Promise<boolean>;
6
+ withLock<T>(key: string, ttlMs: number, callback: () => Promise<T>): Promise<T>;
7
+ }
8
+ export interface PluginReadState {
9
+ get<T = unknown>(key: string): Promise<T | undefined>;
10
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Public plugin background-task contracts.
3
+ *
4
+ * Plugins register small task handlers, while Junior core owns durable
5
+ * scheduling, queue delivery, retries, and the bounded run projection.
6
+ */
7
+ import { z } from "zod";
8
+ import type { PluginContext, PluginEmbedder, PluginModel } from "./context";
9
+ import type { PluginState } from "./state";
10
+ /** One normalized transcript entry from the completed run exposed to plugin tasks. */
11
+ export declare const pluginRunTranscriptEntrySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
12
+ type: z.ZodLiteral<"message">;
13
+ role: z.ZodEnum<{
14
+ user: "user";
15
+ assistant: "assistant";
16
+ }>;
17
+ text: z.ZodString;
18
+ }, z.core.$strict>, z.ZodObject<{
19
+ type: z.ZodLiteral<"toolResult">;
20
+ toolName: z.ZodString;
21
+ isError: z.ZodBoolean;
22
+ text: z.ZodOptional<z.ZodString>;
23
+ }, z.core.$strict>], "type">;
24
+ /** Runtime-owned completed-run projection exposed to plugin tasks. */
25
+ export declare const pluginRunContextSchema: z.ZodObject<{
26
+ completedAtMs: z.ZodNumber;
27
+ conversationId: z.ZodString;
28
+ destination: z.ZodDiscriminatedUnion<[z.ZodObject<{
29
+ platform: z.ZodLiteral<"slack">;
30
+ teamId: z.ZodString;
31
+ channelId: z.ZodString;
32
+ }, z.core.$strict>, z.ZodObject<{
33
+ platform: z.ZodLiteral<"local">;
34
+ conversationId: z.ZodString;
35
+ }, z.core.$strict>], "platform">;
36
+ requester: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
37
+ platform: z.ZodLiteral<"slack">;
38
+ teamId: z.ZodString;
39
+ email: z.ZodOptional<z.ZodString>;
40
+ fullName: z.ZodOptional<z.ZodString>;
41
+ userId: z.ZodString;
42
+ userName: z.ZodOptional<z.ZodString>;
43
+ }, z.core.$strict>, z.ZodObject<{
44
+ platform: z.ZodLiteral<"local">;
45
+ email: z.ZodOptional<z.ZodString>;
46
+ fullName: z.ZodOptional<z.ZodString>;
47
+ userId: z.ZodString;
48
+ userName: z.ZodOptional<z.ZodString>;
49
+ }, z.core.$strict>], "platform">>;
50
+ runId: z.ZodString;
51
+ source: z.ZodDiscriminatedUnion<[z.ZodObject<{
52
+ platform: z.ZodLiteral<"slack">;
53
+ teamId: z.ZodString;
54
+ channelId: z.ZodString;
55
+ type: z.ZodEnum<{
56
+ pub: "pub";
57
+ priv: "priv";
58
+ }>;
59
+ messageTs: z.ZodOptional<z.ZodString>;
60
+ threadTs: z.ZodOptional<z.ZodString>;
61
+ }, z.core.$strict>, z.ZodObject<{
62
+ platform: z.ZodLiteral<"local">;
63
+ type: z.ZodLiteral<"priv">;
64
+ conversationId: z.ZodString;
65
+ }, z.core.$strict>], "platform">;
66
+ transcript: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
67
+ type: z.ZodLiteral<"message">;
68
+ role: z.ZodEnum<{
69
+ user: "user";
70
+ assistant: "assistant";
71
+ }>;
72
+ text: z.ZodString;
73
+ }, z.core.$strict>, z.ZodObject<{
74
+ type: z.ZodLiteral<"toolResult">;
75
+ toolName: z.ZodString;
76
+ isError: z.ZodBoolean;
77
+ text: z.ZodOptional<z.ZodString>;
78
+ }, z.core.$strict>], "type">>;
79
+ }, z.core.$strict>;
80
+ export type PluginRunTranscriptEntry = z.output<typeof pluginRunTranscriptEntrySchema>;
81
+ export type PluginRunContext = z.output<typeof pluginRunContextSchema>;
82
+ /** Runtime context passed to a plugin-owned background task. */
83
+ export interface PluginTaskContext extends PluginContext {
84
+ embedder: PluginEmbedder;
85
+ id: string;
86
+ model: PluginModel;
87
+ name: string;
88
+ run: {
89
+ load(): Promise<PluginRunContext>;
90
+ };
91
+ state: PluginState;
92
+ }
93
+ /** Plugin task handler registered by name in a plugin manifest module. */
94
+ export interface PluginTaskDefinition {
95
+ run(ctx: PluginTaskContext): Promise<void> | void;
96
+ }
97
+ /** Task handlers keyed by the plugin-owned task name. */
98
+ export type PluginTasks = Record<string, PluginTaskDefinition>;
@@ -0,0 +1,116 @@
1
+ import type { PluginContext, LocalInvocationContext, PluginEmbedder, PluginModel, Requester, SlackInvocationContext } from "./context";
2
+ import type { PluginCredentialSubject } from "./credentials";
3
+ import type { PluginState } from "./state";
4
+ export interface PluginEnv {
5
+ get(key: string): string | undefined;
6
+ set(key: string, value: string): void;
7
+ }
8
+ export interface PluginDecision {
9
+ deny(message: string): void;
10
+ replaceInput(input: Record<string, unknown>): void;
11
+ }
12
+ /** Thrown when a plugin tool rejects invalid model or user input. */
13
+ export declare class PluginToolInputError extends Error {
14
+ constructor(message: string, options?: {
15
+ cause?: unknown;
16
+ });
17
+ }
18
+ export interface PluginSandbox {
19
+ juniorRoot: string;
20
+ root: string;
21
+ readFile(path: string): Promise<Uint8Array | null>;
22
+ run(input: {
23
+ args?: string[];
24
+ cmd: string;
25
+ cwd?: string;
26
+ env?: Record<string, string>;
27
+ sudo?: boolean;
28
+ }): Promise<{
29
+ exitCode: number;
30
+ stderr: string;
31
+ stdout: string;
32
+ }>;
33
+ writeFile(input: {
34
+ content: string | Uint8Array;
35
+ mode?: number;
36
+ path: string;
37
+ }): Promise<void>;
38
+ }
39
+ export interface SandboxPrepareHookContext extends PluginContext {
40
+ requester?: Requester;
41
+ sandbox: PluginSandbox;
42
+ }
43
+ export interface BeforeToolExecuteHookContext extends PluginContext {
44
+ decision: PluginDecision;
45
+ env: PluginEnv;
46
+ requester?: Requester;
47
+ tool: {
48
+ input: Record<string, unknown>;
49
+ name: string;
50
+ };
51
+ }
52
+ export interface PluginToolExecuteOptions {
53
+ /**
54
+ * @deprecated Internal compatibility escape hatch for legacy tool bridges.
55
+ * Plugin tools should use typed input fields and runtime hook context instead.
56
+ */
57
+ experimental_context?: unknown;
58
+ /** Stable runtime tool-call id; durable create tools should derive idempotency keys from it. */
59
+ toolCallId?: string;
60
+ }
61
+ export type PluginToolExecute<TInput = unknown> = {
62
+ bivarianceHack(input: TInput, options: PluginToolExecuteOptions): Promise<unknown> | unknown;
63
+ }["bivarianceHack"];
64
+ export interface PluginToolDefinition<TInput = unknown> {
65
+ annotations?: unknown;
66
+ description: string;
67
+ executionMode?: unknown;
68
+ inputSchema: unknown;
69
+ prepareArguments?: (args: unknown) => unknown;
70
+ /**
71
+ * @deprecated Put tool-selection and usage guidance directly in `description`
72
+ * and parameter descriptions. Retained for compatibility; may be removed in a
73
+ * future major version.
74
+ */
75
+ promptGuidelines?: string[];
76
+ /**
77
+ * @deprecated Put tool-selection and usage guidance directly in `description`
78
+ * and parameter descriptions. Retained for compatibility; may be removed in a
79
+ * future major version.
80
+ */
81
+ promptSnippet?: string;
82
+ execute?: PluginToolExecute<TInput>;
83
+ }
84
+ export interface SlackToolRegistrationHookContext {
85
+ /**
86
+ * Capabilities of the source Slack conversation exposed to this plugin.
87
+ * Recomputed from `source.channelId`, not from `destination`.
88
+ */
89
+ channelCapabilities: {
90
+ canAddReactions: boolean;
91
+ canCreateCanvas: boolean;
92
+ canPostToChannel: boolean;
93
+ };
94
+ credentialSubject?: PluginCredentialSubject;
95
+ }
96
+ interface BaseToolRegistrationHookContext extends PluginContext {
97
+ /**
98
+ * Opaque Junior conversation/session identity for this turn.
99
+ * Interactive Slack turns use `slack:{channelId}:{threadTs}`.
100
+ * Scheduled/API turns use an internal id such as `agent-dispatch:{id}`.
101
+ * Do not parse as Slack unless the value starts with `slack:`.
102
+ */
103
+ conversationId?: string;
104
+ embedder: PluginEmbedder;
105
+ model: PluginModel;
106
+ state: PluginState;
107
+ userText?: string;
108
+ }
109
+ interface SlackToolRegistrationContext extends BaseToolRegistrationHookContext, SlackInvocationContext {
110
+ slack: SlackToolRegistrationHookContext;
111
+ }
112
+ interface LocalToolRegistrationContext extends BaseToolRegistrationHookContext, LocalInvocationContext {
113
+ slack?: never;
114
+ }
115
+ export type ToolRegistrationHookContext = LocalToolRegistrationContext | SlackToolRegistrationContext;
116
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-plugin-api",
3
- "version": "0.74.1",
3
+ "version": "0.76.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,6 +22,7 @@
22
22
  "src"
23
23
  ],
24
24
  "dependencies": {
25
+ "commander": "^14.0.3",
25
26
  "zod": "^4.4.3"
26
27
  },
27
28
  "devDependencies": {
package/src/cli.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Public Commander-based CLI contract for plugin-owned admin commands.
3
+ * Junior owns the root command, plugin namespaces, context injection, and exit
4
+ * normalization; plugins only configure subcommands under their namespace.
5
+ */
6
+ import type { Command } from "commander";
7
+ import type { PluginContext } from "./context";
8
+
9
+ export interface PluginCliIo {
10
+ writeError(text: string): Promise<void> | void;
11
+ writeOutput(text: string): Promise<void> | void;
12
+ }
13
+
14
+ export interface PluginCliActionCommand {
15
+ name: string;
16
+ summary: string;
17
+ }
18
+
19
+ /** Host/admin context exposed to plugin-owned CLI command actions. */
20
+ export interface PluginCliActionContext extends Pick<
21
+ PluginContext,
22
+ "db" | "log" | "plugin"
23
+ > {
24
+ command: PluginCliActionCommand;
25
+ io: PluginCliIo;
26
+ }
27
+
28
+ /** Plugin action callback wrapped by the Junior host for context and exit codes. */
29
+ export type PluginCliActionHandler<Args extends unknown[] = unknown[]> = (
30
+ ctx: PluginCliActionContext,
31
+ ...args: Args
32
+ ) => Promise<number | void> | number | void;
33
+
34
+ export interface PluginCliHost {
35
+ /** Wrap a Commander action so Junior can inject context and normalize exits. */
36
+ action<Args extends unknown[]>(
37
+ handler: PluginCliActionHandler<Args>,
38
+ ): (...args: Args) => Promise<void>;
39
+ }
40
+
41
+ /** Plugin-owned top-level CLI command registration. */
42
+ export interface PluginCliCommandDefinition {
43
+ /** Configure subcommands under the host-created top-level namespace. */
44
+ configure(command: Command, junior: PluginCliHost): void;
45
+ /** Unique host-level command namespace owned by this plugin. */
46
+ name: string;
47
+ /** One-line summary used in generated command help. */
48
+ summary: string;
49
+ }
50
+
51
+ /** Plugin-owned CLI command catalog. */
52
+ export interface PluginCliDefinition {
53
+ commands: PluginCliCommandDefinition[];
54
+ }
package/src/context.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { z } from "zod";
2
+ import type { ZodTypeAny } from "zod";
3
+ import {
4
+ destinationSchema,
5
+ localRequesterSchema,
6
+ platformSchema,
7
+ requesterSchema,
8
+ slackRequesterSchema,
9
+ sourceSchema,
10
+ } from "./schemas";
11
+
12
+ /** Runtime platform name without source or destination coordinates. */
13
+ export type Platform = z.output<typeof platformSchema>;
14
+ export type Requester = z.output<typeof requesterSchema>;
15
+ export type SlackRequester = z.output<typeof slackRequesterSchema>;
16
+ export type LocalRequester = z.output<typeof localRequesterSchema>;
17
+ export type Source = z.output<typeof sourceSchema>;
18
+ export type SlackSource = Extract<Source, { platform: "slack" }>;
19
+ export type LocalSource = Extract<Source, { platform: "local" }>;
20
+ export type SourceType = Source["type"];
21
+
22
+ export type Destination = z.output<typeof destinationSchema>;
23
+
24
+ export type SlackDestination = Extract<Destination, { platform: "slack" }>;
25
+
26
+ export type LocalDestination = Extract<Destination, { platform: "local" }>;
27
+
28
+ export interface PluginMetadata {
29
+ name: string;
30
+ }
31
+
32
+ export interface PluginLogger {
33
+ error(message: string, metadata?: Record<string, unknown>): void;
34
+ info(message: string, metadata?: Record<string, unknown>): void;
35
+ warn(message: string, metadata?: Record<string, unknown>): void;
36
+ }
37
+
38
+ export interface PluginModel {
39
+ /** Run a host-owned structured model call without exposing provider credentials. */
40
+ completeObject<TSchema extends ZodTypeAny>(input: {
41
+ maxTokens?: number;
42
+ prompt: string;
43
+ schema: TSchema;
44
+ system?: string;
45
+ }): Promise<{ object: z.infer<TSchema> }>;
46
+ }
47
+
48
+ export interface PluginEmbedder {
49
+ /** Embed plugin-owned text for derived retrieval without exposing provider credentials. */
50
+ embedTexts(input: { texts: string[] }): Promise<{
51
+ dimensions: number;
52
+ model: string;
53
+ provider: string;
54
+ vectors: number[][];
55
+ }>;
56
+ }
57
+
58
+ export interface PluginContext {
59
+ /** Shared Drizzle database connection for plugin runtime code. */
60
+ db: unknown;
61
+ log: PluginLogger;
62
+ plugin: PluginMetadata;
63
+ }
64
+
65
+ interface BaseInvocationContext {
66
+ /**
67
+ * Opaque Junior conversation/session identity for this invocation.
68
+ * Interactive Slack turns use `slack:{channelId}:{threadTs}`.
69
+ */
70
+ conversationId?: string;
71
+ }
72
+
73
+ export interface SlackInvocationContext extends BaseInvocationContext {
74
+ /** Runtime-owned default outbound destination for this invocation. */
75
+ destination: SlackDestination;
76
+ requester?: SlackRequester;
77
+ /** Runtime-owned source where the invocation came from. */
78
+ source: SlackSource;
79
+ }
80
+
81
+ export interface LocalInvocationContext extends BaseInvocationContext {
82
+ /** Runtime-owned default outbound destination for this invocation. */
83
+ destination: LocalDestination;
84
+ requester?: LocalRequester;
85
+ /** Runtime-owned source where the invocation came from. */
86
+ source: LocalSource;
87
+ }
88
+
89
+ export type InvocationContext = LocalInvocationContext | SlackInvocationContext;
90
+
91
+ /** Build a normalized Slack source from runtime-owned Slack coordinates. */
92
+ export function createSlackSource(input: {
93
+ channelId: string;
94
+ messageTs?: string;
95
+ teamId: string;
96
+ threadTs?: string;
97
+ }): SlackSource {
98
+ return {
99
+ platform: "slack",
100
+ type: slackSourceType(input.channelId),
101
+ teamId: input.teamId,
102
+ channelId: input.channelId,
103
+ ...(input.messageTs ? { messageTs: input.messageTs } : {}),
104
+ ...(input.threadTs ? { threadTs: input.threadTs } : {}),
105
+ };
106
+ }
107
+
108
+ /** Classify Slack's documented C/D/G channel id prefixes into source visibility. */
109
+ function slackSourceType(channelId: string): SourceType {
110
+ if (channelId.startsWith("C")) return "pub";
111
+ if (channelId.startsWith("D") || channelId.startsWith("G")) return "priv";
112
+ throw new Error(`Unsupported Slack channel ID prefix: ${channelId}`);
113
+ }
114
+
115
+ /** Build a normalized local source from a local conversation id. */
116
+ export function createLocalSource(conversationId: string): LocalSource {
117
+ return {
118
+ platform: "local",
119
+ type: "priv",
120
+ conversationId,
121
+ };
122
+ }
123
+
124
+ /** Return whether a source is private to a person or restricted group. */
125
+ export function isPrivateSource(source: Source): boolean {
126
+ return source.type === "priv";
127
+ }
128
+
129
+ /** Return the stable source identity used for idempotency and attribution. */
130
+ export function getSourceKey(source: Source): string | undefined {
131
+ if (source.platform === "local") {
132
+ return source.conversationId;
133
+ }
134
+ const messageKey = source.threadTs ?? source.messageTs;
135
+ if (!messageKey) {
136
+ return undefined;
137
+ }
138
+ return `slack:${source.teamId}:${source.channelId}:${messageKey}`;
139
+ }
140
+
141
+ /** Narrow a runtime destination to the Slack-specific address shape. */
142
+ export function isSlackDestination(
143
+ destination: Destination | undefined,
144
+ ): destination is SlackDestination {
145
+ return destination?.platform === "slack";
146
+ }