runline 0.12.0 → 0.13.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/plugins/_shared/googleSchemas.js +23 -0
- package/dist/plugins/gmail/src/index.js +168 -184
- package/dist/plugins/googleAppsScript/src/index.js +88 -43
- package/dist/plugins/googleCalendar/src/index.js +307 -250
- package/dist/plugins/googleContacts/src/index.js +134 -145
- package/dist/plugins/googleDocs/src/documents.js +27 -38
- package/dist/plugins/googleDocs/src/formatting.js +49 -47
- package/dist/plugins/googleDocs/src/images.js +21 -26
- package/dist/plugins/googleDocs/src/shared.js +55 -0
- package/dist/plugins/googleDocs/src/structure.js +98 -123
- package/dist/plugins/googleDocs/src/tables.js +92 -154
- package/dist/plugins/googleDocs/src/tabs.js +24 -23
- package/dist/plugins/googleDocs/src/text.js +97 -136
- package/dist/plugins/googleDrive/src/index.js +414 -408
- package/dist/plugins/googleImage/src/index.js +13 -16
- package/dist/plugins/googleSheets/src/index.js +504 -339
- package/dist/plugins/googleSlides/src/index.js +85 -72
- package/dist/plugins/googleTasks/src/index.js +82 -92
- package/dist/plugins/shiftLabs/src/index.js +6 -0
- package/dist/plugins/shiftLabs/src/shared.js +2 -0
- package/dist/plugins/shiftLabs/src/transcription.js +203 -0
- package/package.json +1 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { openAsBlob } from "node:fs";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { extname } from "node:path";
|
|
4
|
+
import * as t from "typebox";
|
|
5
|
+
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
|
+
/** Transcript previews returned into the sandbox are capped, not truncated silently. */
|
|
25
|
+
const MAX_TRANSCRIPT_BYTES = 2 * 1024 * 1024;
|
|
26
|
+
const STRICT_OBJECT = { additionalProperties: false };
|
|
27
|
+
const Id = t.String({ minLength: 1, pattern: "\\S" });
|
|
28
|
+
function organizationId(ctx) {
|
|
29
|
+
const value = ctx.connection.config.organizationId;
|
|
30
|
+
if (typeof value !== "string" || !value) {
|
|
31
|
+
throw new Error("SHIFT_LABS_ORG_ID is not set. Transcription actions need the organization ID that owns the API key.");
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function mediaTypeFor(path) {
|
|
36
|
+
const mediaType = MEDIA_TYPES[extname(path).slice(1).toLowerCase()];
|
|
37
|
+
if (!mediaType) {
|
|
38
|
+
throw new Error(`Unsupported media extension for ${path}. Supported: ${Object.keys(MEDIA_TYPES).join(", ")}`);
|
|
39
|
+
}
|
|
40
|
+
return mediaType;
|
|
41
|
+
}
|
|
42
|
+
async function fileBody(path, sizeBytes) {
|
|
43
|
+
try {
|
|
44
|
+
return await openAsBlob(path);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Runtimes without fs.openAsBlob fall back to buffering; refuse to
|
|
48
|
+
// buffer files that would strain the host process.
|
|
49
|
+
if (sizeBytes > 512 * 1024 * 1024) {
|
|
50
|
+
throw new Error("This runtime cannot stream uploads and the file is too large to buffer.");
|
|
51
|
+
}
|
|
52
|
+
return await readFile(path);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function registerTranscriptionActions(rl) {
|
|
56
|
+
rl.registerAction("transcription.transcribe", {
|
|
57
|
+
access: "write",
|
|
58
|
+
description: "Upload a local audio or video file and start a transcription job. " +
|
|
59
|
+
"Returns the job; poll transcription.job.get, then read the result " +
|
|
60
|
+
"with transcription.transcript. Diarization labels speakers but is " +
|
|
61
|
+
"much slower.",
|
|
62
|
+
inputSchema: t.Object({
|
|
63
|
+
path: t.String({
|
|
64
|
+
minLength: 1,
|
|
65
|
+
pattern: "\\S",
|
|
66
|
+
description: "Host path of the media file",
|
|
67
|
+
}),
|
|
68
|
+
language: t.Optional(enumSchema("Language (auto handles mixed media)", [
|
|
69
|
+
...TRANSCRIPTION_LANGUAGE,
|
|
70
|
+
])),
|
|
71
|
+
diarize: t.Optional(t.Boolean({ description: "Label speakers (slower). Default false" })),
|
|
72
|
+
name: t.Optional(t.String({
|
|
73
|
+
minLength: 1,
|
|
74
|
+
maxLength: 200,
|
|
75
|
+
pattern: "\\S",
|
|
76
|
+
description: "Label shown in Shift Cloud",
|
|
77
|
+
})),
|
|
78
|
+
waitSeconds: t.Optional(t.Number({
|
|
79
|
+
minimum: 0,
|
|
80
|
+
maximum: 120,
|
|
81
|
+
description: "Block up to this many seconds for completion. Longer media should be polled instead.",
|
|
82
|
+
})),
|
|
83
|
+
}, STRICT_OBJECT),
|
|
84
|
+
async execute(input, ctx) {
|
|
85
|
+
const fields = input;
|
|
86
|
+
const org = organizationId(ctx);
|
|
87
|
+
const contentType = mediaTypeFor(fields.path);
|
|
88
|
+
const file = await stat(fields.path);
|
|
89
|
+
if (!file.isFile())
|
|
90
|
+
throw new Error(`${fields.path} is not a file`);
|
|
91
|
+
if (file.size === 0)
|
|
92
|
+
throw new Error("Media file is empty");
|
|
93
|
+
if (file.size > MAX_MEDIA_BYTES) {
|
|
94
|
+
throw new Error(`Media file is larger than the ${MAX_MEDIA_BYTES}-byte service limit`);
|
|
95
|
+
}
|
|
96
|
+
const created = await request(ctx, "/v1/services/transcription/assets", {
|
|
97
|
+
method: "POST",
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
organizationId: org,
|
|
100
|
+
contentType,
|
|
101
|
+
sizeBytes: file.size,
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
const uploadHeaders = new Headers(created.upload.headers);
|
|
105
|
+
if (!uploadHeaders.has("content-type")) {
|
|
106
|
+
uploadHeaders.set("content-type", contentType);
|
|
107
|
+
}
|
|
108
|
+
const upload = await fetch(created.upload.url, {
|
|
109
|
+
method: created.upload.method,
|
|
110
|
+
headers: uploadHeaders,
|
|
111
|
+
body: await fileBody(fields.path, file.size),
|
|
112
|
+
});
|
|
113
|
+
if (!upload.ok) {
|
|
114
|
+
throw new Error(`Media upload failed: HTTP ${upload.status}`);
|
|
115
|
+
}
|
|
116
|
+
await request(ctx, `/v1/services/transcription/assets/${pathSegment(created.asset.id)}/complete`, { method: "POST" });
|
|
117
|
+
const job = await request(ctx, "/v1/services/transcription/jobs", {
|
|
118
|
+
method: "POST",
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
organizationId: org,
|
|
121
|
+
assetId: created.asset.id,
|
|
122
|
+
language: fields.language,
|
|
123
|
+
diarize: fields.diarize,
|
|
124
|
+
name: fields.name,
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
if (!fields.waitSeconds)
|
|
128
|
+
return job;
|
|
129
|
+
const timeoutMs = Math.min(Math.max(fields.waitSeconds, 1), 120) * 1000;
|
|
130
|
+
return request(ctx, `/v1/services/transcription/jobs/${pathSegment(job.id)}/await?timeoutMs=${timeoutMs}`, { method: "POST" });
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
rl.registerAction("transcription.job.list", {
|
|
134
|
+
access: "read",
|
|
135
|
+
description: "List transcription jobs for the API key's organization, newest first.",
|
|
136
|
+
inputSchema: t.Object({}, STRICT_OBJECT),
|
|
137
|
+
async execute(_input, ctx) {
|
|
138
|
+
const body = await request(ctx, `/v1/services/transcription/jobs?organizationId=${pathSegment(organizationId(ctx))}`);
|
|
139
|
+
return body.jobs;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
rl.registerAction("transcription.job.get", {
|
|
143
|
+
access: "read",
|
|
144
|
+
description: "Get a transcription job: status, progress, duration, speaker count.",
|
|
145
|
+
inputSchema: t.Object({ id: Id }, STRICT_OBJECT),
|
|
146
|
+
async execute(input, ctx) {
|
|
147
|
+
const { id } = input;
|
|
148
|
+
return request(ctx, `/v1/services/transcription/jobs/${pathSegment(id)}`);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
rl.registerAction("transcription.job.cancel", {
|
|
152
|
+
access: "write",
|
|
153
|
+
description: "Cancel a queued or running transcription job.",
|
|
154
|
+
inputSchema: t.Object({ id: Id }, STRICT_OBJECT),
|
|
155
|
+
async execute(input, ctx) {
|
|
156
|
+
const { id } = input;
|
|
157
|
+
return request(ctx, `/v1/services/transcription/jobs/${pathSegment(id)}/cancel`, { method: "POST" });
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
rl.registerAction("transcription.artifact.list", {
|
|
161
|
+
access: "read",
|
|
162
|
+
description: "List the available transcript artifacts for a job.",
|
|
163
|
+
inputSchema: t.Object({ jobId: Id }, STRICT_OBJECT),
|
|
164
|
+
async execute(input, ctx) {
|
|
165
|
+
const { jobId } = input;
|
|
166
|
+
const body = await request(ctx, `/v1/services/transcription/jobs/${pathSegment(jobId)}/artifacts`);
|
|
167
|
+
return body.artifacts;
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
rl.registerAction("transcription.transcript", {
|
|
171
|
+
access: "read",
|
|
172
|
+
description: "Fetch a finished job's transcript. txt returns plain text; srt/vtt " +
|
|
173
|
+
"return subtitles; json returns the full timestamped document. Set " +
|
|
174
|
+
"downloadOnly=true to return the short-lived signed download grant.",
|
|
175
|
+
inputSchema: t.Object({
|
|
176
|
+
jobId: Id,
|
|
177
|
+
format: t.Optional(enumSchema("Transcript format", [...TRANSCRIPT_FORMAT])),
|
|
178
|
+
downloadOnly: t.Optional(t.Boolean({ description: "Return a signed download grant only" })),
|
|
179
|
+
}, STRICT_OBJECT),
|
|
180
|
+
async execute(input, ctx) {
|
|
181
|
+
const fields = input;
|
|
182
|
+
const format = fields.format ?? "txt";
|
|
183
|
+
const grant = await request(ctx, `/v1/services/transcription/jobs/${pathSegment(fields.jobId)}/artifacts/${pathSegment(format)}/download`, { method: "POST" });
|
|
184
|
+
if (fields.downloadOnly ||
|
|
185
|
+
grant.artifact.sizeBytes > MAX_TRANSCRIPT_BYTES) {
|
|
186
|
+
return { format, ...grant };
|
|
187
|
+
}
|
|
188
|
+
const response = await fetch(grant.download.url, {
|
|
189
|
+
method: grant.download.method,
|
|
190
|
+
});
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
throw new Error(`Transcript download failed: HTTP ${response.status}`);
|
|
193
|
+
}
|
|
194
|
+
const content = await response.text();
|
|
195
|
+
if (Buffer.byteLength(content, "utf8") > MAX_TRANSCRIPT_BYTES) {
|
|
196
|
+
return { format, ...grant };
|
|
197
|
+
}
|
|
198
|
+
return format === "json"
|
|
199
|
+
? { format, document: JSON.parse(content) }
|
|
200
|
+
: { format, content };
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|