runline 0.20.0 → 0.22.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.
@@ -14,9 +14,11 @@ export interface EngineOptions {
14
14
  *
15
15
  * Fired only around `action.execute()` — an unknown path or a failed
16
16
  * input validation throws before plugin code runs and is not an
17
- * invocation. `error` is the thrown value, unmodified, when execute
18
- * rejected; the hook sees it in the host process even though the
19
- * worker only ever receives the message string.
17
+ * invocation. `error` is present exactly when execute rejected: the
18
+ * thrown value as-is, except a nullish throw, which is substituted
19
+ * with an Error so failure is never mistaken for success. The hook
20
+ * sees it in the host process even though the worker only ever
21
+ * receives the message string.
20
22
  */
21
23
  export interface ActionInvocation {
22
24
  plugin: string;
@@ -0,0 +1,118 @@
1
+ import { openAsBlob } from "node:fs";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { extname } from "node:path";
4
+ /**
5
+ * The Shift cloud signed-grant upload arc, shared by every plugin that
6
+ * moves local file bytes into the cloud (shiftTranscription assets,
7
+ * shiftObjects objects): stat + size-gate the file, create the server
8
+ * record with { contentType, sizeBytes }, PUT the bytes to the grant
9
+ * URL with the grant's headers, then POST /complete. The subtle parts
10
+ * live here once: the openAsBlob streaming fallback, grant-header
11
+ * merging, and the size ceilings.
12
+ *
13
+ * shiftOcr deliberately does not use this arc — it inlines small files
14
+ * as base64 data URLs instead of uploading through a grant.
15
+ */
16
+ /** Mirrors the public service contracts' 5 GiB upload ceiling. */
17
+ export const SIGNED_UPLOAD_MAX_BYTES = 5 * 1024 * 1024 * 1024;
18
+ /**
19
+ * Runtimes without fs.openAsBlob fall back to buffering; refuse to
20
+ * buffer files that would strain the host process.
21
+ */
22
+ const MAX_BUFFERED_BYTES = 512 * 1024 * 1024;
23
+ /** Audio and video formats the transcription service accepts. */
24
+ export const AUDIO_VIDEO_MEDIA_TYPES = {
25
+ aac: "audio/aac",
26
+ flac: "audio/flac",
27
+ m4a: "audio/m4a",
28
+ mov: "video/quicktime",
29
+ mp3: "audio/mpeg",
30
+ mp4: "video/mp4",
31
+ mpeg: "video/mpeg",
32
+ mpg: "video/mpeg",
33
+ oga: "audio/ogg",
34
+ ogg: "audio/ogg",
35
+ opus: "audio/ogg",
36
+ wav: "audio/wav",
37
+ wave: "audio/wav",
38
+ webm: "video/webm",
39
+ };
40
+ /** Common document, image, and media formats for durable objects. */
41
+ export const GENERAL_MEDIA_TYPES = {
42
+ csv: "text/csv",
43
+ doc: "application/msword",
44
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
45
+ gif: "image/gif",
46
+ html: "text/html",
47
+ jpeg: "image/jpeg",
48
+ jpg: "image/jpeg",
49
+ json: "application/json",
50
+ md: "text/markdown",
51
+ mp3: "audio/mpeg",
52
+ mp4: "video/mp4",
53
+ pdf: "application/pdf",
54
+ png: "image/png",
55
+ txt: "text/plain",
56
+ wav: "audio/wav",
57
+ webp: "image/webp",
58
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
59
+ zip: "application/zip",
60
+ };
61
+ /** Extension-based media type lookup; undefined when unknown. */
62
+ export function mediaTypeFromPath(path, types) {
63
+ return types[extname(path).slice(1).toLowerCase()];
64
+ }
65
+ /** Like {@link mediaTypeFromPath}, but unknown extensions are an error. */
66
+ export function requireMediaType(path, types) {
67
+ const mediaType = mediaTypeFromPath(path, types);
68
+ if (!mediaType) {
69
+ throw new Error(`Unsupported media extension for ${path}. Supported: ${Object.keys(types).join(", ")}`);
70
+ }
71
+ return mediaType;
72
+ }
73
+ /**
74
+ * Gate a local file before any network call: it must exist, be a real
75
+ * file, be non-empty, and fit the service ceiling. Returns its size.
76
+ * The label keeps each plugin's established error vocabulary
77
+ * ("File is empty" vs "Media file is empty").
78
+ */
79
+ export async function statUploadFile(path, maxBytes, label = "File") {
80
+ const file = await stat(path);
81
+ if (!file.isFile())
82
+ throw new Error(`${path} is not a file`);
83
+ if (file.size === 0)
84
+ throw new Error(`${label} is empty`);
85
+ if (file.size > maxBytes) {
86
+ throw new Error(`${label} is larger than the ${maxBytes}-byte service limit`);
87
+ }
88
+ return file.size;
89
+ }
90
+ async function fileBody(path, sizeBytes) {
91
+ try {
92
+ return await openAsBlob(path);
93
+ }
94
+ catch {
95
+ if (sizeBytes > MAX_BUFFERED_BYTES) {
96
+ throw new Error("This runtime cannot stream uploads and the file is too large to buffer.");
97
+ }
98
+ return await readFile(path);
99
+ }
100
+ }
101
+ /**
102
+ * PUT a local file's bytes to a signed grant. Grant headers win;
103
+ * content-type is filled in only when the grant leaves it unset.
104
+ */
105
+ export async function putThroughGrant(grant, path, contentType, sizeBytes, label = "Upload") {
106
+ const headers = new Headers(grant.headers);
107
+ if (!headers.has("content-type")) {
108
+ headers.set("content-type", contentType);
109
+ }
110
+ const response = await fetch(grant.url, {
111
+ method: grant.method,
112
+ headers,
113
+ body: await fileBody(path, sizeBytes),
114
+ });
115
+ if (!response.ok) {
116
+ throw new Error(`${label} failed: HTTP ${response.status}`);
117
+ }
118
+ }
@@ -0,0 +1,319 @@
1
+ import * as t from "typebox";
2
+ import { ATLAS_BASE, cursorSchema, enumSchema, GRAPH_ACTOR_KIND, GRAPH_ASSIGNMENT_ROLE, GRAPH_BINDING_EXECUTOR, GRAPH_BINDING_STATUS, GRAPH_CHANGE_ENTITY_TYPE, GRAPH_EVIDENCE_KIND, GRAPH_JOURNEY_STATUS, GRAPH_RELATION_KIND, GRAPH_TASK_MODE, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, withQuery, } from "./shared.js";
3
+ /**
4
+ * The operational graph behind the Atlas view: a business spec of how
5
+ * a company operates, in its own language. Four fixed levels — line →
6
+ * journey → stage → task — plus named actors, RACI assignments,
7
+ * level-typed relations, execution bindings, and evidence. Automation
8
+ * status is always derived from bindings, never declared.
9
+ */
10
+ const name = (description) => t.String({ minLength: 1, maxLength: 300, pattern: "\\S", description });
11
+ const longText = (description) => t.String({ maxLength: 5_000, description });
12
+ const position = t.Integer({
13
+ minimum: 0,
14
+ maximum: 100_000,
15
+ description: "Sort position within the parent (0-based)",
16
+ });
17
+ const urlField = t.String({
18
+ minLength: 1,
19
+ maxLength: 2_000,
20
+ description: "Link URL",
21
+ });
22
+ const referenceField = t.String({
23
+ minLength: 1,
24
+ maxLength: 1_000,
25
+ pattern: "\\S",
26
+ description: "External reference (system-specific identifier)",
27
+ });
28
+ export function registerGraphActions(rl) {
29
+ /* -------------------------------------------------------------- */
30
+ /* Reads */
31
+ /* -------------------------------------------------------------- */
32
+ rl.registerAction("summary", {
33
+ access: "read",
34
+ description: "The operational graph's structure without task bodies: every " +
35
+ "line, journey, and stage with rollups (total/liveAi/planned/" +
36
+ "human counts), plus actors and level labels. Small at any org " +
37
+ "size; the right default read.",
38
+ inputSchema: t.Object({}, STRICT_OBJECT),
39
+ async execute(_input, ctx) {
40
+ const body = await request(ctx, `${ATLAS_BASE}/summary`);
41
+ return body.summary;
42
+ },
43
+ });
44
+ rl.registerAction("graph.get", {
45
+ access: "read",
46
+ description: "The entire operational graph including every task body, " +
47
+ "assignment, binding, and evidence. Payload grows with task " +
48
+ "count — prefer summary plus stage.get.",
49
+ inputSchema: t.Object({}, STRICT_OBJECT),
50
+ async execute(_input, ctx) {
51
+ const body = await request(ctx, `${ATLAS_BASE}/graph`);
52
+ return body.graph;
53
+ },
54
+ });
55
+ rl.registerAction("stage.get", {
56
+ access: "read",
57
+ description: "One stage in full: its ordered tasks with participants, owner, " +
58
+ "bindings, evidence, and derived automation status.",
59
+ inputSchema: t.Object({ id: idSchema("Stage ID") }, STRICT_OBJECT),
60
+ async execute(input, ctx) {
61
+ const { id } = input;
62
+ const body = await request(ctx, `${ATLAS_BASE}/stages/${pathSegment(id)}`);
63
+ return body.stage;
64
+ },
65
+ });
66
+ rl.registerAction("actor.list", {
67
+ access: "read",
68
+ description: "List the named workers of the graph: agents and human roles.",
69
+ inputSchema: t.Object({}, STRICT_OBJECT),
70
+ async execute(_input, ctx) {
71
+ const body = await request(ctx, `${ATLAS_BASE}/actors`);
72
+ return body.actors;
73
+ },
74
+ });
75
+ rl.registerAction("line.list", {
76
+ access: "read",
77
+ description: "List business lines. Every organization has one default line; " +
78
+ "journeys created without a lineId land there.",
79
+ inputSchema: t.Object({}, STRICT_OBJECT),
80
+ async execute(_input, ctx) {
81
+ const body = await request(ctx, `${ATLAS_BASE}/lines`);
82
+ return body.lines;
83
+ },
84
+ });
85
+ rl.registerAction("journey.list", {
86
+ access: "read",
87
+ description: "List journeys (id, name, outcome, status, line).",
88
+ inputSchema: t.Object({}, STRICT_OBJECT),
89
+ async execute(_input, ctx) {
90
+ const body = await request(ctx, `${ATLAS_BASE}/journeys`);
91
+ return body.journeys;
92
+ },
93
+ });
94
+ rl.registerAction("journey.get", {
95
+ access: "read",
96
+ description: "One journey in full: its stages, their tasks, triggers, and rollups.",
97
+ inputSchema: t.Object({ id: idSchema("Journey ID") }, STRICT_OBJECT),
98
+ async execute(input, ctx) {
99
+ const { id } = input;
100
+ const body = await request(ctx, `${ATLAS_BASE}/journeys/${pathSegment(id)}`);
101
+ return body.journey;
102
+ },
103
+ });
104
+ rl.registerAction("labels.get", {
105
+ access: "read",
106
+ description: "What this organization calls each level (line/journey/stage/task).",
107
+ inputSchema: t.Object({}, STRICT_OBJECT),
108
+ async execute(_input, ctx) {
109
+ const body = await request(ctx, `${ATLAS_BASE}/labels`);
110
+ return body.labels;
111
+ },
112
+ });
113
+ rl.registerAction("change.list", {
114
+ access: "read",
115
+ description: "Append-only change log of the graph, newest first, cursor-" +
116
+ "paginated. Returns { changes, nextCursor? }.",
117
+ inputSchema: t.Object({
118
+ entityType: t.Optional(enumSchema("Entity type", GRAPH_CHANGE_ENTITY_TYPE)),
119
+ entityId: t.Optional(idSchema("Entity ID")),
120
+ cursor: t.Optional(cursorSchema()),
121
+ limit: t.Optional(t.Integer({ minimum: 1, maximum: 200 })),
122
+ }, STRICT_OBJECT),
123
+ async execute(input, ctx) {
124
+ return request(ctx, withQuery(`${ATLAS_BASE}/changes`, listParams(input)));
125
+ },
126
+ });
127
+ /* -------------------------------------------------------------- */
128
+ /* Mutations — each mirrors one service input schema. */
129
+ /* -------------------------------------------------------------- */
130
+ /**
131
+ * Sub-resource creates carry one routing key (journeyId, stageId,
132
+ * taskId) that belongs in the path, not the body. It is named as the
133
+ * key to omit — rather than allowlisting body fields — so a field
134
+ * added to the schema later flows through by default instead of
135
+ * being silently dropped.
136
+ */
137
+ function create(action, entity, path, description, fields, routingKey) {
138
+ rl.registerAction(action, {
139
+ access: "write",
140
+ description,
141
+ inputSchema: t.Object(fields, STRICT_OBJECT),
142
+ async execute(input, ctx) {
143
+ const all = input;
144
+ const body = { ...all };
145
+ if (routingKey)
146
+ delete body[routingKey];
147
+ const response = await request(ctx, path(all), { method: "POST", body: JSON.stringify(body) });
148
+ return response[entity];
149
+ },
150
+ });
151
+ }
152
+ function update(action, entity, collection, description, fields) {
153
+ rl.registerAction(action, {
154
+ access: "write",
155
+ description,
156
+ inputSchema: t.Object({ id: idSchema(`${entity} ID`), ...fields }, STRICT_UPDATE_OBJECT),
157
+ async execute(input, ctx) {
158
+ const { id, ...patch } = input;
159
+ const response = await request(ctx, `${ATLAS_BASE}/${collection}/${pathSegment(id)}`, { method: "PATCH", body: JSON.stringify(patch) });
160
+ return response[entity];
161
+ },
162
+ });
163
+ }
164
+ function remove(action, entity, collection, description) {
165
+ rl.registerAction(action, {
166
+ access: "write",
167
+ description,
168
+ inputSchema: t.Object({ id: idSchema(`${entity} ID`) }, STRICT_OBJECT),
169
+ async execute(input, ctx) {
170
+ const { id } = input;
171
+ await request(ctx, `${ATLAS_BASE}/${collection}/${pathSegment(id)}`, { method: "DELETE" });
172
+ return { deleted: true, id };
173
+ },
174
+ });
175
+ }
176
+ /* Actors */
177
+ create("actor.create", "actor", () => `${ATLAS_BASE}/actors`, "Create a named worker: an agent (e.g. Jarvis) or a human role (e.g. CFO).", {
178
+ name: name("Actor name"),
179
+ kind: enumSchema("Actor kind", GRAPH_ACTOR_KIND),
180
+ description: t.Optional(longText("What this actor does")),
181
+ });
182
+ update("actor.update", "actor", "actors", "Update an actor.", {
183
+ name: t.Optional(name("Actor name")),
184
+ kind: t.Optional(enumSchema("Actor kind", GRAPH_ACTOR_KIND)),
185
+ description: t.Optional(longText("What this actor does")),
186
+ });
187
+ remove("actor.delete", "actor", "actors", "Delete an actor. Refused (409 actor_in_use) while assignments reference it.");
188
+ /* Lines */
189
+ create("line.create", "line", () => `${ATLAS_BASE}/lines`, "Create a business line — the widest grouping of journeys.", {
190
+ name: name("Line name"),
191
+ blurb: t.Optional(longText("One-sentence description")),
192
+ position: t.Optional(position),
193
+ });
194
+ update("line.update", "line", "lines", "Update a business line.", {
195
+ name: t.Optional(name("Line name")),
196
+ blurb: t.Optional(longText("One-sentence description")),
197
+ position: t.Optional(position),
198
+ });
199
+ remove("line.delete", "line", "lines", "Delete a business line. Refused while journeys reference it (line_in_use) or it is the last line (last_line).");
200
+ /* Journeys */
201
+ create("journey.create", "journey", () => `${ATLAS_BASE}/journeys`, "Create a journey: an end-to-end flow with a business outcome. " +
202
+ "Omitting lineId places it in the organization's default line.", {
203
+ lineId: t.Optional(idSchema("Business line ID")),
204
+ name: name("Journey name"),
205
+ outcome: t.Optional(longText("The business outcome it delivers")),
206
+ position: t.Optional(position),
207
+ status: t.Optional(enumSchema("Journey status", GRAPH_JOURNEY_STATUS)),
208
+ });
209
+ update("journey.update", "journey", "journeys", "Update a journey.", {
210
+ lineId: t.Optional(idSchema("Business line ID")),
211
+ name: t.Optional(name("Journey name")),
212
+ outcome: t.Optional(longText("The business outcome it delivers")),
213
+ position: t.Optional(position),
214
+ status: t.Optional(enumSchema("Journey status", GRAPH_JOURNEY_STATUS)),
215
+ });
216
+ remove("journey.delete", "journey", "journeys", "Delete a journey and, in cascade, its stages and tasks.");
217
+ /* Stages */
218
+ create("stage.create", "stage", (input) => `${ATLAS_BASE}/journeys/${pathSegment(input.journeyId)}/stages`, "Create an ordered stage inside a journey.", {
219
+ journeyId: idSchema("Journey ID"),
220
+ name: name("Stage name"),
221
+ intent: t.Optional(longText("What this stage exists to ensure")),
222
+ position: t.Optional(position),
223
+ }, "journeyId");
224
+ update("stage.update", "stage", "stages", "Update a stage.", {
225
+ name: t.Optional(name("Stage name")),
226
+ intent: t.Optional(longText("What this stage exists to ensure")),
227
+ position: t.Optional(position),
228
+ });
229
+ remove("stage.delete", "stage", "stages", "Delete a stage and, in cascade, its tasks.");
230
+ /* Tasks */
231
+ create("task.create", "task", (input) => `${ATLAS_BASE}/stages/${pathSegment(input.stageId)}/tasks`, "Create a task — the atomic responsibility inside a stage. " +
232
+ "actorId also creates the matching `does` assignment (the owner).", {
233
+ stageId: idSchema("Stage ID"),
234
+ description: t.String({
235
+ minLength: 1,
236
+ maxLength: 2_000,
237
+ pattern: "\\S",
238
+ description: "What the work is, in the business's own words",
239
+ }),
240
+ position: t.Optional(position),
241
+ intendedMode: enumSchema("Intended mode", GRAPH_TASK_MODE),
242
+ actorId: t.Optional(idSchema("Owner actor ID")),
243
+ }, "stageId");
244
+ update("task.update", "task", "tasks", "Update a task. actorId replaces the `does` assignment; null clears the owner.", {
245
+ description: t.Optional(t.String({ minLength: 1, maxLength: 2_000, pattern: "\\S" })),
246
+ position: t.Optional(position),
247
+ intendedMode: t.Optional(enumSchema("Intended mode", GRAPH_TASK_MODE)),
248
+ actorId: t.Optional(t.Union([idSchema("Owner actor ID"), t.Null()])),
249
+ });
250
+ remove("task.delete", "task", "tasks", "Delete a task.");
251
+ /* Assignments */
252
+ create("assignment.create", "assignment", (input) => `${ATLAS_BASE}/tasks/${pathSegment(input.taskId)}/assignments`, "Add an actor to a task in a RACI role. `does` is the owner; a task has at most one.", {
253
+ taskId: idSchema("Task ID"),
254
+ actorId: idSchema("Actor ID"),
255
+ role: t.Optional(enumSchema("Assignment role", GRAPH_ASSIGNMENT_ROLE)),
256
+ }, "taskId");
257
+ remove("assignment.delete", "assignment", "assignments", "Remove an actor's part in a task.");
258
+ /* Relations */
259
+ create("relation.create", "relation", () => `${ATLAS_BASE}/relations`, "Create a level-typed link: `follows` joins two tasks in one stage " +
260
+ "(cycles rejected); `triggers` joins two journeys (cycles allowed).", {
261
+ kind: enumSchema("Relation kind", GRAPH_RELATION_KIND),
262
+ fromId: idSchema("Source task or journey ID"),
263
+ toId: idSchema("Target task or journey ID"),
264
+ });
265
+ remove("relation.delete", "relation", "relations", "Delete a relation.");
266
+ /* Bindings */
267
+ create("binding.create", "binding", (input) => `${ATLAS_BASE}/tasks/${pathSegment(input.taskId)}/bindings`, "Bind a task to how it is executed: a Vex deployment, an external " +
268
+ "system, or manual work. A live non-manual binding is what makes " +
269
+ "a task count as live AI.", {
270
+ taskId: idSchema("Task ID"),
271
+ executor: enumSchema("Executor", GRAPH_BINDING_EXECUTOR),
272
+ label: name("Human-readable binding label"),
273
+ status: t.Optional(enumSchema("Binding status", GRAPH_BINDING_STATUS)),
274
+ deploymentId: t.Optional(idSchema("Vex deployment ID")),
275
+ reference: t.Optional(referenceField),
276
+ url: t.Optional(urlField),
277
+ }, "taskId");
278
+ update("binding.update", "binding", "bindings", "Update a binding; null clears deploymentId, reference, or url.", {
279
+ executor: t.Optional(enumSchema("Executor", GRAPH_BINDING_EXECUTOR)),
280
+ label: t.Optional(name("Human-readable binding label")),
281
+ status: t.Optional(enumSchema("Binding status", GRAPH_BINDING_STATUS)),
282
+ deploymentId: t.Optional(t.Union([idSchema("Vex deployment ID"), t.Null()])),
283
+ reference: t.Optional(t.Union([referenceField, t.Null()])),
284
+ url: t.Optional(t.Union([urlField, t.Null()])),
285
+ });
286
+ remove("binding.delete", "binding", "bindings", "Delete a binding.");
287
+ /* Evidence */
288
+ create("evidence.create", "evidence", (input) => `${ATLAS_BASE}/tasks/${pathSegment(input.taskId)}/evidence`, "Attach proof the work happens: a report, artifact, or dashboard. " +
289
+ "Requires url or artifactRef.", {
290
+ taskId: idSchema("Task ID"),
291
+ title: name("Evidence title"),
292
+ kind: enumSchema("Evidence kind", GRAPH_EVIDENCE_KIND),
293
+ url: t.Optional(urlField),
294
+ artifactRef: t.Optional(referenceField),
295
+ }, "taskId");
296
+ update("evidence.update", "evidence", "evidence", "Update evidence; null clears url or artifactRef.", {
297
+ title: t.Optional(name("Evidence title")),
298
+ kind: t.Optional(enumSchema("Evidence kind", GRAPH_EVIDENCE_KIND)),
299
+ url: t.Optional(t.Union([urlField, t.Null()])),
300
+ artifactRef: t.Optional(t.Union([referenceField, t.Null()])),
301
+ });
302
+ remove("evidence.delete", "evidence", "evidence", "Delete an evidence entry.");
303
+ /* Labels */
304
+ rl.registerAction("labels.update", {
305
+ access: "write",
306
+ description: "Rename what this organization calls each level — display only, " +
307
+ "no structural consequence.",
308
+ inputSchema: t.Object({
309
+ line: t.Optional(name("Label for the line level")),
310
+ journey: t.Optional(name("Label for the journey level")),
311
+ stage: t.Optional(name("Label for the stage level")),
312
+ task: t.Optional(name("Label for the task level")),
313
+ }, { additionalProperties: false, minProperties: 1 }),
314
+ async execute(input, ctx) {
315
+ const body = await request(ctx, `${ATLAS_BASE}/labels`, { method: "PATCH", body: JSON.stringify(input) });
316
+ return body.labels;
317
+ },
318
+ });
319
+ }
@@ -0,0 +1,22 @@
1
+ import * as t from "typebox";
2
+ import { registerGraphActions } from "./graph.js";
3
+ /**
4
+ * Shift Atlas — the operational graph of a Shift cloud organization:
5
+ * the map of how the business runs. Independent of the shiftLabs
6
+ * plugin the same way shiftCrm is, but authenticated with the same
7
+ * Shift cloud API key; the cloud derives the organization from the
8
+ * key, so no organization ID is ever sent (SHFT-852).
9
+ */
10
+ export default function shiftAtlas(rl) {
11
+ rl.setName("shiftAtlas");
12
+ rl.setVersion("0.1.0");
13
+ rl.setConnectionSchema(t.Object({
14
+ // Same Shift Labs API key family as the shiftLabs plugin; needs
15
+ // the service:operational-graph:use scope.
16
+ apiKey: t.String({
17
+ description: "Shift Labs API key",
18
+ env: "SHIFT_LABS_API_KEY",
19
+ }),
20
+ }));
21
+ registerGraphActions(rl);
22
+ }
@@ -0,0 +1,28 @@
1
+ export { cursorSchema, enumSchema, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, withQuery, } from "../../_shared/shiftCloud.js";
2
+ /** Base path of the operational graph service in the Shift cloud API. */
3
+ export const ATLAS_BASE = "/v1/services/operational-graph";
4
+ export const GRAPH_ACTOR_KIND = ["agent", "human_role"];
5
+ export const GRAPH_JOURNEY_STATUS = ["draft", "published", "retired"];
6
+ export const GRAPH_ASSIGNMENT_ROLE = [
7
+ "does",
8
+ "approves",
9
+ "consulted",
10
+ "informed",
11
+ ];
12
+ export const GRAPH_RELATION_KIND = ["follows", "triggers"];
13
+ export const GRAPH_TASK_MODE = ["ai", "human", "hybrid"];
14
+ export const GRAPH_BINDING_EXECUTOR = ["vex", "external", "manual"];
15
+ export const GRAPH_BINDING_STATUS = ["planned", "live", "paused"];
16
+ export const GRAPH_EVIDENCE_KIND = ["report", "artifact", "dashboard"];
17
+ export const GRAPH_CHANGE_ENTITY_TYPE = [
18
+ "actor",
19
+ "line",
20
+ "journey",
21
+ "stage",
22
+ "task",
23
+ "assignment",
24
+ "relation",
25
+ "binding",
26
+ "evidence",
27
+ "labels",
28
+ ];
@@ -0,0 +1,20 @@
1
+ import * as t from "typebox";
2
+ import { registerObjectActions } from "./objects.js";
3
+ /**
4
+ * Shift Objects — the organization's durable object bucket in the
5
+ * Shift cloud: upload binary files through signed grants, mint
6
+ * download grants, and record append-only provenance links. The API
7
+ * key is the tenant authority; the cloud derives the organization from
8
+ * it (SHFT-852), so no org ID is sent.
9
+ */
10
+ export default function shiftObjects(rl) {
11
+ rl.setName("shiftObjects");
12
+ rl.setVersion("0.1.0");
13
+ rl.setConnectionSchema(t.Object({
14
+ apiKey: t.String({
15
+ description: "Shift Labs API key",
16
+ env: "SHIFT_LABS_API_KEY",
17
+ }),
18
+ }));
19
+ registerObjectActions(rl);
20
+ }
@@ -1,8 +1,8 @@
1
- import { openAsBlob } from "node:fs";
2
- import { readFile, stat, writeFile } from "node:fs/promises";
3
- import { basename, extname } from "node:path";
1
+ import { writeFile } from "node:fs/promises";
2
+ import { basename } from "node:path";
4
3
  import * as t from "typebox";
5
- import { enumSchema, pathSegment, request } from "./shared.js";
4
+ import { enumSchema, listParams, pathSegment, request, withQuery, } from "../../_shared/shiftCloud.js";
5
+ import { GENERAL_MEDIA_TYPES, mediaTypeFromPath, putThroughGrant, SIGNED_UPLOAD_MAX_BYTES, statUploadFile, } from "../../_shared/shiftUpload.js";
6
6
  const OBJECTS_BASE = "/v1/services/objects";
7
7
  export const OBJECT_STATUS = ["pending_upload", "ready", "archived"];
8
8
  export const OBJECT_LINK_TARGET = [
@@ -11,49 +11,8 @@ export const OBJECT_LINK_TARGET = [
11
11
  "eval_case",
12
12
  "db_record",
13
13
  ];
14
- /** Mirrors the public service contract's 5 GiB object ceiling. */
15
- const MAX_OBJECT_BYTES = 5 * 1024 * 1024 * 1024;
16
14
  const STRICT_OBJECT = { additionalProperties: false };
17
15
  const Id = t.String({ minLength: 1, pattern: "\\S" });
18
- const MEDIA_TYPES = {
19
- csv: "text/csv",
20
- doc: "application/msword",
21
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
22
- gif: "image/gif",
23
- html: "text/html",
24
- jpeg: "image/jpeg",
25
- jpg: "image/jpeg",
26
- json: "application/json",
27
- md: "text/markdown",
28
- mp3: "audio/mpeg",
29
- mp4: "video/mp4",
30
- pdf: "application/pdf",
31
- png: "image/png",
32
- txt: "text/plain",
33
- wav: "audio/wav",
34
- webp: "image/webp",
35
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
36
- zip: "application/zip",
37
- };
38
- function mediaTypeFor(path, override) {
39
- if (override)
40
- return override;
41
- return (MEDIA_TYPES[extname(path).slice(1).toLowerCase()] ??
42
- "application/octet-stream");
43
- }
44
- async function fileBody(path, sizeBytes) {
45
- try {
46
- return await openAsBlob(path);
47
- }
48
- catch {
49
- // Runtimes without fs.openAsBlob fall back to buffering; refuse to
50
- // buffer files that would strain the host process.
51
- if (sizeBytes > 512 * 1024 * 1024) {
52
- throw new Error("This runtime cannot stream uploads and the file is too large to buffer.");
53
- }
54
- return await readFile(path);
55
- }
56
- }
57
16
  function linkSchema() {
58
17
  return t.Object({
59
18
  targetType: enumSchema("Link target type", OBJECT_LINK_TARGET),
@@ -108,21 +67,16 @@ export function registerObjectActions(rl) {
108
67
  }, STRICT_OBJECT),
109
68
  async execute(input, ctx) {
110
69
  const fields = input;
111
- const contentType = mediaTypeFor(fields.path, fields.contentType);
112
- const file = await stat(fields.path);
113
- if (!file.isFile())
114
- throw new Error(`${fields.path} is not a file`);
115
- if (file.size === 0)
116
- throw new Error("File is empty");
117
- if (file.size > MAX_OBJECT_BYTES) {
118
- throw new Error(`File is larger than the ${MAX_OBJECT_BYTES}-byte service limit`);
119
- }
70
+ const contentType = fields.contentType ??
71
+ mediaTypeFromPath(fields.path, GENERAL_MEDIA_TYPES) ??
72
+ "application/octet-stream";
73
+ const sizeBytes = await statUploadFile(fields.path, SIGNED_UPLOAD_MAX_BYTES);
120
74
  const created = await request(ctx, `${OBJECTS_BASE}/objects`, {
121
75
  method: "POST",
122
76
  // The API key is the tenant authority; no organizationId is sent.
123
77
  body: JSON.stringify({
124
78
  contentType,
125
- sizeBytes: file.size,
79
+ sizeBytes,
126
80
  filename: fields.filename ?? basename(fields.path),
127
81
  workspaceId: fields.workspaceId,
128
82
  sessionId: fields.sessionId,
@@ -130,18 +84,7 @@ export function registerObjectActions(rl) {
130
84
  metadata: fields.metadata,
131
85
  }),
132
86
  });
133
- const uploadHeaders = new Headers(created.upload.headers);
134
- if (!uploadHeaders.has("content-type")) {
135
- uploadHeaders.set("content-type", contentType);
136
- }
137
- const upload = await fetch(created.upload.url, {
138
- method: created.upload.method,
139
- headers: uploadHeaders,
140
- body: await fileBody(fields.path, file.size),
141
- });
142
- if (!upload.ok) {
143
- throw new Error(`Object upload failed: HTTP ${upload.status}`);
144
- }
87
+ await putThroughGrant(created.upload, fields.path, contentType, sizeBytes, "Object upload");
145
88
  return request(ctx, `${OBJECTS_BASE}/objects/${pathSegment(created.object.id)}/complete`, { method: "POST" });
146
89
  },
147
90
  });
@@ -155,13 +98,7 @@ export function registerObjectActions(rl) {
155
98
  ...provenanceFields,
156
99
  }, STRICT_OBJECT),
157
100
  async execute(input, ctx) {
158
- const params = new URLSearchParams();
159
- for (const [key, value] of Object.entries(input)) {
160
- if (value !== undefined)
161
- params.set(key, String(value));
162
- }
163
- const query = params.toString();
164
- const body = await request(ctx, `${OBJECTS_BASE}/objects${query ? `?${query}` : ""}`);
101
+ const body = await request(ctx, withQuery(`${OBJECTS_BASE}/objects`, listParams(input)));
165
102
  return body.objects;
166
103
  },
167
104
  });
@@ -1,11 +1,17 @@
1
- import { readFile, stat } from "node:fs/promises";
2
- import { extname } from "node:path";
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename, extname } from "node:path";
3
3
  import * as t from "typebox";
4
4
  import { request, STRICT_OBJECT } from "../../_shared/shiftCloud.js";
5
+ import { putThroughGrant, SIGNED_UPLOAD_MAX_BYTES, statUploadFile, } from "../../_shared/shiftUpload.js";
5
6
  /**
6
7
  * Shift OCR — text and structured-field extraction from images and
7
8
  * PDFs through the Shift cloud OCR service. The API key is the tenant
8
9
  * authority; no organization ID is ever sent (SHFT-852).
10
+ *
11
+ * Local files route by size: small ones inline as base64 data URLs
12
+ * (one round-trip); larger ones ride the signed-grant arc into the
13
+ * organization's durable object bucket and extract by objectId
14
+ * (SHFT-924), so bytes go to S3, never through the API.
9
15
  */
10
16
  const IMAGE_TYPES = {
11
17
  avif: "image/avif",
@@ -20,10 +26,11 @@ const DOCUMENT_TYPES = {
20
26
  };
21
27
  /**
22
28
  * Raw-file ceiling for inlining as a base64 data URL. Base64 inflates
23
- * by 4/3, so 20 MiB raw stays well inside provider request limits.
29
+ * by 4/3, so 20 MiB raw stays well inside provider request limits;
30
+ * anything larger uploads to the object bucket instead.
24
31
  */
25
- const MAX_FILE_BYTES = 20 * 1024 * 1024;
26
- async function documentFromPath(path) {
32
+ const MAX_INLINE_BYTES = 20 * 1024 * 1024;
33
+ async function documentFromPath(ctx, path) {
27
34
  const extension = extname(path).slice(1).toLowerCase();
28
35
  const imageType = IMAGE_TYPES[extension];
29
36
  const documentType = DOCUMENT_TYPES[extension];
@@ -33,19 +40,27 @@ async function documentFromPath(path) {
33
40
  ...Object.keys(DOCUMENT_TYPES),
34
41
  ].join(", ")}`);
35
42
  }
36
- const file = await stat(path);
37
- if (!file.isFile())
38
- throw new Error(`${path} is not a file`);
39
- if (file.size === 0)
40
- throw new Error("File is empty");
41
- if (file.size > MAX_FILE_BYTES) {
42
- throw new Error(`File is ${file.size} bytes; inline OCR uploads are capped at ${MAX_FILE_BYTES} bytes. Host the file and pass url instead.`);
43
+ const mediaType = (imageType ?? documentType);
44
+ const sizeBytes = await statUploadFile(path, SIGNED_UPLOAD_MAX_BYTES);
45
+ if (sizeBytes <= MAX_INLINE_BYTES) {
46
+ const url = `data:${mediaType};base64,${(await readFile(path)).toString("base64")}`;
47
+ return imageType
48
+ ? { type: "image", url }
49
+ : { type: "document", url, name: basename(path) };
43
50
  }
44
- const mediaType = imageType ?? documentType;
45
- const url = `data:${mediaType};base64,${(await readFile(path)).toString("base64")}`;
46
- return imageType
47
- ? { type: "image", url }
48
- : { type: "document", url, name: path.replace(/.*\//, "") };
51
+ // Large file: into the durable bucket through a signed grant, then
52
+ // extract by reference — the object persists as provenance.
53
+ const created = await request(ctx, "/v1/services/objects/objects", {
54
+ method: "POST",
55
+ body: JSON.stringify({
56
+ contentType: mediaType,
57
+ sizeBytes,
58
+ filename: basename(path),
59
+ }),
60
+ });
61
+ await putThroughGrant(created.upload, path, mediaType, sizeBytes);
62
+ await request(ctx, `/v1/services/objects/objects/${created.object.id}/complete`, { method: "POST" });
63
+ return { type: "object", objectId: created.object.id };
49
64
  }
50
65
  function documentFromUrl(url, kind) {
51
66
  if (kind)
@@ -75,9 +90,10 @@ export default function shiftOcr(rl) {
75
90
  rl.registerAction("ocr.extract", {
76
91
  access: "write",
77
92
  description: "Extract markdown text — and optionally schema-constrained JSON — " +
78
- "from an image or PDF. Pass a local file path (inlined as a data " +
79
- "URL) or an https URL. Provide schema to get structured fields " +
80
- "back instead of parsing the text yourself.",
93
+ "from an image or PDF. Pass a local file path (small files inline; " +
94
+ "large ones upload to the org's object bucket automatically), an " +
95
+ "https URL, or the objectId of a stored object. Provide schema to " +
96
+ "get structured fields back instead of parsing the text yourself.",
81
97
  inputSchema: t.Object({
82
98
  path: t.Optional(t.String({
83
99
  minLength: 1,
@@ -89,6 +105,11 @@ export default function shiftOcr(rl) {
89
105
  pattern: "\\S",
90
106
  description: "https or data URL of the image or PDF",
91
107
  })),
108
+ objectId: t.Optional(t.String({
109
+ minLength: 1,
110
+ pattern: "\\S",
111
+ description: "ID of a ready object in the organization's durable bucket",
112
+ })),
92
113
  kind: t.Optional(t.Union([t.Literal("image"), t.Literal("document")], {
93
114
  description: "Override for URLs whose extension does not reveal the type. " +
94
115
  "Inferred from the extension otherwise.",
@@ -114,15 +135,18 @@ export default function shiftOcr(rl) {
114
135
  }, STRICT_OBJECT),
115
136
  async execute(input, ctx) {
116
137
  const fields = input;
117
- if (!fields.path === !fields.url) {
118
- throw new Error("Provide exactly one of path or url");
138
+ const sources = [fields.path, fields.url, fields.objectId].filter((value) => value !== undefined);
139
+ if (sources.length !== 1) {
140
+ throw new Error("Provide exactly one of path, url, or objectId");
119
141
  }
120
142
  if (fields.prompt && !fields.schema) {
121
143
  throw new Error("prompt requires schema");
122
144
  }
123
- const document = fields.path
124
- ? await documentFromPath(fields.path)
125
- : documentFromUrl(fields.url, fields.kind);
145
+ const document = fields.objectId
146
+ ? { type: "object", objectId: fields.objectId }
147
+ : fields.path
148
+ ? await documentFromPath(ctx, fields.path)
149
+ : documentFromUrl(fields.url, fields.kind);
126
150
  return await request(ctx, "/v1/services/ocr/extract", {
127
151
  method: "POST",
128
152
  body: JSON.stringify({
@@ -0,0 +1,19 @@
1
+ import * as t from "typebox";
2
+ import { registerPageActions } from "./pages.js";
3
+ /**
4
+ * Shift Pages — hosted HTML pages in the Shift cloud: draft, publish,
5
+ * archive, share with invited viewers, and resolve authenticated
6
+ * render URLs. The API key is the tenant authority; the cloud derives
7
+ * the organization from it (SHFT-852), so no org ID is sent.
8
+ */
9
+ export default function shiftPages(rl) {
10
+ rl.setName("shiftPages");
11
+ rl.setVersion("0.1.0");
12
+ rl.setConnectionSchema(t.Object({
13
+ apiKey: t.String({
14
+ description: "Shift Labs API key",
15
+ env: "SHIFT_LABS_API_KEY",
16
+ }),
17
+ }));
18
+ registerPageActions(rl);
19
+ }
@@ -1,5 +1,5 @@
1
1
  import * as t from "typebox";
2
- import { enumSchema, idSchema, PAGE_STATUS, PAGE_VISIBILITY, pageRenderUrl, pathSegment, request, STRICT_OBJECT, timestampSchema, } from "./shared.js";
2
+ import { enumSchema, idSchema, listParams, PAGE_STATUS, PAGE_VISIBILITY, pageRenderUrl, pathSegment, request, STRICT_OBJECT, timestampSchema, withQuery, } from "./shared.js";
3
3
  const PageSlug = t.String({
4
4
  minLength: 1,
5
5
  maxLength: 120,
@@ -19,13 +19,7 @@ export function registerPageActions(rl) {
19
19
  })),
20
20
  }, STRICT_OBJECT),
21
21
  async execute(input, ctx) {
22
- const fields = (input ?? {});
23
- const params = new URLSearchParams();
24
- if (fields.status)
25
- params.set("status", String(fields.status));
26
- if (fields.limit)
27
- params.set("limit", String(fields.limit));
28
- const body = await request(ctx, `/v1/pages?${params}`);
22
+ const body = await request(ctx, withQuery("/v1/pages", listParams(input)));
29
23
  return body.pages;
30
24
  },
31
25
  });
@@ -0,0 +1,7 @@
1
+ import { baseUrl, pathSegment } from "../../_shared/shiftCloud.js";
2
+ export { enumSchema, idSchema, listParams, pathSegment, request, STRICT_OBJECT, timestampSchema, withQuery, } from "../../_shared/shiftCloud.js";
3
+ export const PAGE_STATUS = ["draft", "published", "archived"];
4
+ export const PAGE_VISIBILITY = ["org", "invited"];
5
+ export function pageRenderUrl(organizationId, slug) {
6
+ return new URL(`/pages/${pathSegment(organizationId)}/${pathSegment(slug)}`, baseUrl()).toString();
7
+ }
@@ -0,0 +1,20 @@
1
+ import * as t from "typebox";
2
+ import { registerTranscriptionActions } from "./transcription.js";
3
+ /**
4
+ * Shift Transcription — audio and video transcription through the
5
+ * Shift cloud: upload media, run jobs (optionally diarized), and read
6
+ * transcripts as text, subtitles, or timestamped JSON. The API key is
7
+ * the tenant authority; the cloud derives the organization from it
8
+ * (SHFT-852), so no org ID is sent.
9
+ */
10
+ export default function shiftTranscription(rl) {
11
+ rl.setName("shiftTranscription");
12
+ rl.setVersion("0.1.0");
13
+ rl.setConnectionSchema(t.Object({
14
+ apiKey: t.String({
15
+ description: "Shift Labs API key",
16
+ env: "SHIFT_LABS_API_KEY",
17
+ }),
18
+ }));
19
+ registerTranscriptionActions(rl);
20
+ }
@@ -0,0 +1,3 @@
1
+ export { enumSchema, pathSegment, request, } from "../../_shared/shiftCloud.js";
2
+ export const TRANSCRIPTION_LANGUAGE = ["auto", "en", "he"];
3
+ export const TRANSCRIPT_FORMAT = ["txt", "srt", "vtt", "json"];
@@ -1,50 +1,10 @@
1
- import { openAsBlob } from "node:fs";
2
- import { readFile, stat } from "node:fs/promises";
3
- import { extname } from "node:path";
4
1
  import * as t from "typebox";
2
+ import { AUDIO_VIDEO_MEDIA_TYPES, putThroughGrant, requireMediaType, SIGNED_UPLOAD_MAX_BYTES, statUploadFile, } from "../../_shared/shiftUpload.js";
5
3
  import { enumSchema, pathSegment, request, TRANSCRIPT_FORMAT, TRANSCRIPTION_LANGUAGE, } from "./shared.js";
6
- const MEDIA_TYPES = {
7
- aac: "audio/aac",
8
- flac: "audio/flac",
9
- m4a: "audio/m4a",
10
- mov: "video/quicktime",
11
- mp3: "audio/mpeg",
12
- mp4: "video/mp4",
13
- mpeg: "video/mpeg",
14
- mpg: "video/mpeg",
15
- oga: "audio/ogg",
16
- ogg: "audio/ogg",
17
- opus: "audio/ogg",
18
- wav: "audio/wav",
19
- wave: "audio/wav",
20
- webm: "video/webm",
21
- };
22
- /** Mirrors the public service contract's 5 GiB source-object ceiling. */
23
- const MAX_MEDIA_BYTES = 5 * 1024 * 1024 * 1024;
24
4
  /** Transcript previews returned into the sandbox are capped, not truncated silently. */
25
5
  const MAX_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
26
6
  const STRICT_OBJECT = { additionalProperties: false };
27
7
  const Id = t.String({ minLength: 1, pattern: "\\S" });
28
- function mediaTypeFor(path) {
29
- const mediaType = MEDIA_TYPES[extname(path).slice(1).toLowerCase()];
30
- if (!mediaType) {
31
- throw new Error(`Unsupported media extension for ${path}. Supported: ${Object.keys(MEDIA_TYPES).join(", ")}`);
32
- }
33
- return mediaType;
34
- }
35
- async function fileBody(path, sizeBytes) {
36
- try {
37
- return await openAsBlob(path);
38
- }
39
- catch {
40
- // Runtimes without fs.openAsBlob fall back to buffering; refuse to
41
- // buffer files that would strain the host process.
42
- if (sizeBytes > 512 * 1024 * 1024) {
43
- throw new Error("This runtime cannot stream uploads and the file is too large to buffer.");
44
- }
45
- return await readFile(path);
46
- }
47
- }
48
8
  export function registerTranscriptionActions(rl) {
49
9
  rl.registerAction("transcription.transcribe", {
50
10
  access: "write",
@@ -76,35 +36,17 @@ export function registerTranscriptionActions(rl) {
76
36
  }, STRICT_OBJECT),
77
37
  async execute(input, ctx) {
78
38
  const fields = input;
79
- const contentType = mediaTypeFor(fields.path);
80
- const file = await stat(fields.path);
81
- if (!file.isFile())
82
- throw new Error(`${fields.path} is not a file`);
83
- if (file.size === 0)
84
- throw new Error("Media file is empty");
85
- if (file.size > MAX_MEDIA_BYTES) {
86
- throw new Error(`Media file is larger than the ${MAX_MEDIA_BYTES}-byte service limit`);
87
- }
39
+ const contentType = requireMediaType(fields.path, AUDIO_VIDEO_MEDIA_TYPES);
40
+ const sizeBytes = await statUploadFile(fields.path, SIGNED_UPLOAD_MAX_BYTES, "Media file");
88
41
  const created = await request(ctx, "/v1/services/transcription/assets", {
89
42
  method: "POST",
90
43
  // The API key is the tenant authority; no organizationId is sent.
91
44
  body: JSON.stringify({
92
45
  contentType,
93
- sizeBytes: file.size,
46
+ sizeBytes,
94
47
  }),
95
48
  });
96
- const uploadHeaders = new Headers(created.upload.headers);
97
- if (!uploadHeaders.has("content-type")) {
98
- uploadHeaders.set("content-type", contentType);
99
- }
100
- const upload = await fetch(created.upload.url, {
101
- method: created.upload.method,
102
- headers: uploadHeaders,
103
- body: await fileBody(fields.path, file.size),
104
- });
105
- if (!upload.ok) {
106
- throw new Error(`Media upload failed: HTTP ${upload.status}`);
107
- }
49
+ await putThroughGrant(created.upload, fields.path, contentType, sizeBytes, "Media upload");
108
50
  await request(ctx, `/v1/services/transcription/assets/${pathSegment(created.asset.id)}/complete`, { method: "POST" });
109
51
  const job = await request(ctx, "/v1/services/transcription/jobs", {
110
52
  method: "POST",
@@ -1,16 +1,17 @@
1
1
  import * as t from "typebox";
2
2
  import { registerIssueViewActions } from "./issue-views.js";
3
3
  import { registerIssueActions } from "./issues.js";
4
- import { registerObjectActions } from "./objects.js";
5
- import { registerPageActions } from "./pages.js";
6
4
  import { registerProjectActions } from "./projects.js";
7
- import { registerTranscriptionActions } from "./transcription.js";
8
- export default function shiftLabs(rl) {
9
- rl.setName("shiftLabs");
5
+ /**
6
+ * Shift Work — the work-tracking domain of the Shift cloud: Projects,
7
+ * Issues (with dependencies and comments), and saved Issue Views.
8
+ * The API key is the tenant authority for every action; the cloud
9
+ * derives the organization from it (SHFT-852), so no org ID is sent.
10
+ */
11
+ export default function shiftWork(rl) {
12
+ rl.setName("shiftWork");
10
13
  rl.setVersion("0.1.0");
11
14
  rl.setConnectionSchema(t.Object({
12
- // The API key is the tenant authority for every action; the cloud
13
- // derives the organization from it (SHFT-852), so no org ID is needed.
14
15
  apiKey: t.String({
15
16
  description: "Shift Labs API key",
16
17
  env: "SHIFT_LABS_API_KEY",
@@ -19,7 +20,4 @@ export default function shiftLabs(rl) {
19
20
  registerProjectActions(rl);
20
21
  registerIssueActions(rl);
21
22
  registerIssueViewActions(rl);
22
- registerPageActions(rl);
23
- registerTranscriptionActions(rl);
24
- registerObjectActions(rl);
25
23
  }
@@ -1,5 +1,5 @@
1
1
  import * as t from "typebox";
2
- import { cursorSchema, enumSchema, ISSUE_PRIORITY, ISSUE_STATUS, ISSUE_VIEW_GROUP, ISSUE_VIEW_LAYOUT, ISSUE_VIEW_SORT, ISSUE_VIEW_VISIBILITY, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "./shared.js";
2
+ import { cursorSchema, enumSchema, ISSUE_PRIORITY, ISSUE_STATUS, ISSUE_VIEW_GROUP, ISSUE_VIEW_LAYOUT, ISSUE_VIEW_SORT, ISSUE_VIEW_VISIBILITY, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, withQuery, } from "./shared.js";
3
3
  const viewFields = {
4
4
  name: t.String({
5
5
  minLength: 1,
@@ -143,18 +143,6 @@ export function registerIssueViewActions(rl) {
143
143
  },
144
144
  });
145
145
  }
146
- function listParams(input) {
147
- const params = new URLSearchParams();
148
- for (const [key, value] of Object.entries((input ?? {}))) {
149
- if (value !== undefined)
150
- params.set(key, String(value));
151
- }
152
- return params;
153
- }
154
- function withQuery(path, params) {
155
- const query = params.toString();
156
- return query ? `${path}?${query}` : path;
157
- }
158
146
  function assertDateOrder(startAfter, dueBefore) {
159
147
  if (typeof startAfter === "string" &&
160
148
  typeof dueBefore === "string" &&
@@ -1,5 +1,5 @@
1
1
  import * as t from "typebox";
2
- import { cursorSchema, enumSchema, ISSUE_PRIORITY, ISSUE_SOURCE, ISSUE_STATUS, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "./shared.js";
2
+ import { cursorSchema, enumSchema, ISSUE_PRIORITY, ISSUE_SOURCE, ISSUE_STATUS, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, withQuery, } from "./shared.js";
3
3
  const issueListFields = {
4
4
  status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
5
5
  projectId: t.Optional(idSchema("Project ID")),
@@ -14,25 +14,13 @@ const issueListFields = {
14
14
  description: "Max results, default 50",
15
15
  })),
16
16
  };
17
- function issueListParams(input) {
18
- const params = new URLSearchParams();
19
- for (const [key, value] of Object.entries((input ?? {}))) {
20
- if (value !== undefined)
21
- params.set(key, String(value));
22
- }
23
- return params;
24
- }
25
- function withQuery(path, params) {
26
- const query = params.toString();
27
- return query ? `${path}?${query}` : path;
28
- }
29
17
  export function registerIssueActions(rl) {
30
18
  rl.registerAction("issue.list", {
31
19
  access: "read",
32
20
  description: "List the first page of Shift Labs Issues for the API key's organization.",
33
21
  inputSchema: t.Object(issueListFields, STRICT_OBJECT),
34
22
  async execute(input, ctx) {
35
- const body = await request(ctx, `/v1/issues?${issueListParams(input)}`);
23
+ const body = await request(ctx, `/v1/issues?${listParams(input)}`);
36
24
  return body.issues;
37
25
  },
38
26
  });
@@ -41,7 +29,7 @@ export function registerIssueActions(rl) {
41
29
  description: "List a cursor-paginated page of Shift Labs Issues.",
42
30
  inputSchema: t.Object(issueListFields, STRICT_OBJECT),
43
31
  async execute(input, ctx) {
44
- return request(ctx, `/v1/issues?${issueListParams(input)}`);
32
+ return request(ctx, `/v1/issues?${listParams(input)}`);
45
33
  },
46
34
  });
47
35
  rl.registerAction("issue.get", {
@@ -148,7 +136,7 @@ export function registerIssueActions(rl) {
148
136
  }, STRICT_OBJECT),
149
137
  async execute(input, ctx) {
150
138
  const { id, ...pagination } = input;
151
- const body = await request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`, issueListParams(pagination)));
139
+ const body = await request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`, listParams(pagination)));
152
140
  return body.dependencies;
153
141
  },
154
142
  });
@@ -162,7 +150,7 @@ export function registerIssueActions(rl) {
162
150
  }, STRICT_OBJECT),
163
151
  async execute(input, ctx) {
164
152
  const { id, ...pagination } = input;
165
- return request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`, issueListParams(pagination)));
153
+ return request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`, listParams(pagination)));
166
154
  },
167
155
  });
168
156
  rl.registerAction("issue.dependency.add", {
@@ -1,5 +1,5 @@
1
1
  import * as t from "typebox";
2
- import { cursorSchema, enumSchema, idSchema, PROJECT_STATUS, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "./shared.js";
2
+ import { cursorSchema, enumSchema, idSchema, listParams, PROJECT_STATUS, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "./shared.js";
3
3
  const projectListFields = {
4
4
  status: t.Optional(enumSchema("Project status", PROJECT_STATUS)),
5
5
  leadUserId: t.Optional(idSchema("Lead user ID")),
@@ -7,14 +7,6 @@ const projectListFields = {
7
7
  cursor: t.Optional(cursorSchema()),
8
8
  limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
9
9
  };
10
- function listParams(input) {
11
- const params = new URLSearchParams();
12
- for (const [key, value] of Object.entries((input ?? {}))) {
13
- if (value !== undefined)
14
- params.set(key, String(value));
15
- }
16
- return params;
17
- }
18
10
  export function registerProjectActions(rl) {
19
11
  rl.registerAction("project.list", {
20
12
  access: "read",
@@ -1,8 +1,4 @@
1
- import { baseUrl, pathSegment } from "../../_shared/shiftCloud.js";
2
- export { baseUrl, cursorSchema, enumDescription, enumSchema, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "../../_shared/shiftCloud.js";
3
- export function pageRenderUrl(organizationId, slug) {
4
- return new URL(`/pages/${pathSegment(organizationId)}/${pathSegment(slug)}`, baseUrl()).toString();
5
- }
1
+ export { cursorSchema, enumDescription, enumSchema, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, withQuery, } from "../../_shared/shiftCloud.js";
6
2
  export const ISSUE_STATUS = [
7
3
  "backlog",
8
4
  "todo",
@@ -45,7 +41,3 @@ export const ISSUE_VIEW_SORT = [
45
41
  "dueAt",
46
42
  "updatedAt",
47
43
  ];
48
- export const PAGE_STATUS = ["draft", "published", "archived"];
49
- export const PAGE_VISIBILITY = ["org", "invited"];
50
- export const TRANSCRIPTION_LANGUAGE = ["auto", "en", "he"];
51
- export const TRANSCRIPT_FORMAT = ["txt", "srt", "vtt", "json"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runline",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Code mode for agents \u2014 turn any API or command into a callable action",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",