runline 0.8.0 → 0.8.1

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.
@@ -5,6 +5,7 @@ import { addConnection } from "../config/loader.js";
5
5
  import { OAUTH_CALLBACK_PORT, runOAuth } from "../core/oauth.js";
6
6
  import { loadAllPlugins } from "../plugin/loader.js";
7
7
  import { registry } from "../plugin/registry.js";
8
+ import { connectionFields } from "../plugin/schema.js";
8
9
  import { printError, printJson, printSuccess } from "../utils/output.js";
9
10
  export async function auth(plugin, options) {
10
11
  await loadAllPlugins();
@@ -20,8 +21,9 @@ export async function auth(plugin, options) {
20
21
  // Client credentials: CLI flag > env > interactive prompt.
21
22
  // Env var names follow the plugin's own convention when declared
22
23
  // on its connection schema; fall back to generic names otherwise.
23
- const envIdVar = def.connectionConfigSchema?.clientId?.env;
24
- const envSecretVar = def.connectionConfigSchema?.clientSecret?.env;
24
+ const connectionSchema = connectionFields(def.connectionConfigSchema);
25
+ const envIdVar = connectionSchema.clientId?.env;
26
+ const envSecretVar = connectionSchema.clientSecret?.env;
25
27
  const resolvedClientId = options.clientId ?? (envIdVar ? process.env[envIdVar] : undefined);
26
28
  const resolvedClientSecret = options.clientSecret ??
27
29
  (envSecretVar ? process.env[envSecretVar] : undefined);
@@ -1,4 +1,4 @@
1
- import type { ConnectionConfig } from "../plugin/types.js";
1
+ import type { ConnectionConfig, ConnectionSchema } from "../plugin/types.js";
2
2
  import { type RunlineConfig } from "./types.js";
3
3
  export declare function findConfigDir(): string | null;
4
4
  export declare function loadConfig(): RunlineConfig;
@@ -17,6 +17,4 @@ export declare function removeConnection(name: string): boolean;
17
17
  */
18
18
  export declare function updateConnectionConfig(name: string, patch: Record<string, unknown>): Promise<void>;
19
19
  export declare function getConnection(plugin: string, name?: string): ConnectionConfig | undefined;
20
- export declare function applyEnvOverrides(conn: ConnectionConfig, schema?: Record<string, {
21
- env?: string;
22
- }>): ConnectionConfig;
20
+ export declare function applyEnvOverrides(conn: ConnectionConfig, schema?: ConnectionSchema): ConnectionConfig;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import lockfile from "proper-lockfile";
4
+ import { connectionFields } from "../plugin/schema.js";
4
5
  import { DEFAULT_CONFIG } from "./types.js";
5
6
  const CONFIG_DIR_NAME = ".runline";
6
7
  const CONFIG_FILE = "config.json";
@@ -117,7 +118,7 @@ export function applyEnvOverrides(conn, schema) {
117
118
  if (!schema)
118
119
  return conn;
119
120
  const config = { ...conn.config };
120
- for (const [key, field] of Object.entries(schema)) {
121
+ for (const [key, field] of Object.entries(connectionFields(schema))) {
121
122
  if (field.env && !config[key]) {
122
123
  const envVal = process.env[field.env];
123
124
  if (envVal)
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { getQuickJS, shouldInterruptAfterDeadline, } from "quickjs-emscripten";
3
3
  import { applyEnvOverrides, updateConnectionConfig } from "../config/loader.js";
4
+ import { formatValidationError, helpInputs, isTypedInputSchema, validateTypedInput, } from "../plugin/schema.js";
4
5
  export class ExecutionEngine {
5
6
  registry;
6
7
  config;
@@ -146,6 +147,12 @@ export class ExecutionEngine {
146
147
  await updateConnectionConfig(connection.name, patch);
147
148
  },
148
149
  };
150
+ if (isTypedInputSchema(action.inputSchema)) {
151
+ const validation = validateTypedInput(action.inputSchema, args);
152
+ if (!validation.ok) {
153
+ throw new Error(formatValidationError(path, validation));
154
+ }
155
+ }
149
156
  return action.execute(args, ctx);
150
157
  }
151
158
  resolveConnection(plugin) {
@@ -230,14 +237,7 @@ function buildHelpData(plugins) {
230
237
  data[p.name] = p.actions.map((a) => ({
231
238
  action: a.name,
232
239
  description: a.description,
233
- inputs: Object.fromEntries(Object.entries(a.inputSchema ?? {}).map(([k, v]) => [
234
- k,
235
- {
236
- type: v.type,
237
- required: !!v.required,
238
- description: v.description,
239
- },
240
- ])),
240
+ inputs: helpInputs(a.inputSchema),
241
241
  }));
242
242
  }
243
243
  return data;
@@ -291,7 +291,7 @@ const __index = (() => {
291
291
 
292
292
  const __formatSignature = (plugin, entry) => {
293
293
  const fields = Object.entries(entry.inputs || {})
294
- .map(([k, v]) => k + (v.required ? '' : '?') + ': ' + v.type)
294
+ .map(([k, v]) => k + (v.required ? '' : '?') + ': ' + (v.displayType || v.type))
295
295
  .join(', ');
296
296
  return plugin + '.' + entry.action + (fields ? '({ ' + fields + ' })' : '()');
297
297
  };
@@ -370,10 +370,15 @@ const __actionsApi = {
370
370
  for (const k of Object.keys(provided)) {
371
371
  if (!(k in inputs)) unknown.push(k);
372
372
  else {
373
- const expected = inputs[k].type;
374
- const actual = Array.isArray(provided[k]) ? 'array' : typeof provided[k];
375
- if (expected !== actual && !(provided[k] === null || provided[k] === undefined)) {
376
- typeErrors.push({ field: k, expected, actual });
373
+ const spec = inputs[k];
374
+ const expected = spec.type;
375
+ const actual = Array.isArray(provided[k]) ? 'array' : provided[k] === null ? 'null' : typeof provided[k];
376
+ if (provided[k] !== null && provided[k] !== undefined && expected !== actual) {
377
+ typeErrors.push({ field: k, expected: spec.displayType || expected, actual });
378
+ } else if (spec.enum && !spec.enum.includes(provided[k])) {
379
+ typeErrors.push({ field: k, expected: spec.enum.map(String).join(' | '), actual: __fmt(provided[k]) });
380
+ } else if ('const' in spec && provided[k] !== spec.const) {
381
+ typeErrors.push({ field: k, expected: __fmt(spec.const), actual: __fmt(provided[k]) });
377
382
  }
378
383
  }
379
384
  }
package/dist/index.d.ts CHANGED
@@ -5,13 +5,13 @@ export type { EngineOptions, ExecuteResult } from "./core/engine.js";
5
5
  export { ExecutionEngine } from "./core/engine.js";
6
6
  export type { BuildAuthUrlOptions, ExchangeCodeOptions, OAuthTokens, PKCEPair, RunOAuthOptions, } from "./core/oauth.js";
7
7
  export { buildAuthUrl, exchangeAuthCode, generatePKCE, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_URI, runOAuth, } from "./core/oauth.js";
8
- export type { ActionDefinition, PluginFunction, RunlinePluginAPI, SchemaField, } from "./plugin/api.js";
8
+ export type { ActionDefinition, PluginFunction, RunlinePluginAPI, } from "./plugin/api.js";
9
9
  export { createPluginAPI, isPluginFunction, resolvePluginExport, } from "./plugin/api.js";
10
10
  export type { InstalledPlugin, PluginSource } from "./plugin/installer.js";
11
11
  export { installPlugin, listInstalled, parsePluginSource, removePlugin, } from "./plugin/installer.js";
12
12
  export { discoverPlugins, loadAllPlugins, loadPluginFromPath, loadPluginsFromConfig, } from "./plugin/loader.js";
13
13
  export { PluginRegistry, registry } from "./plugin/registry.js";
14
- export type { ActionContext, ActionDef, ConnectionConfig, InputField, InputSchema, OAuthConfig, PluginDef, } from "./plugin/types.js";
14
+ export type { ActionContext, ActionDef, ConnectionConfig, ConnectionSchema, ConnectionSchemaField, InputField, InputSchema, LegacyConnectionSchema, OAuthConfig, PluginDef, } from "./plugin/types.js";
15
15
  export type { RunlineOptions } from "./sdk.js";
16
16
  export { Runline } from "./sdk.js";
17
17
  export type { ExecOptions, ExecResult, OutputParser } from "./utils/cli.js";
@@ -1,11 +1,4 @@
1
- import type { ActionDef, InputSchema, OAuthConfig, PluginDef } from "./types.js";
2
- export interface SchemaField {
3
- type: "string" | "number" | "boolean";
4
- required?: boolean;
5
- description?: string;
6
- default?: unknown;
7
- env?: string;
8
- }
1
+ import type { ActionDef, ConnectionSchema, InputSchema, OAuthConfig, PluginDef } from "./types.js";
9
2
  export interface ActionDefinition {
10
3
  description?: string;
11
4
  inputSchema?: InputSchema;
@@ -13,7 +6,7 @@ export interface ActionDefinition {
13
6
  }
14
7
  export interface RunlinePluginAPI {
15
8
  registerAction(name: string, def: ActionDefinition): void;
16
- setConnectionSchema(schema: Record<string, SchemaField>): void;
9
+ setConnectionSchema(schema: ConnectionSchema): void;
17
10
  setName(name: string): void;
18
11
  setVersion(version: string): void;
19
12
  /**
@@ -19,10 +19,7 @@ export function createPluginAPI(pluginId) {
19
19
  actions.push({ name: actionName, ...def });
20
20
  },
21
21
  setConnectionSchema(schema) {
22
- connectionConfigSchema = {};
23
- for (const [key, field] of Object.entries(schema)) {
24
- connectionConfigSchema[key] = { ...field };
25
- }
22
+ connectionConfigSchema = { ...schema };
26
23
  },
27
24
  setOAuth(cfg) {
28
25
  oauth = { ...cfg };
@@ -0,0 +1,19 @@
1
+ import type { ConnectionSchema, HelpInput, InputSchema, LegacyConnectionSchema, LegacyInputSchema, TypedInputSchema } from "./types.js";
2
+ export interface ValidationResult {
3
+ ok: boolean;
4
+ missing: string[];
5
+ unknown: string[];
6
+ typeErrors: Array<{
7
+ field: string;
8
+ expected: string;
9
+ actual: string;
10
+ }>;
11
+ errors: string[];
12
+ }
13
+ export declare function isTypedInputSchema(schema: InputSchema | undefined): schema is TypedInputSchema;
14
+ export declare function legacyFields(schema: InputSchema | undefined): LegacyInputSchema;
15
+ export declare function connectionFields(schema: ConnectionSchema | undefined): LegacyConnectionSchema;
16
+ export declare function helpInputs(schema: InputSchema | undefined): Record<string, HelpInput>;
17
+ export declare function validateLegacyInput(schema: LegacyInputSchema, input: unknown): ValidationResult;
18
+ export declare function validateTypedInput(schema: TypedInputSchema, input: unknown): ValidationResult;
19
+ export declare function formatValidationError(path: string, result: ValidationResult): string;
@@ -0,0 +1,168 @@
1
+ import { Check, Errors } from "typebox/value";
2
+ export function isTypedInputSchema(schema) {
3
+ if (!schema || typeof schema !== "object")
4
+ return false;
5
+ const candidate = schema;
6
+ return typeof candidate.type === "string" || Array.isArray(candidate.anyOf);
7
+ }
8
+ export function legacyFields(schema) {
9
+ if (!schema || isTypedInputSchema(schema))
10
+ return {};
11
+ return schema;
12
+ }
13
+ export function connectionFields(schema) {
14
+ if (!schema)
15
+ return {};
16
+ if (!isTypedInputSchema(schema))
17
+ return schema;
18
+ const metadata = schema;
19
+ if (metadata.type !== "object")
20
+ return {};
21
+ const required = new Set(metadata.required ?? []);
22
+ return Object.fromEntries(Object.entries(metadata.properties ?? {}).map(([key, field]) => [
23
+ key,
24
+ {
25
+ type: baseType(field),
26
+ required: required.has(key),
27
+ description: field.description,
28
+ default: field.default,
29
+ env: field.env,
30
+ },
31
+ ]));
32
+ }
33
+ export function helpInputs(schema) {
34
+ if (!schema)
35
+ return {};
36
+ if (!isTypedInputSchema(schema)) {
37
+ const legacy = schema;
38
+ return Object.fromEntries(Object.entries(legacy).map(([key, field]) => [
39
+ key,
40
+ {
41
+ type: field.type,
42
+ required: !!field.required,
43
+ description: field.description,
44
+ },
45
+ ]));
46
+ }
47
+ const metadata = schema;
48
+ if (metadata.type !== "object")
49
+ return {};
50
+ const required = new Set(metadata.required ?? []);
51
+ return Object.fromEntries(Object.entries(metadata.properties ?? {}).map(([key, field]) => [
52
+ key,
53
+ {
54
+ type: baseType(field),
55
+ displayType: displayType(field),
56
+ required: required.has(key),
57
+ description: field.description,
58
+ enum: enumValues(field),
59
+ const: field.const,
60
+ },
61
+ ]));
62
+ }
63
+ export function validateLegacyInput(schema, input) {
64
+ const provided = input && typeof input === "object"
65
+ ? input
66
+ : {};
67
+ const missing = [];
68
+ const unknown = [];
69
+ const typeErrors = [];
70
+ for (const [key, spec] of Object.entries(schema)) {
71
+ if (spec.required && !(key in provided))
72
+ missing.push(key);
73
+ }
74
+ for (const key of Object.keys(provided)) {
75
+ if (!(key in schema)) {
76
+ unknown.push(key);
77
+ continue;
78
+ }
79
+ const expected = schema[key].type;
80
+ const actual = valueType(provided[key]);
81
+ if (provided[key] !== null &&
82
+ provided[key] !== undefined &&
83
+ expected !== actual) {
84
+ typeErrors.push({ field: key, expected, actual });
85
+ }
86
+ }
87
+ return validationResult({ missing, unknown, typeErrors });
88
+ }
89
+ export function validateTypedInput(schema, input) {
90
+ if (Check(schema, input))
91
+ return validationResult({});
92
+ return validationResult({
93
+ errors: [...Errors(schema, input)].map((error) => {
94
+ const path = error.instancePath || "/";
95
+ return `${path} ${error.message}`;
96
+ }),
97
+ });
98
+ }
99
+ export function formatValidationError(path, result) {
100
+ const parts = [`Invalid input for ${path}.`];
101
+ if (result.missing.length > 0) {
102
+ parts.push(`Missing required fields: ${result.missing.join(", ")}.`);
103
+ }
104
+ if (result.unknown.length > 0) {
105
+ parts.push(`Unknown fields: ${result.unknown.join(", ")}.`);
106
+ }
107
+ if (result.typeErrors.length > 0) {
108
+ parts.push(`Type errors: ${result.typeErrors
109
+ .map((e) => `${e.field} expected ${e.expected}, got ${e.actual}`)
110
+ .join("; ")}.`);
111
+ }
112
+ if (result.errors.length > 0) {
113
+ parts.push(`Validation errors: ${result.errors.join("; ")}.`);
114
+ }
115
+ return parts.join(" ");
116
+ }
117
+ function validationResult(input) {
118
+ const missing = input.missing ?? [];
119
+ const unknown = input.unknown ?? [];
120
+ const typeErrors = input.typeErrors ?? [];
121
+ const errors = input.errors ?? [];
122
+ return {
123
+ ok: missing.length === 0 &&
124
+ unknown.length === 0 &&
125
+ typeErrors.length === 0 &&
126
+ errors.length === 0,
127
+ missing,
128
+ unknown,
129
+ typeErrors,
130
+ errors,
131
+ };
132
+ }
133
+ function displayType(schema) {
134
+ if (schema.anyOf?.length)
135
+ return schema.anyOf.map(displayType).join(" | ");
136
+ if (schema.enum?.length)
137
+ return schema.enum.map(String).join(" | ");
138
+ if (schema.const !== undefined)
139
+ return JSON.stringify(schema.const);
140
+ return schema.type ?? "unknown";
141
+ }
142
+ function baseType(schema) {
143
+ if (schema.anyOf?.length) {
144
+ const types = [...new Set(schema.anyOf.map(baseType))];
145
+ return types.length === 1 ? types[0] : types.join(" | ");
146
+ }
147
+ if (schema.type)
148
+ return schema.type;
149
+ if (schema.const !== undefined)
150
+ return valueType(schema.const);
151
+ return "unknown";
152
+ }
153
+ function enumValues(schema) {
154
+ if (schema.enum?.length)
155
+ return schema.enum;
156
+ if (schema.anyOf?.length &&
157
+ schema.anyOf.every((s) => s.const !== undefined)) {
158
+ return schema.anyOf.map((s) => s.const);
159
+ }
160
+ return undefined;
161
+ }
162
+ function valueType(value) {
163
+ if (value === null)
164
+ return "null";
165
+ if (Array.isArray(value))
166
+ return "array";
167
+ return typeof value;
168
+ }
@@ -1,10 +1,30 @@
1
+ import type { TSchema } from "typebox";
1
2
  export interface InputField {
2
3
  type: "string" | "number" | "boolean" | "object" | "array";
3
4
  description?: string;
4
5
  required?: boolean;
5
6
  default?: unknown;
6
7
  }
7
- export type InputSchema = Record<string, InputField>;
8
+ export type LegacyInputSchema = Record<string, InputField>;
9
+ export type TypedInputSchema = TSchema;
10
+ export type InputSchema = LegacyInputSchema | TypedInputSchema;
11
+ export interface ConnectionSchemaField {
12
+ type: string;
13
+ required?: boolean;
14
+ description?: string;
15
+ default?: unknown;
16
+ env?: string;
17
+ }
18
+ export type LegacyConnectionSchema = Record<string, ConnectionSchemaField>;
19
+ export type ConnectionSchema = LegacyConnectionSchema | TSchema;
20
+ export interface HelpInput {
21
+ type: string;
22
+ displayType?: string;
23
+ required: boolean;
24
+ description?: string;
25
+ enum?: unknown[];
26
+ const?: unknown;
27
+ }
8
28
  export interface ActionDef {
9
29
  name: string;
10
30
  description?: string;
@@ -73,13 +93,7 @@ export interface PluginDef {
73
93
  name: string;
74
94
  version: string;
75
95
  actions: ActionDef[];
76
- connectionConfigSchema?: Record<string, {
77
- type: string;
78
- required?: boolean;
79
- description?: string;
80
- default?: unknown;
81
- env?: string;
82
- }>;
96
+ connectionConfigSchema?: ConnectionSchema;
83
97
  /** OAuth2 config for `runline auth <plugin>`. */
84
98
  oauth?: OAuthConfig;
85
99
  /** @internal */
@@ -27,6 +27,11 @@ async function accessToken(ctx) {
27
27
  }
28
28
  // ─── Request ─────────────────────────────────────────────────────
29
29
  const API_BASE = "https://gmail.googleapis.com/gmail/v1/users/me";
30
+ function sendFailureRetryHint(method, path) {
31
+ if (method !== "POST" || !path.endsWith("/send"))
32
+ return "";
33
+ return " Before retrying, check whether the email was already sent using gmail.message.list({ q: 'in:sent to:<recipient> subject:\"<subject>\"', maxResults: 10 }) and inspect likely matches with gmail.message.get({ id, format: 'metadata' }).";
34
+ }
30
35
  async function gmailRequest(ctx, method, path, body, qs) {
31
36
  const token = await accessToken(ctx);
32
37
  const url = new URL(`${API_BASE}${path}`);
@@ -55,12 +60,19 @@ async function gmailRequest(ctx, method, path, body, qs) {
55
60
  "application/json";
56
61
  init.body = JSON.stringify(body);
57
62
  }
58
- const res = await fetch(url.toString(), init);
63
+ let res;
64
+ try {
65
+ res = await fetch(url.toString(), init);
66
+ }
67
+ catch (err) {
68
+ const msg = err instanceof Error ? err.message : String(err);
69
+ throw new Error(`gmail: ${method} ${path} failed: ${msg}.${sendFailureRetryHint(method, path)}`);
70
+ }
59
71
  if (res.status === 204)
60
72
  return { success: true };
61
73
  const text = await res.text();
62
74
  if (!res.ok) {
63
- throw new Error(`gmail: ${method} ${path} → ${res.status} ${text}`);
75
+ throw new Error(`gmail: ${method} ${path} → ${res.status} ${text}${sendFailureRetryHint(method, path)}`);
64
76
  }
65
77
  return text ? JSON.parse(text) : { success: true };
66
78
  }
@@ -0,0 +1,65 @@
1
+ import * as t from "typebox";
2
+ import { ATTACHMENT_FIELDS, bindGetAction, bindListAction, gql, key } from "./shared.js";
3
+ export function registerAttachmentActions(rl) {
4
+ const listAction = bindListAction(rl);
5
+ const getAction = bindGetAction(rl);
6
+ listAction("attachment.list", "List issue attachments.", "attachments", "AttachmentFilter", ATTACHMENT_FIELDS);
7
+ getAction("attachment.get", "Get an attachment by ID.", "attachment", ATTACHMENT_FIELDS);
8
+ rl.registerAction("attachment.create", {
9
+ description: "Create an attachment on an issue.",
10
+ inputSchema: t.Object({
11
+ issueId: t.String({ description: "The issue to associate the attachment with. UUID or issue identifier (e.g., 'LIN-123')" }),
12
+ title: t.String({ description: "The attachment title" }),
13
+ url: t.String({ description: "Attachment location, also used as a unique identifier. Re-creating with the same url updates the existing record" }),
14
+ subtitle: t.Optional(t.String({ description: "The attachment subtitle" })),
15
+ iconUrl: t.Optional(t.String({ description: "An icon url to display with the attachment (jpg or png, max 1MB, ideally 20x20px)" })),
16
+ commentBody: t.Optional(t.String({ description: "Create a linked comment with markdown body" })),
17
+ groupBySource: t.Optional(t.Boolean({ description: "Whether attachments for the same source application should be grouped in the Linear UI" })),
18
+ metadata: t.Optional(t.Object({}, { description: "Attachment metadata object with string and number values (JSONObject)" })),
19
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
20
+ }),
21
+ async execute(input, ctx) {
22
+ const data = await gql(key(ctx), `mutation($input: AttachmentCreateInput!) { attachmentCreate(input: $input) { success attachment { ${ATTACHMENT_FIELDS} } } }`, { input: input });
23
+ return data.attachmentCreate?.attachment;
24
+ },
25
+ });
26
+ rl.registerAction("attachment.update", {
27
+ description: "Update an attachment. title is required.",
28
+ inputSchema: t.Object({
29
+ id: t.String({ description: "The identifier of the attachment to update" }),
30
+ title: t.String({ description: "The attachment title" }),
31
+ subtitle: t.Optional(t.String({ description: "The attachment subtitle" })),
32
+ iconUrl: t.Optional(t.String({ description: "An icon url to display with the attachment" })),
33
+ metadata: t.Optional(t.Object({}, { description: "Attachment metadata object with string and number values (JSONObject)" })),
34
+ }),
35
+ async execute(input, ctx) {
36
+ const { id, ...fields } = input;
37
+ const data = await gql(key(ctx), `mutation($id: String!, $input: AttachmentUpdateInput!) { attachmentUpdate(id: $id, input: $input) { success attachment { ${ATTACHMENT_FIELDS} } } }`, { id, input: fields });
38
+ return data.attachmentUpdate?.attachment;
39
+ },
40
+ });
41
+ rl.registerAction("attachment.linkURL", {
42
+ description: "Link any URL to an issue. If a workspace integration matches the URL (Zendesk, GitHub, Slack, etc.) a rich attachment is created; otherwise a basic one.",
43
+ inputSchema: t.Object({
44
+ issueId: t.String({ description: "The issue for which to link the url. UUID or issue identifier (e.g., 'LIN-123')" }),
45
+ url: t.String({ description: "The url to link" }),
46
+ title: t.Optional(t.String({ description: "The title to use for the attachment" })),
47
+ id: t.Optional(t.String({ description: "The id for the attachment (optional UUID override)" })),
48
+ }),
49
+ async execute(input, ctx) {
50
+ const { issueId, url, title, id } = input;
51
+ const data = await gql(key(ctx), `mutation($issueId: String!, $url: String!, $title: String, $id: String) {
52
+ attachmentLinkURL(issueId: $issueId, url: $url, title: $title, id: $id) { success attachment { ${ATTACHMENT_FIELDS} } }
53
+ }`, { issueId, url, title: title ?? null, id: id ?? null });
54
+ return data.attachmentLinkURL?.attachment;
55
+ },
56
+ });
57
+ rl.registerAction("attachment.delete", {
58
+ description: "Delete an attachment.",
59
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the attachment to delete" }) }),
60
+ async execute(input, ctx) {
61
+ const data = await gql(key(ctx), `mutation($id: String!) { attachmentDelete(id: $id) { success } }`, { id: input.id });
62
+ return data.attachmentDelete;
63
+ },
64
+ });
65
+ }
@@ -0,0 +1,50 @@
1
+ import * as t from "typebox";
2
+ import { COMMENT_FIELDS, bindListAction, gql, key } from "./shared.js";
3
+ export function registerCommentActions(rl) {
4
+ const listAction = bindListAction(rl);
5
+ rl.registerAction("issue.addComment", {
6
+ description: "Add a comment to an issue. Pass parentId to nest as a reply.",
7
+ inputSchema: t.Object({
8
+ issueId: t.String({ description: "The issue to associate the comment with. UUID or issue identifier (e.g., 'LIN-123')" }),
9
+ body: t.String({ description: "The comment content in markdown format" }),
10
+ parentId: t.Optional(t.String({ description: "The parent comment under which to nest this comment" })),
11
+ doNotSubscribeToIssue: t.Optional(t.Boolean({ description: "Prevent auto-subscription to the issue the comment is created on" })),
12
+ quotedText: t.Optional(t.String({ description: "The text that this comment references (inline comments)" })),
13
+ }),
14
+ async execute(input, ctx) {
15
+ const fields = input;
16
+ const data = await gql(key(ctx), `mutation($input: CommentCreateInput!) { commentCreate(input: $input) { success comment { ${COMMENT_FIELDS} } } }`, { input: fields });
17
+ return data.commentCreate?.comment;
18
+ },
19
+ });
20
+ listAction("comment.list", "List comments across the workspace.", "comments", "CommentFilter", COMMENT_FIELDS);
21
+ rl.registerAction("comment.get", {
22
+ description: "Get a comment by ID.",
23
+ inputSchema: t.Object({ id: t.String() }),
24
+ async execute(input, ctx) {
25
+ const data = await gql(key(ctx), `query($id: String!) { comment(id: $id) { ${COMMENT_FIELDS} } }`, { id: input.id });
26
+ return data.comment;
27
+ },
28
+ });
29
+ rl.registerAction("comment.update", {
30
+ description: "Update a comment.",
31
+ inputSchema: t.Object({
32
+ id: t.String({ description: "The identifier of the comment to update" }),
33
+ body: t.Optional(t.String({ description: "The comment content in markdown format" })),
34
+ quotedText: t.Optional(t.String({ description: "The text that this comment references (inline comments)" })),
35
+ }),
36
+ async execute(input, ctx) {
37
+ const { id, ...fields } = input;
38
+ const data = await gql(key(ctx), `mutation($id: String!, $input: CommentUpdateInput!) { commentUpdate(id: $id, input: $input) { success comment { ${COMMENT_FIELDS} } } }`, { id, input: fields });
39
+ return data.commentUpdate?.comment;
40
+ },
41
+ });
42
+ rl.registerAction("comment.delete", {
43
+ description: "Delete a comment.",
44
+ inputSchema: t.Object({ id: t.String() }),
45
+ async execute(input, ctx) {
46
+ const data = await gql(key(ctx), `mutation($id: String!) { commentDelete(id: $id) { success } }`, { id: input.id });
47
+ return data.commentDelete;
48
+ },
49
+ });
50
+ }
@@ -0,0 +1,40 @@
1
+ import * as t from "typebox";
2
+ import { CYCLE_FIELDS, bindGetAction, bindListAction, gql, key } from "./shared.js";
3
+ export function registerCycleActions(rl) {
4
+ const listAction = bindListAction(rl);
5
+ const getAction = bindGetAction(rl);
6
+ listAction("cycle.list", "List cycles. Use filter for isActive/isNext/isPrevious.", "cycles", "CycleFilter", CYCLE_FIELDS);
7
+ getAction("cycle.get", "Get a cycle by ID.", "cycle", CYCLE_FIELDS);
8
+ rl.registerAction("cycle.create", {
9
+ description: "Create a cycle for a team.",
10
+ inputSchema: t.Object({
11
+ teamId: t.String({ description: "The team to associate the cycle with" }),
12
+ startsAt: t.String({ description: "The start time of the cycle (DateTime, ISO 8601)" }),
13
+ endsAt: t.String({ description: "The end time of the cycle (DateTime, ISO 8601)" }),
14
+ name: t.Optional(t.String({ description: "The custom name of the cycle" })),
15
+ description: t.Optional(t.String({ description: "The description of the cycle" })),
16
+ completedAt: t.Optional(t.String({ description: "The completion time of the cycle (DateTime). If null, the cycle hasn't been completed" })),
17
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
18
+ }),
19
+ async execute(input, ctx) {
20
+ const data = await gql(key(ctx), `mutation($input: CycleCreateInput!) { cycleCreate(input: $input) { success cycle { ${CYCLE_FIELDS} } } }`, { input: input });
21
+ return data.cycleCreate?.cycle;
22
+ },
23
+ });
24
+ rl.registerAction("cycle.update", {
25
+ description: "Update a cycle.",
26
+ inputSchema: t.Object({
27
+ id: t.String({ description: "The identifier of the cycle to update" }),
28
+ name: t.Optional(t.String({ description: "The custom name of the cycle" })),
29
+ description: t.Optional(t.String({ description: "The description of the cycle" })),
30
+ startsAt: t.Optional(t.String({ description: "The start time of the cycle (DateTime, ISO 8601)" })),
31
+ endsAt: t.Optional(t.String({ description: "The end time of the cycle (DateTime, ISO 8601)" })),
32
+ completedAt: t.Optional(t.String({ description: "The completion time of the cycle (DateTime). If null, the cycle hasn't been completed" })),
33
+ }),
34
+ async execute(input, ctx) {
35
+ const { id, ...fields } = input;
36
+ const data = await gql(key(ctx), `mutation($id: String!, $input: CycleUpdateInput!) { cycleUpdate(id: $id, input: $input) { success cycle { ${CYCLE_FIELDS} } } }`, { id, input: fields });
37
+ return data.cycleUpdate?.cycle;
38
+ },
39
+ });
40
+ }