runline 0.7.7 → 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.
- package/dist/commands/auth.js +4 -2
- package/dist/config/loader.d.ts +2 -4
- package/dist/config/loader.js +2 -1
- package/dist/core/engine.js +18 -13
- package/dist/index.d.ts +2 -2
- package/dist/plugin/api.d.ts +2 -9
- package/dist/plugin/api.js +1 -4
- package/dist/plugin/schema.d.ts +19 -0
- package/dist/plugin/schema.js +168 -0
- package/dist/plugin/types.d.ts +22 -8
- package/dist/plugins/_shared/imageFile.js +40 -0
- package/dist/plugins/_shared/microsoftAuth.js +100 -0
- package/dist/plugins/gmail/src/index.js +27 -3
- package/dist/plugins/googleAppsScript/src/index.js +203 -0
- package/dist/plugins/googleImage/src/index.js +30 -11
- package/dist/plugins/linear/src/attachments.js +65 -0
- package/dist/plugins/linear/src/comments.js +50 -0
- package/dist/plugins/linear/src/cycles.js +40 -0
- package/dist/plugins/linear/src/index.js +31 -1153
- package/dist/plugins/linear/src/initiatives.js +79 -0
- package/dist/plugins/linear/src/issues.js +245 -0
- package/dist/plugins/linear/src/labels.js +69 -0
- package/dist/plugins/linear/src/organization.js +12 -0
- package/dist/plugins/linear/src/projects.js +189 -0
- package/dist/plugins/linear/src/shared.js +131 -0
- package/dist/plugins/linear/src/states.js +39 -0
- package/dist/plugins/linear/src/teams.js +74 -0
- package/dist/plugins/linear/src/users.js +36 -0
- package/dist/plugins/linear/src/views.js +96 -0
- package/dist/plugins/linear/src/webhooks.js +57 -0
- package/dist/plugins/microsoftCalendar/src/index.js +46 -0
- package/dist/plugins/microsoftFiles/src/index.js +127 -0
- package/dist/plugins/microsoftMail/src/index.js +91 -0
- package/dist/plugins/openai/src/index.js +45 -20
- package/dist/plugins/parallel/src/index.js +100 -0
- package/dist/plugins/recraft/src/index.js +10 -6
- package/dist/plugins/replicate/src/index.js +17 -3
- package/dist/plugins/steel/src/index.js +378 -0
- package/dist/plugins/together/src/index.js +10 -6
- package/dist/plugins/xai/src/index.js +11 -5
- package/package.json +3 -2
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
|
24
|
-
const
|
|
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);
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -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?:
|
|
21
|
-
env?: string;
|
|
22
|
-
}>): ConnectionConfig;
|
|
20
|
+
export declare function applyEnvOverrides(conn: ConnectionConfig, schema?: ConnectionSchema): ConnectionConfig;
|
package/dist/config/loader.js
CHANGED
|
@@ -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)
|
package/dist/core/engine.js
CHANGED
|
@@ -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:
|
|
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
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
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,
|
|
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";
|
package/dist/plugin/api.d.ts
CHANGED
|
@@ -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:
|
|
9
|
+
setConnectionSchema(schema: ConnectionSchema): void;
|
|
17
10
|
setName(name: string): void;
|
|
18
11
|
setVersion(version: string): void;
|
|
19
12
|
/**
|
package/dist/plugin/api.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -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
|
|
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?:
|
|
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 */
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for image-generation plugins: write generated bytes to a
|
|
3
|
+
* file and return its path, instead of returning raw base64.
|
|
4
|
+
*
|
|
5
|
+
* Base64 image payloads in an action result bloat the agent context and are
|
|
6
|
+
* stripped by many hosts before reaching the model, so the agent can never
|
|
7
|
+
* actually deliver the image. A file `path` hands straight to a host
|
|
8
|
+
* send_file/attachment tool. Plugins load in-process via jiti, so node:fs is
|
|
9
|
+
* available (same pattern as googleDrive/googleSlides).
|
|
10
|
+
*/
|
|
11
|
+
import { writeFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
const MIME_EXT = {
|
|
15
|
+
"image/png": "png",
|
|
16
|
+
"image/jpeg": "jpg",
|
|
17
|
+
"image/jpg": "jpg",
|
|
18
|
+
"image/webp": "webp",
|
|
19
|
+
"image/gif": "gif",
|
|
20
|
+
"image/svg+xml": "svg",
|
|
21
|
+
};
|
|
22
|
+
export function extForMime(mimeType) {
|
|
23
|
+
if (!mimeType)
|
|
24
|
+
return "png";
|
|
25
|
+
return MIME_EXT[mimeType] ?? mimeType.split("/")[1]?.split("+")[0] ?? "png";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Decode base64 image bytes and write them to `saveDir` (or the OS temp dir),
|
|
29
|
+
* named `<provider>-<stamp>-<index>.<ext>`. Returns the file path + size.
|
|
30
|
+
*/
|
|
31
|
+
export function writeImageFile(opts) {
|
|
32
|
+
const mimeType = opts.mimeType ?? "image/png";
|
|
33
|
+
const dir = (typeof opts.saveDir === "string" && opts.saveDir.trim()) || tmpdir();
|
|
34
|
+
const bytes = Buffer.from(opts.base64 ?? "", "base64");
|
|
35
|
+
const stamp = opts.stamp ?? Date.now();
|
|
36
|
+
const path = join(dir, `${opts.provider}-${stamp}-${opts.index}.${extForMime(mimeType)}`);
|
|
37
|
+
writeFileSync(path, bytes);
|
|
38
|
+
return { path, mimeType, byteLength: bytes.length };
|
|
39
|
+
}
|
|
40
|
+
export const SEND_FILE_NOTE = "Image(s) written to disk. Deliver each to the user with send_file using its `path`.";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const REFRESH_SKEW_MS = 60_000;
|
|
2
|
+
function authority(cfg) {
|
|
3
|
+
return `https://login.microsoftonline.com/${cfg.tenantId || "common"}/oauth2/v2.0/token`;
|
|
4
|
+
}
|
|
5
|
+
export function isAppOnly(cfg) {
|
|
6
|
+
return !cfg.refreshToken && !!(cfg.tenantId && cfg.clientId && cfg.clientSecret);
|
|
7
|
+
}
|
|
8
|
+
/** Graph path prefix for the acting principal: /me (delegated) or /users/{upn} (app-only). */
|
|
9
|
+
export function userBase(ctx) {
|
|
10
|
+
const cfg = ctx.connection.config;
|
|
11
|
+
if (cfg.refreshToken)
|
|
12
|
+
return "/me";
|
|
13
|
+
if (cfg.userUpn)
|
|
14
|
+
return `/users/${encodeURIComponent(cfg.userUpn)}`;
|
|
15
|
+
throw new Error("microsoft: app-only mode requires userUpn (target mailbox/drive). Set MS_GRAPH_USER_UPN, or connect via OAuth.");
|
|
16
|
+
}
|
|
17
|
+
export async function microsoftAccessToken(ctx, pluginName, scopes) {
|
|
18
|
+
const cfg = ctx.connection.config;
|
|
19
|
+
if (cfg.accessToken &&
|
|
20
|
+
typeof cfg.accessTokenExpiresAt === "number" &&
|
|
21
|
+
Date.now() < cfg.accessTokenExpiresAt - REFRESH_SKEW_MS) {
|
|
22
|
+
return cfg.accessToken;
|
|
23
|
+
}
|
|
24
|
+
let body;
|
|
25
|
+
if (cfg.refreshToken) {
|
|
26
|
+
if (!cfg.clientId || !cfg.clientSecret) {
|
|
27
|
+
throw new Error(`${pluginName}: missing clientId/clientSecret for OAuth refresh.`);
|
|
28
|
+
}
|
|
29
|
+
body = new URLSearchParams({
|
|
30
|
+
client_id: cfg.clientId,
|
|
31
|
+
client_secret: cfg.clientSecret,
|
|
32
|
+
refresh_token: cfg.refreshToken,
|
|
33
|
+
grant_type: "refresh_token",
|
|
34
|
+
scope: [...scopes, "offline_access"].join(" "),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else if (isAppOnly(cfg)) {
|
|
38
|
+
body = new URLSearchParams({
|
|
39
|
+
client_id: cfg.clientId,
|
|
40
|
+
client_secret: cfg.clientSecret,
|
|
41
|
+
grant_type: "client_credentials",
|
|
42
|
+
scope: "https://graph.microsoft.com/.default",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
throw new Error(`${pluginName}: no credentials. Connect via OAuth, or set tenantId/clientId/clientSecret (app-only).`);
|
|
47
|
+
}
|
|
48
|
+
const res = await fetch(authority(cfg), {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
51
|
+
body: body.toString(),
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw new Error(`${pluginName}: token request failed (${res.status}): ${await res.text()}`);
|
|
55
|
+
}
|
|
56
|
+
const data = (await res.json());
|
|
57
|
+
const patch = {
|
|
58
|
+
accessToken: data.access_token,
|
|
59
|
+
accessTokenExpiresAt: Date.now() + data.expires_in * 1000,
|
|
60
|
+
};
|
|
61
|
+
// Microsoft rotates refresh tokens — persist the new one when present.
|
|
62
|
+
if (data.refresh_token)
|
|
63
|
+
patch.refreshToken = data.refresh_token;
|
|
64
|
+
await ctx.updateConnection(patch);
|
|
65
|
+
return data.access_token;
|
|
66
|
+
}
|
|
67
|
+
/** Authenticated Graph v1.0 request. Returns parsed JSON ({success:true} for 204). */
|
|
68
|
+
export async function graphRequest(ctx, pluginName, scopes, method, path, body) {
|
|
69
|
+
const token = await microsoftAccessToken(ctx, pluginName, scopes);
|
|
70
|
+
const init = {
|
|
71
|
+
method,
|
|
72
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
73
|
+
};
|
|
74
|
+
if (body !== undefined) {
|
|
75
|
+
init.headers["Content-Type"] = "application/json";
|
|
76
|
+
init.body = JSON.stringify(body);
|
|
77
|
+
}
|
|
78
|
+
const res = await fetch(`https://graph.microsoft.com/v1.0${path}`, init);
|
|
79
|
+
if (res.status === 204)
|
|
80
|
+
return { success: true };
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
if (!res.ok)
|
|
83
|
+
throw new Error(`${pluginName}: ${method} ${path} → ${res.status} ${text}`);
|
|
84
|
+
return text ? JSON.parse(text) : { success: true };
|
|
85
|
+
}
|
|
86
|
+
/** Setup help shown by the OAuth wizard for all Microsoft plugins. */
|
|
87
|
+
export function microsoftSetupHelp(apiName) {
|
|
88
|
+
return [
|
|
89
|
+
`You need a Microsoft Entra (Azure AD) app registration. One-time, ~5 minutes.`,
|
|
90
|
+
"",
|
|
91
|
+
"1. Register an app: https://entra.microsoft.com → App registrations → New registration.",
|
|
92
|
+
" Supported account types: your org (single tenant) is fine.",
|
|
93
|
+
"2. Add a Web redirect URI (Authentication → Add platform → Web):",
|
|
94
|
+
" {{redirectUri}}",
|
|
95
|
+
"3. Certificates & secrets → New client secret → copy the VALUE (not the Secret ID).",
|
|
96
|
+
`4. API permissions → Add → Microsoft Graph → Delegated → add the ${apiName} scopes,`,
|
|
97
|
+
" then 'Grant admin consent'.",
|
|
98
|
+
"5. Paste the Application (client) ID and the client secret VALUE below.",
|
|
99
|
+
];
|
|
100
|
+
}
|
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -108,6 +120,18 @@ function header(name, value) {
|
|
|
108
120
|
function foldedBase64ByteLength(length) {
|
|
109
121
|
return length === 0 ? 0 : length + Math.floor((length - 1) / 76) * CRLF.length;
|
|
110
122
|
}
|
|
123
|
+
function normalizeMimeBase64(value, index) {
|
|
124
|
+
const compact = value.replace(/\s+/g, "");
|
|
125
|
+
if (!/^[A-Za-z0-9+/=_-]*$/.test(compact)) {
|
|
126
|
+
throw new Error(`gmail: attachment ${index} contentBase64 contains invalid base64 characters`);
|
|
127
|
+
}
|
|
128
|
+
const standard = compact.replace(/-/g, "+").replace(/_/g, "/");
|
|
129
|
+
const remainder = standard.length % 4;
|
|
130
|
+
if (remainder === 1) {
|
|
131
|
+
throw new Error(`gmail: attachment ${index} contentBase64 has invalid base64 length`);
|
|
132
|
+
}
|
|
133
|
+
return remainder === 0 ? standard : `${standard}${"=".repeat(4 - remainder)}`;
|
|
134
|
+
}
|
|
111
135
|
function foldBase64(encoded) {
|
|
112
136
|
let folded = "";
|
|
113
137
|
for (let i = 0; i < encoded.length; i += 76) {
|
|
@@ -147,7 +171,7 @@ function normalizeAttachment(input, index) {
|
|
|
147
171
|
return {
|
|
148
172
|
name: input.name ?? input.filename ?? `attachment-${index + 1}`,
|
|
149
173
|
mimeType: input.mimeType ?? "application/octet-stream",
|
|
150
|
-
contentBase64,
|
|
174
|
+
contentBase64: normalizeMimeBase64(contentBase64, index),
|
|
151
175
|
};
|
|
152
176
|
}
|
|
153
177
|
function attachmentPart(att) {
|