runline 0.28.1 → 0.30.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.
- package/dist/auth/errors.d.ts +22 -0
- package/dist/auth/errors.js +25 -0
- package/dist/auth/oauth2.d.ts +21 -0
- package/dist/auth/oauth2.js +154 -0
- package/dist/auth/token.d.ts +11 -0
- package/dist/auth/token.js +231 -0
- package/dist/auth/types.d.ts +93 -0
- package/dist/auth/types.js +1 -0
- package/dist/commands/auth.js +26 -13
- package/dist/commands/connection.js +3 -6
- package/dist/commands/exec.js +7 -2
- package/dist/commands/init.js +9 -2
- package/dist/config/loader.d.ts +7 -13
- package/dist/config/loader.js +58 -88
- package/dist/config/store.d.ts +17 -0
- package/dist/config/store.js +85 -0
- package/dist/connections/file.d.ts +13 -0
- package/dist/connections/file.js +53 -0
- package/dist/connections/memory.d.ts +15 -0
- package/dist/connections/memory.js +70 -0
- package/dist/connections/types.d.ts +47 -0
- package/dist/connections/types.js +10 -0
- package/dist/core/engine.d.ts +5 -2
- package/dist/core/engine.js +48 -55
- package/dist/core/oauth.d.ts +20 -15
- package/dist/core/oauth.js +195 -98
- package/dist/credentials/http.d.ts +19 -0
- package/dist/credentials/http.js +109 -0
- package/dist/credentials/policy.d.ts +5 -0
- package/dist/credentials/policy.js +92 -0
- package/dist/credentials/registry.d.ts +31 -0
- package/dist/credentials/registry.js +171 -0
- package/dist/credentials/transport.d.ts +38 -0
- package/dist/credentials/transport.js +283 -0
- package/dist/credentials/types.d.ts +80 -0
- package/dist/credentials/types.js +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/plugin/types.d.ts +30 -17
- package/dist/plugins/_shared/credentialAdapter.js +88 -0
- package/dist/plugins/_shared/googleAuth.js +89 -103
- package/dist/plugins/_shared/googleCredentials.js +160 -0
- package/dist/plugins/_shared/microsoftAuth.js +81 -74
- package/dist/plugins/_shared/microsoftCredentials.js +90 -0
- package/dist/plugins/_shared/tokenRefresh.js +39 -0
- package/dist/plugins/bitwarden/src/index.js +13 -31
- package/dist/plugins/gmail/src/index.js +8 -44
- package/dist/plugins/googleAppsScript/src/index.js +3 -25
- package/dist/plugins/googleCalendar/src/index.js +3 -41
- package/dist/plugins/googleContacts/src/index.js +3 -36
- package/dist/plugins/googleDocs/src/index.js +5 -0
- package/dist/plugins/googleDocs/src/shared.js +2 -30
- package/dist/plugins/googleDrive/src/index.js +52 -98
- package/dist/plugins/googleSheets/src/index.js +3 -40
- package/dist/plugins/googleSlides/src/index.js +5 -39
- package/dist/plugins/googleTasks/src/index.js +3 -30
- package/dist/plugins/halopsa/src/index.js +16 -31
- package/dist/plugins/linear/src/comments.js +2 -2
- package/dist/plugins/linear/src/index.js +1 -1
- package/dist/plugins/linear/src/issues.js +1 -17
- package/dist/plugins/microsoftCalendar/src/index.js +2 -1
- package/dist/plugins/microsoftFiles/src/index.js +10 -24
- package/dist/plugins/microsoftMail/src/index.js +2 -1
- package/dist/plugins/openai/src/index.js +13 -8
- package/dist/plugins/paypal/src/index.js +14 -29
- package/dist/plugins/plaud/src/index.js +210 -0
- package/dist/plugins/plaud/src/shared.js +221 -0
- package/dist/plugins/salesforce/src/shared.js +17 -32
- package/dist/sdk.d.ts +13 -5
- package/dist/sdk.js +35 -29
- package/dist/utils/observer.d.ts +2 -0
- package/dist/utils/observer.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { AuthError, downloadResource, OAuthGrantSchema, } from "runline";
|
|
2
|
+
import * as t from "typebox";
|
|
3
|
+
import { credentialRuntime } from "../../_shared/credentialAdapter.js";
|
|
4
|
+
const BASE = "https://platform.plaud.ai/developer/api";
|
|
5
|
+
/** Public client identifier and fixed callback published in Plaud's official CLI. */
|
|
6
|
+
export const PLAUD_PUBLIC_CLIENT_ID = "client_f9e0b214-c11f-434b-8b95-c4497d1feb81";
|
|
7
|
+
export const PLAUD_REDIRECT_URI = "http://localhost:8199/auth/callback";
|
|
8
|
+
/** Third-party personal-recording API, NOT Plaud Embedded's partner-token API. */
|
|
9
|
+
export const PLAUD_OAUTH = {
|
|
10
|
+
id: "plaud.oauth2",
|
|
11
|
+
provider: "plaud",
|
|
12
|
+
authorization: { url: "https://web.plaud.ai/platform/oauth" },
|
|
13
|
+
exchange: {
|
|
14
|
+
url: `${BASE}/oauth/third-party/access-token`,
|
|
15
|
+
clientAuthentication: "client_id_basic",
|
|
16
|
+
grantType: null,
|
|
17
|
+
sendState: true,
|
|
18
|
+
requirePkce: true,
|
|
19
|
+
},
|
|
20
|
+
refresh: {
|
|
21
|
+
url: `${BASE}/oauth/third-party/access-token/refresh`,
|
|
22
|
+
clientAuthentication: "none",
|
|
23
|
+
grantType: null,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export const PLAUD_CREDENTIAL = {
|
|
27
|
+
id: "plaud",
|
|
28
|
+
methods: {
|
|
29
|
+
oauth2: {
|
|
30
|
+
schema: t.Object({ grant: t.Optional(OAuthGrantSchema) }, { additionalProperties: false }),
|
|
31
|
+
authentication: {
|
|
32
|
+
kind: "oauth2",
|
|
33
|
+
grantField: "grant",
|
|
34
|
+
definition: PLAUD_OAUTH,
|
|
35
|
+
renewal: "refresh",
|
|
36
|
+
},
|
|
37
|
+
targets: {
|
|
38
|
+
api: { baseUrl: `${BASE}/open/third-party/`, methods: ["GET"] },
|
|
39
|
+
},
|
|
40
|
+
probe: {
|
|
41
|
+
target: "api",
|
|
42
|
+
path: "users/current",
|
|
43
|
+
method: "GET",
|
|
44
|
+
acceptedStatuses: [200],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
export function plaudRuntime(ctx) {
|
|
50
|
+
return credentialRuntime(ctx, PLAUD_CREDENTIAL, "oauth2", (config) => [
|
|
51
|
+
config.clientId,
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
function object(value) {
|
|
55
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
56
|
+
throw new AuthError("invalid_response");
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
export async function request(ctx, path) {
|
|
60
|
+
const { binding, transport } = plaudRuntime(ctx);
|
|
61
|
+
const response = await transport.request(binding, { target: "api", path });
|
|
62
|
+
if (!response.ok)
|
|
63
|
+
throw new Error(`plaud: request failed (HTTP ${response.status})`);
|
|
64
|
+
try {
|
|
65
|
+
return object(await response.json());
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new AuthError("invalid_response");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function recording(ctx, id) {
|
|
72
|
+
// Encoding protects separators/query injection; the transport also rejects traversal.
|
|
73
|
+
return request(ctx, `files/${encodeURIComponent(id)}`);
|
|
74
|
+
}
|
|
75
|
+
export async function list(ctx, page, pageSize) {
|
|
76
|
+
const result = await request(ctx, `files/?${new URLSearchParams({ page: String(page), page_size: String(pageSize) })}`);
|
|
77
|
+
if (!Array.isArray(result.data) || result.data.length > pageSize)
|
|
78
|
+
throw new AuthError("invalid_response");
|
|
79
|
+
const data = result.data.map(object);
|
|
80
|
+
return { ...result, data };
|
|
81
|
+
}
|
|
82
|
+
export function blocks(file, field) {
|
|
83
|
+
const value = file[field];
|
|
84
|
+
if (value === undefined || value === null)
|
|
85
|
+
return [];
|
|
86
|
+
if (!Array.isArray(value))
|
|
87
|
+
throw new AuthError("invalid_response");
|
|
88
|
+
return value.map(object);
|
|
89
|
+
}
|
|
90
|
+
/** Linked content is a separate, unauthenticated capability approved by host config. */
|
|
91
|
+
export async function blockContent(ctx, block) {
|
|
92
|
+
if (!block)
|
|
93
|
+
return { status: "unavailable", content: null };
|
|
94
|
+
if (typeof block.data_content === "string" && block.data_content.length)
|
|
95
|
+
return { status: "available", content: block.data_content };
|
|
96
|
+
const link = block.data_link;
|
|
97
|
+
if (typeof link !== "string" || !link)
|
|
98
|
+
return { status: "unavailable", content: null };
|
|
99
|
+
const origins = ctx.connection.config.contentOrigins ?? [];
|
|
100
|
+
if (!Array.isArray(origins) ||
|
|
101
|
+
origins.some((origin) => {
|
|
102
|
+
if (typeof origin !== "string")
|
|
103
|
+
return true;
|
|
104
|
+
try {
|
|
105
|
+
const url = new URL(origin);
|
|
106
|
+
return url.protocol !== "https:" || url.origin !== origin;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}))
|
|
112
|
+
throw new AuthError("invalid_credentials");
|
|
113
|
+
let origin;
|
|
114
|
+
try {
|
|
115
|
+
const url = new URL(link);
|
|
116
|
+
if (url.protocol !== "https:" || url.username || url.password || url.hash)
|
|
117
|
+
throw new Error();
|
|
118
|
+
origin = url.origin;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new AuthError("request_not_allowed");
|
|
122
|
+
}
|
|
123
|
+
if (!origins.includes(origin))
|
|
124
|
+
return {
|
|
125
|
+
status: "link_requires_approval",
|
|
126
|
+
content: null,
|
|
127
|
+
contentUrl: link,
|
|
128
|
+
};
|
|
129
|
+
const response = await downloadResource(link, {
|
|
130
|
+
allowedOrigins: [...origins],
|
|
131
|
+
fetch: globalThis.fetch,
|
|
132
|
+
maxResponseBytes: 8 * 1024 * 1024,
|
|
133
|
+
});
|
|
134
|
+
if (!response.ok)
|
|
135
|
+
throw new Error(`plaud: content download failed (HTTP ${response.status})`);
|
|
136
|
+
const content = await response.text();
|
|
137
|
+
return {
|
|
138
|
+
status: content ? "available" : "unavailable",
|
|
139
|
+
content: content || null,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function utcDay(value) {
|
|
143
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
144
|
+
return undefined;
|
|
145
|
+
const ms = Date.parse(`${value}T00:00:00.000Z`);
|
|
146
|
+
return Number.isFinite(ms) &&
|
|
147
|
+
new Date(ms).toISOString().slice(0, 10) === value
|
|
148
|
+
? ms
|
|
149
|
+
: undefined;
|
|
150
|
+
}
|
|
151
|
+
/** Timezone-less timestamps are UTC; invalid calendar days/times are not normalized. */
|
|
152
|
+
function timestamp(value) {
|
|
153
|
+
if (typeof value !== "string")
|
|
154
|
+
return undefined;
|
|
155
|
+
const match = /^(\d{4}-\d{2}-\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/i.exec(value.trim());
|
|
156
|
+
if (!match)
|
|
157
|
+
return undefined;
|
|
158
|
+
const [, day, hour, minute, second = "00", fraction = "", zone = "Z"] = match;
|
|
159
|
+
const midnight = utcDay(day);
|
|
160
|
+
if (midnight === undefined || hour === undefined)
|
|
161
|
+
return midnight;
|
|
162
|
+
if (Number(hour) > 23 || Number(minute) > 59 || Number(second) > 59)
|
|
163
|
+
return undefined;
|
|
164
|
+
const result = Date.parse(`${day}T${hour}:${minute}:${second}${fraction}${zone}`);
|
|
165
|
+
return Number.isFinite(result) ? result : undefined;
|
|
166
|
+
}
|
|
167
|
+
export function dateBoundary(value, end) {
|
|
168
|
+
if (value === undefined)
|
|
169
|
+
return undefined;
|
|
170
|
+
const ms = utcDay(value);
|
|
171
|
+
if (ms === undefined)
|
|
172
|
+
throw new Error("plaud: invalid date; use YYYY-MM-DD");
|
|
173
|
+
return ms + (end ? 86_400_000 - 1 : 0);
|
|
174
|
+
}
|
|
175
|
+
export async function scan(ctx, options) {
|
|
176
|
+
if (options.from !== undefined &&
|
|
177
|
+
options.to !== undefined &&
|
|
178
|
+
options.from > options.to)
|
|
179
|
+
throw new Error("plaud: from must not be after to");
|
|
180
|
+
const recordings = [];
|
|
181
|
+
const seen = new Set();
|
|
182
|
+
const query = options.query?.toLowerCase();
|
|
183
|
+
let scanned = 0;
|
|
184
|
+
let pagesScanned = 0;
|
|
185
|
+
for (let page = 1; page <= options.maxPages; page++) {
|
|
186
|
+
const result = await list(ctx, page, 100);
|
|
187
|
+
pagesScanned++;
|
|
188
|
+
for (let index = 0; index < result.data.length; index++) {
|
|
189
|
+
const file = result.data[index];
|
|
190
|
+
scanned++;
|
|
191
|
+
if (typeof file.id !== "string" || !file.id)
|
|
192
|
+
throw new AuthError("invalid_response");
|
|
193
|
+
if (seen.has(file.id))
|
|
194
|
+
continue;
|
|
195
|
+
seen.add(file.id);
|
|
196
|
+
if (query &&
|
|
197
|
+
!(typeof file.name === "string" &&
|
|
198
|
+
file.name.toLowerCase().includes(query)))
|
|
199
|
+
continue;
|
|
200
|
+
if (options.from !== undefined || options.to !== undefined) {
|
|
201
|
+
const created = timestamp(file.created_at);
|
|
202
|
+
if (created === undefined ||
|
|
203
|
+
(options.from !== undefined && created < options.from) ||
|
|
204
|
+
(options.to !== undefined && created > options.to))
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
recordings.push(file);
|
|
208
|
+
if (recordings.length >= options.limit)
|
|
209
|
+
return {
|
|
210
|
+
recordings,
|
|
211
|
+
scanned,
|
|
212
|
+
pagesScanned,
|
|
213
|
+
truncated: index < result.data.length - 1 || result.data.length === 100,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (result.data.length < 100)
|
|
217
|
+
return { recordings, scanned, pagesScanned, truncated: false };
|
|
218
|
+
}
|
|
219
|
+
// Do not claim exhaustive results or assume provider ordering beyond the scanned pages.
|
|
220
|
+
return { recordings, scanned, pagesScanned, truncated: true };
|
|
221
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requestOAuth2Token } from "runline";
|
|
1
2
|
export const DEFAULT_API_VERSION = "v59.0";
|
|
2
3
|
export function config(ctx) {
|
|
3
4
|
return ctx.connection.config;
|
|
@@ -37,40 +38,24 @@ export async function getSession(ctx) {
|
|
|
37
38
|
const loginUrl = validateInstanceUrl(requireString(c.loginUrl ?? c.instanceUrl, "loginUrl or instanceUrl"));
|
|
38
39
|
const clientId = requireString(c.clientId, "clientId");
|
|
39
40
|
const clientSecret = requireString(c.clientSecret, "clientSecret");
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
const tokens = await requestOAuth2Token({
|
|
42
|
+
url: `${loginUrl}/services/oauth2/token`,
|
|
43
|
+
clientAuthentication: "client_secret_post",
|
|
44
|
+
response: {
|
|
45
|
+
metadata: {
|
|
46
|
+
instanceUrl: "instance_url",
|
|
47
|
+
id: "id",
|
|
48
|
+
issuedAt: "issued_at",
|
|
49
|
+
},
|
|
49
50
|
},
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
const text = await res.text();
|
|
53
|
-
let data;
|
|
54
|
-
try {
|
|
55
|
-
data = text ? JSON.parse(text) : {};
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
data = { raw: text };
|
|
59
|
-
}
|
|
60
|
-
if (!res.ok) {
|
|
61
|
-
const err = typeof data.error === "string" ? data.error : res.status;
|
|
62
|
-
const description = typeof data.error_description === "string"
|
|
63
|
-
? `: ${data.error_description}`
|
|
64
|
-
: "";
|
|
65
|
-
throw new Error(`Salesforce token error ${err}${description}`);
|
|
66
|
-
}
|
|
51
|
+
}, { grant_type: "client_credentials" }, { clientId, clientSecret });
|
|
67
52
|
return {
|
|
68
|
-
instanceUrl: validateInstanceUrl(requireString(
|
|
69
|
-
accessToken:
|
|
70
|
-
tokenType:
|
|
71
|
-
scope:
|
|
72
|
-
id:
|
|
73
|
-
issuedAt:
|
|
53
|
+
instanceUrl: validateInstanceUrl(requireString(tokens.metadata?.instanceUrl, "token response instance_url")),
|
|
54
|
+
accessToken: tokens.accessToken,
|
|
55
|
+
tokenType: tokens.tokenType ?? "Bearer",
|
|
56
|
+
scope: tokens.scope,
|
|
57
|
+
id: tokens.metadata?.id,
|
|
58
|
+
issuedAt: tokens.metadata?.issuedAt,
|
|
74
59
|
};
|
|
75
60
|
}
|
|
76
61
|
export async function api(ctx, method, endpoint, body, qs, session = getSession(ctx)) {
|
package/dist/sdk.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { ConnectionProvider } from "./connections/types.js";
|
|
2
|
+
import { type EngineHooks, type ExecuteResult } from "./core/engine.js";
|
|
2
3
|
import type { PluginFunction } from "./plugin/api.js";
|
|
3
4
|
import type { ActionAccess, ConnectionConfig, InputSchema, PluginDef } from "./plugin/types.js";
|
|
4
5
|
export interface RunlineOptions {
|
|
5
6
|
plugins?: Array<PluginDef | PluginFunction>;
|
|
7
|
+
/** Initial process-local connections. No environment lookup or disk writes. */
|
|
6
8
|
connections?: ConnectionConfig[];
|
|
9
|
+
/** Host-owned credentials; mutually exclusive with initial connections. */
|
|
10
|
+
connectionProvider?: ConnectionProvider;
|
|
7
11
|
timeoutMs?: number;
|
|
8
12
|
memoryLimitBytes?: number;
|
|
9
13
|
/**
|
|
@@ -19,7 +23,7 @@ export interface RunlineOptions {
|
|
|
19
23
|
* never breaks a run. Not part of `RunlineConfig` on purpose —
|
|
20
24
|
* config round-trips through JSON (`fromProject`), functions do not.
|
|
21
25
|
*/
|
|
22
|
-
onAction?:
|
|
26
|
+
onAction?: EngineHooks["onAction"];
|
|
23
27
|
}
|
|
24
28
|
export interface RunlineExecuteOptions {
|
|
25
29
|
timeoutMs?: number;
|
|
@@ -30,10 +34,11 @@ export interface RunlineExecuteOptions {
|
|
|
30
34
|
export declare class Runline {
|
|
31
35
|
private _registry;
|
|
32
36
|
private _config;
|
|
37
|
+
private readonly _connectionProvider;
|
|
38
|
+
private readonly _memoryConnections?;
|
|
33
39
|
/**
|
|
34
40
|
* One engine per Runline instance, so its pooled worker survives across
|
|
35
|
-
* `execute()` calls
|
|
36
|
-
* leak) a fresh worker every time — the exact bug pooling exists to fix.
|
|
41
|
+
* `execute()` calls and avoids per-execution thread allocation.
|
|
37
42
|
*/
|
|
38
43
|
private _engine;
|
|
39
44
|
private readonly _onAction;
|
|
@@ -65,7 +70,10 @@ export declare class Runline {
|
|
|
65
70
|
actions: string[];
|
|
66
71
|
connectionConfigSchema?: PluginDef["connectionConfigSchema"];
|
|
67
72
|
}>;
|
|
68
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Snapshot of process-local connections. Host-managed providers are not
|
|
75
|
+
* enumerated: their accounts can be caller-scoped and must be listed by the host.
|
|
76
|
+
*/
|
|
69
77
|
connections(): ConnectionConfig[];
|
|
70
78
|
/**
|
|
71
79
|
* Load runline from a project directory.
|
package/dist/sdk.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { existsSync
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { loadConfigFrom } from "./config/loader.js";
|
|
3
4
|
import { DEFAULT_CONFIG } from "./config/types.js";
|
|
5
|
+
import { FileConnectionProvider } from "./connections/file.js";
|
|
6
|
+
import { MemoryConnectionProvider } from "./connections/memory.js";
|
|
4
7
|
import { ExecutionEngine, } from "./core/engine.js";
|
|
5
8
|
import { resolvePluginExport } from "./plugin/api.js";
|
|
6
9
|
import { discoverPlugins } from "./plugin/loader.js";
|
|
@@ -8,22 +11,33 @@ import { PluginRegistry } from "./plugin/registry.js";
|
|
|
8
11
|
export class Runline {
|
|
9
12
|
_registry;
|
|
10
13
|
_config;
|
|
14
|
+
_connectionProvider;
|
|
15
|
+
_memoryConnections;
|
|
11
16
|
/**
|
|
12
17
|
* One engine per Runline instance, so its pooled worker survives across
|
|
13
|
-
* `execute()` calls
|
|
14
|
-
* leak) a fresh worker every time — the exact bug pooling exists to fix.
|
|
18
|
+
* `execute()` calls and avoids per-execution thread allocation.
|
|
15
19
|
*/
|
|
16
20
|
_engine = null;
|
|
17
21
|
_onAction;
|
|
18
22
|
constructor(options) {
|
|
19
23
|
this._registry = new PluginRegistry();
|
|
20
24
|
this._onAction = options.onAction;
|
|
25
|
+
if (options.connectionProvider && options.connections) {
|
|
26
|
+
throw new Error("Pass connections or connectionProvider, not both");
|
|
27
|
+
}
|
|
28
|
+
if (options.connectionProvider) {
|
|
29
|
+
this._connectionProvider = options.connectionProvider;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
this._memoryConnections = new MemoryConnectionProvider(options.connections);
|
|
33
|
+
this._connectionProvider = this._memoryConnections;
|
|
34
|
+
}
|
|
21
35
|
for (const pluginOrFn of options.plugins ?? []) {
|
|
22
36
|
const plugin = resolvePluginExport(pluginOrFn, "unknown");
|
|
23
37
|
this._registry.register(plugin);
|
|
24
38
|
}
|
|
25
39
|
this._config = {
|
|
26
|
-
connections:
|
|
40
|
+
connections: [],
|
|
27
41
|
timeoutMs: options.timeoutMs ?? DEFAULT_CONFIG.timeoutMs,
|
|
28
42
|
memoryLimitBytes: options.memoryLimitBytes ?? DEFAULT_CONFIG.memoryLimitBytes,
|
|
29
43
|
...(options.maxRunsPerWorker !== undefined
|
|
@@ -35,6 +49,7 @@ export class Runline {
|
|
|
35
49
|
if (!this._engine) {
|
|
36
50
|
this._engine = new ExecutionEngine(this._registry, this._config, {
|
|
37
51
|
onAction: this._onAction,
|
|
52
|
+
connectionProvider: this._connectionProvider,
|
|
38
53
|
});
|
|
39
54
|
}
|
|
40
55
|
return this._engine;
|
|
@@ -58,15 +73,14 @@ export class Runline {
|
|
|
58
73
|
/** Register an additional plugin after creation. */
|
|
59
74
|
addPlugin(pluginOrFn, connections) {
|
|
60
75
|
const plugin = resolvePluginExport(pluginOrFn, "unknown");
|
|
61
|
-
this._registry.register(plugin);
|
|
62
76
|
if (connections) {
|
|
63
|
-
this.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
if (!this._memoryConnections) {
|
|
78
|
+
throw new Error("Configure connections through the host connection provider");
|
|
79
|
+
}
|
|
80
|
+
this._memoryConnections.add(connections);
|
|
67
81
|
}
|
|
68
|
-
|
|
69
|
-
// the
|
|
82
|
+
this._registry.register(plugin);
|
|
83
|
+
// Rebuild the worker's action surface; connection custody survives disposal.
|
|
70
84
|
this.dispose();
|
|
71
85
|
}
|
|
72
86
|
/** List all available actions across all plugins. */
|
|
@@ -88,9 +102,12 @@ export class Runline {
|
|
|
88
102
|
connectionConfigSchema: p.connectionConfigSchema,
|
|
89
103
|
}));
|
|
90
104
|
}
|
|
91
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Snapshot of process-local connections. Host-managed providers are not
|
|
107
|
+
* enumerated: their accounts can be caller-scoped and must be listed by the host.
|
|
108
|
+
*/
|
|
92
109
|
connections() {
|
|
93
|
-
return
|
|
110
|
+
return this._memoryConnections?.list() ?? [];
|
|
94
111
|
}
|
|
95
112
|
/**
|
|
96
113
|
* Load runline from a project directory.
|
|
@@ -118,16 +135,17 @@ export class Runline {
|
|
|
118
135
|
const configDir = findRunlineDir(dir);
|
|
119
136
|
if (!configDir)
|
|
120
137
|
return null;
|
|
121
|
-
const config = loadConfigFrom(configDir);
|
|
138
|
+
const config = loadConfigFrom(join(configDir, "config.json"));
|
|
122
139
|
const builtinAllowlist = new Set(config.connections.map((c) => c.plugin));
|
|
123
140
|
const plugins = await discoverPlugins(configDir, {
|
|
124
141
|
builtinAllowlist,
|
|
125
142
|
builtinDir: options.builtinDir,
|
|
126
143
|
});
|
|
127
144
|
const rl = new Runline({
|
|
128
|
-
|
|
145
|
+
connectionProvider: new FileConnectionProvider(join(configDir, "config.json")),
|
|
129
146
|
timeoutMs: config.timeoutMs,
|
|
130
147
|
memoryLimitBytes: config.memoryLimitBytes,
|
|
148
|
+
maxRunsPerWorker: config.maxRunsPerWorker,
|
|
131
149
|
});
|
|
132
150
|
for (const plugin of plugins) {
|
|
133
151
|
rl._registry.register(plugin);
|
|
@@ -136,7 +154,7 @@ export class Runline {
|
|
|
136
154
|
}
|
|
137
155
|
}
|
|
138
156
|
function findRunlineDir(from) {
|
|
139
|
-
let dir = from;
|
|
157
|
+
let dir = resolve(from);
|
|
140
158
|
while (true) {
|
|
141
159
|
if (existsSync(join(dir, ".runline")))
|
|
142
160
|
return join(dir, ".runline");
|
|
@@ -147,15 +165,3 @@ function findRunlineDir(from) {
|
|
|
147
165
|
}
|
|
148
166
|
return null;
|
|
149
167
|
}
|
|
150
|
-
function loadConfigFrom(configDir) {
|
|
151
|
-
const configPath = join(configDir, "config.json");
|
|
152
|
-
if (!existsSync(configPath))
|
|
153
|
-
return { ...DEFAULT_CONFIG };
|
|
154
|
-
try {
|
|
155
|
-
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
156
|
-
return { ...DEFAULT_CONFIG, ...raw };
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
return { ...DEFAULT_CONFIG };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Observers cannot fail an operation, whether they throw or reject asynchronously. */
|
|
2
|
+
export function notifyObserver(observer, event) {
|
|
3
|
+
if (!observer)
|
|
4
|
+
return;
|
|
5
|
+
try {
|
|
6
|
+
void Promise.resolve(observer(event)).catch(() => { });
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
// Operational success does not depend on observability.
|
|
10
|
+
}
|
|
11
|
+
}
|