runline 0.21.0 → 0.23.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/shiftUpload.js +118 -0
- package/dist/plugins/shiftAtlas/src/graph.js +2 -7
- package/dist/plugins/shiftAtlas/src/shared.js +1 -1
- package/dist/plugins/shiftObjects/src/index.js +20 -0
- package/dist/plugins/{shiftLabs → shiftObjects}/src/objects.js +11 -74
- package/dist/plugins/shiftOcr/src/index.js +62 -25
- package/dist/plugins/shiftPages/src/index.js +19 -0
- package/dist/plugins/{shiftLabs → shiftPages}/src/pages.js +73 -22
- package/dist/plugins/shiftPages/src/shared.js +12 -0
- package/dist/plugins/shiftTranscription/src/index.js +20 -0
- package/dist/plugins/shiftTranscription/src/shared.js +3 -0
- package/dist/plugins/{shiftLabs → shiftTranscription}/src/transcription.js +14 -68
- package/dist/plugins/{shiftLabs → shiftWork}/src/index.js +8 -10
- package/dist/plugins/{shiftLabs → shiftWork}/src/issue-views.js +1 -13
- package/dist/plugins/{shiftLabs → shiftWork}/src/issues.js +124 -50
- package/dist/plugins/{shiftLabs → shiftWork}/src/projects.js +1 -9
- package/dist/plugins/{shiftLabs → shiftWork}/src/shared.js +1 -9
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
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, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, } from "./shared.js";
|
|
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
3
|
/**
|
|
4
4
|
* The operational graph behind the Atlas view: a business spec of how
|
|
5
5
|
* a company operates, in its own language. Four fixed levels — line →
|
|
@@ -121,12 +121,7 @@ export function registerGraphActions(rl) {
|
|
|
121
121
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 200 })),
|
|
122
122
|
}, STRICT_OBJECT),
|
|
123
123
|
async execute(input, ctx) {
|
|
124
|
-
|
|
125
|
-
for (const [key, value] of Object.entries(input)) {
|
|
126
|
-
if (value !== undefined)
|
|
127
|
-
params.set(key, String(value));
|
|
128
|
-
}
|
|
129
|
-
return request(ctx, `${ATLAS_BASE}/changes?${params}`);
|
|
124
|
+
return request(ctx, withQuery(`${ATLAS_BASE}/changes`, listParams(input)));
|
|
130
125
|
},
|
|
131
126
|
});
|
|
132
127
|
/* -------------------------------------------------------------- */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { cursorSchema, enumSchema, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, } from "../../_shared/shiftCloud.js";
|
|
1
|
+
export { cursorSchema, enumSchema, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, withQuery, } from "../../_shared/shiftCloud.js";
|
|
2
2
|
/** Base path of the operational graph service in the Shift cloud API. */
|
|
3
3
|
export const ATLAS_BASE = "/v1/services/operational-graph";
|
|
4
4
|
export const GRAPH_ACTOR_KIND = ["agent", "human_role"];
|
|
@@ -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 {
|
|
2
|
-
import {
|
|
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 "
|
|
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 =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
:
|
|
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 (
|
|
79
|
-
"
|
|
80
|
-
"
|
|
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,10 +105,26 @@ 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.",
|
|
95
116
|
})),
|
|
117
|
+
provider: t.Optional(t.String({
|
|
118
|
+
minLength: 1,
|
|
119
|
+
pattern: "\\S",
|
|
120
|
+
description: "Provider ID from ocr.providers. Defaults to the first " +
|
|
121
|
+
"catalog provider.",
|
|
122
|
+
})),
|
|
123
|
+
model: t.Optional(t.String({
|
|
124
|
+
minLength: 1,
|
|
125
|
+
pattern: "\\S",
|
|
126
|
+
description: "Model override. Defaults to the provider's defaultModel.",
|
|
127
|
+
})),
|
|
96
128
|
pages: t.Optional(t.String({
|
|
97
129
|
minLength: 1,
|
|
98
130
|
pattern: "^\\d+(-\\d+)?(,\\d+(-\\d+)?)*$",
|
|
@@ -114,19 +146,24 @@ export default function shiftOcr(rl) {
|
|
|
114
146
|
}, STRICT_OBJECT),
|
|
115
147
|
async execute(input, ctx) {
|
|
116
148
|
const fields = input;
|
|
117
|
-
|
|
118
|
-
|
|
149
|
+
const sources = [fields.path, fields.url, fields.objectId].filter((value) => value !== undefined);
|
|
150
|
+
if (sources.length !== 1) {
|
|
151
|
+
throw new Error("Provide exactly one of path, url, or objectId");
|
|
119
152
|
}
|
|
120
153
|
if (fields.prompt && !fields.schema) {
|
|
121
154
|
throw new Error("prompt requires schema");
|
|
122
155
|
}
|
|
123
|
-
const document = fields.
|
|
124
|
-
?
|
|
125
|
-
:
|
|
156
|
+
const document = fields.objectId
|
|
157
|
+
? { type: "object", objectId: fields.objectId }
|
|
158
|
+
: fields.path
|
|
159
|
+
? await documentFromPath(ctx, fields.path)
|
|
160
|
+
: documentFromUrl(fields.url, fields.kind);
|
|
126
161
|
return await request(ctx, "/v1/services/ocr/extract", {
|
|
127
162
|
method: "POST",
|
|
128
163
|
body: JSON.stringify({
|
|
129
164
|
document,
|
|
165
|
+
...(fields.provider ? { provider: fields.provider } : {}),
|
|
166
|
+
...(fields.model ? { model: fields.model } : {}),
|
|
130
167
|
...(fields.pages ? { pages: fields.pages } : {}),
|
|
131
168
|
...(fields.schema
|
|
132
169
|
? {
|
|
@@ -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,11 +1,18 @@
|
|
|
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_TYPE, 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,
|
|
6
6
|
pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
7
7
|
description: "Lowercase kebab-case page slug",
|
|
8
8
|
});
|
|
9
|
+
const OriginUrl = t.String({
|
|
10
|
+
minLength: 1,
|
|
11
|
+
format: "uri",
|
|
12
|
+
pattern: "^https://",
|
|
13
|
+
description: "HTTPS origin to proxy behind SSO (protected_origin pages only). " +
|
|
14
|
+
"Localhost and private IP hosts are rejected.",
|
|
15
|
+
});
|
|
9
16
|
export function registerPageActions(rl) {
|
|
10
17
|
rl.registerAction("page.list", {
|
|
11
18
|
access: "read",
|
|
@@ -19,13 +26,7 @@ export function registerPageActions(rl) {
|
|
|
19
26
|
})),
|
|
20
27
|
}, STRICT_OBJECT),
|
|
21
28
|
async execute(input, ctx) {
|
|
22
|
-
const
|
|
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}`);
|
|
29
|
+
const body = await request(ctx, withQuery("/v1/pages", listParams(input)));
|
|
29
30
|
return body.pages;
|
|
30
31
|
},
|
|
31
32
|
});
|
|
@@ -39,9 +40,23 @@ export function registerPageActions(rl) {
|
|
|
39
40
|
return body.page;
|
|
40
41
|
},
|
|
41
42
|
});
|
|
43
|
+
rl.registerAction("page.vexArtifacts", {
|
|
44
|
+
access: "read",
|
|
45
|
+
description: "List the shareable Vex artifacts on a managed deployment, sorted by " +
|
|
46
|
+
"title, each carrying the workspace it belongs to. Pass an artifact's " +
|
|
47
|
+
"ID to page.create with type vex_artifact to publish it. The " +
|
|
48
|
+
"deployment must belong to the API key's organization.",
|
|
49
|
+
inputSchema: t.Object({ deploymentId: idSchema("Managed Vex deployment ID") }, STRICT_OBJECT),
|
|
50
|
+
async execute(input, ctx) {
|
|
51
|
+
const body = await request(ctx, withQuery("/v1/pages/vex-artifacts", listParams(input)));
|
|
52
|
+
return body.artifacts;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
42
55
|
rl.registerAction("page.create", {
|
|
43
56
|
access: "write",
|
|
44
|
-
description: "Create a draft
|
|
57
|
+
description: "Create a draft page and publish it with page.publish. hosted_html " +
|
|
58
|
+
"pages carry their own HTML; vex_artifact pages render a live Vex " +
|
|
59
|
+
"artifact (pass deploymentId and artifactId from page.vexArtifacts).",
|
|
45
60
|
inputSchema: t.Object({
|
|
46
61
|
slug: PageSlug,
|
|
47
62
|
title: t.String({
|
|
@@ -50,14 +65,19 @@ export function registerPageActions(rl) {
|
|
|
50
65
|
pattern: "\\S",
|
|
51
66
|
description: "Page title",
|
|
52
67
|
}),
|
|
68
|
+
type: t.Optional(enumSchema("Page type", PAGE_TYPE)),
|
|
53
69
|
html: t.Optional(t.String({
|
|
54
70
|
maxLength: 1_000_000,
|
|
55
|
-
description: "Hosted HTML content",
|
|
71
|
+
description: "Hosted HTML content (hosted_html pages only)",
|
|
56
72
|
})),
|
|
73
|
+
originUrl: t.Optional(OriginUrl),
|
|
74
|
+
deploymentId: t.Optional(idSchema("Managed Vex deployment ID (vex_artifact pages only)")),
|
|
75
|
+
artifactId: t.Optional(idSchema("Vex artifact ID (vex_artifact pages only)")),
|
|
57
76
|
visibility: t.Optional(enumSchema("Page visibility", PAGE_VISIBILITY)),
|
|
58
77
|
}, STRICT_OBJECT),
|
|
59
78
|
async execute(input, ctx) {
|
|
60
79
|
const fields = input;
|
|
80
|
+
assertPageContent(fields);
|
|
61
81
|
const body = await request(ctx, "/v1/pages", {
|
|
62
82
|
method: "POST",
|
|
63
83
|
body: JSON.stringify({
|
|
@@ -71,7 +91,9 @@ export function registerPageActions(rl) {
|
|
|
71
91
|
});
|
|
72
92
|
rl.registerAction("page.update", {
|
|
73
93
|
access: "write",
|
|
74
|
-
description: "Update a
|
|
94
|
+
description: "Update a page's slug, title, visibility, HTML, or — for vex_artifact " +
|
|
95
|
+
"pages — which deployment and artifact it renders. A page's type is " +
|
|
96
|
+
"fixed at creation.",
|
|
75
97
|
inputSchema: t.Object({
|
|
76
98
|
id: idSchema("Page ID"),
|
|
77
99
|
slug: t.Optional(PageSlug),
|
|
@@ -84,8 +106,11 @@ export function registerPageActions(rl) {
|
|
|
84
106
|
visibility: t.Optional(enumSchema("Page visibility", PAGE_VISIBILITY)),
|
|
85
107
|
html: t.Optional(t.String({
|
|
86
108
|
maxLength: 1_000_000,
|
|
87
|
-
description: "Hosted HTML content",
|
|
109
|
+
description: "Hosted HTML content (hosted_html pages only)",
|
|
88
110
|
})),
|
|
111
|
+
originUrl: t.Optional(OriginUrl),
|
|
112
|
+
deploymentId: t.Optional(idSchema("Managed Vex deployment ID (vex_artifact pages only)")),
|
|
113
|
+
artifactId: t.Optional(idSchema("Vex artifact ID (vex_artifact pages only)")),
|
|
89
114
|
}, STRICT_OBJECT),
|
|
90
115
|
async execute(input, ctx) {
|
|
91
116
|
const { id, ...fields } = input;
|
|
@@ -98,7 +123,8 @@ export function registerPageActions(rl) {
|
|
|
98
123
|
});
|
|
99
124
|
rl.registerAction("page.publish", {
|
|
100
125
|
access: "write",
|
|
101
|
-
description: "Publish a
|
|
126
|
+
description: "Publish a draft page, making it reachable at its render URL by the " +
|
|
127
|
+
"organization (or by invited viewers when visibility is invited).",
|
|
102
128
|
inputSchema: t.Object({ id: idSchema("Page ID") }, STRICT_OBJECT),
|
|
103
129
|
async execute(input, ctx) {
|
|
104
130
|
const { id } = input;
|
|
@@ -119,10 +145,10 @@ export function registerPageActions(rl) {
|
|
|
119
145
|
rl.registerAction("page.shares", {
|
|
120
146
|
access: "read",
|
|
121
147
|
description: "List shares for a Shift Labs page.",
|
|
122
|
-
inputSchema: t.Object({
|
|
148
|
+
inputSchema: t.Object({ id: idSchema("Page ID") }, STRICT_OBJECT),
|
|
123
149
|
async execute(input, ctx) {
|
|
124
|
-
const {
|
|
125
|
-
const body = await request(ctx, `/v1/pages/${pathSegment(
|
|
150
|
+
const { id } = input;
|
|
151
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(id)}/shares`);
|
|
126
152
|
return body.shares;
|
|
127
153
|
},
|
|
128
154
|
});
|
|
@@ -130,7 +156,7 @@ export function registerPageActions(rl) {
|
|
|
130
156
|
access: "write",
|
|
131
157
|
description: "Create a viewer share for a Shift Labs page.",
|
|
132
158
|
inputSchema: t.Object({
|
|
133
|
-
|
|
159
|
+
id: idSchema("Page ID"),
|
|
134
160
|
email: t.String({
|
|
135
161
|
minLength: 1,
|
|
136
162
|
format: "email",
|
|
@@ -139,8 +165,8 @@ export function registerPageActions(rl) {
|
|
|
139
165
|
expiresAt: t.Optional(timestampSchema("Optional ISO-8601 expiration")),
|
|
140
166
|
}, STRICT_OBJECT),
|
|
141
167
|
async execute(input, ctx) {
|
|
142
|
-
const {
|
|
143
|
-
const body = await request(ctx, `/v1/pages/${pathSegment(String(
|
|
168
|
+
const { id, ...fields } = input;
|
|
169
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(String(id))}/shares`, {
|
|
144
170
|
method: "POST",
|
|
145
171
|
body: JSON.stringify(fields),
|
|
146
172
|
});
|
|
@@ -162,13 +188,38 @@ export function registerPageActions(rl) {
|
|
|
162
188
|
rl.registerAction("page.renderUrl", {
|
|
163
189
|
access: "read",
|
|
164
190
|
description: "Return the authenticated render URL for a page.",
|
|
165
|
-
inputSchema: t.Object({
|
|
191
|
+
inputSchema: t.Object({ id: idSchema("Page ID") }, STRICT_OBJECT),
|
|
166
192
|
async execute(input, ctx) {
|
|
167
|
-
const {
|
|
168
|
-
const body = await request(ctx, `/v1/pages/${pathSegment(
|
|
193
|
+
const { id } = input;
|
|
194
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(id)}`);
|
|
169
195
|
return {
|
|
170
196
|
url: pageRenderUrl(body.page.organizationId, body.page.slug),
|
|
171
197
|
};
|
|
172
198
|
},
|
|
173
199
|
});
|
|
174
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* The content rules the API enforces per page type, checked before the
|
|
203
|
+
* round-trip so a caller gets the reason rather than a bare 400. Each
|
|
204
|
+
* type owns exactly one content field.
|
|
205
|
+
*/
|
|
206
|
+
function assertPageContent(fields) {
|
|
207
|
+
const type = fields.type ?? "hosted_html";
|
|
208
|
+
const referencesArtifact = fields.deploymentId !== undefined || fields.artifactId !== undefined;
|
|
209
|
+
if (type !== "vex_artifact" && referencesArtifact) {
|
|
210
|
+
throw new Error("Only vex_artifact pages may reference a deployment or artifact");
|
|
211
|
+
}
|
|
212
|
+
if (type !== "protected_origin" && fields.originUrl !== undefined) {
|
|
213
|
+
throw new Error("Only protected_origin pages may set an originUrl");
|
|
214
|
+
}
|
|
215
|
+
if (type !== "hosted_html" && fields.html !== undefined) {
|
|
216
|
+
throw new Error("Only hosted_html pages may carry html");
|
|
217
|
+
}
|
|
218
|
+
if (type === "vex_artifact" &&
|
|
219
|
+
(fields.deploymentId === undefined || fields.artifactId === undefined)) {
|
|
220
|
+
throw new Error("vex_artifact pages require both deploymentId and artifactId");
|
|
221
|
+
}
|
|
222
|
+
if (type === "protected_origin" && fields.originUrl === undefined) {
|
|
223
|
+
throw new Error("protected_origin pages require an originUrl");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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_TYPE = [
|
|
5
|
+
"hosted_html",
|
|
6
|
+
"protected_origin",
|
|
7
|
+
"vex_artifact",
|
|
8
|
+
];
|
|
9
|
+
export const PAGE_VISIBILITY = ["org", "invited"];
|
|
10
|
+
export function pageRenderUrl(organizationId, slug) {
|
|
11
|
+
return new URL(`/pages/${pathSegment(organizationId)}/${pathSegment(slug)}`, baseUrl()).toString();
|
|
12
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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",
|
|
@@ -68,6 +28,9 @@ export function registerTranscriptionActions(rl) {
|
|
|
68
28
|
pattern: "\\S",
|
|
69
29
|
description: "Label shown in Shift Cloud",
|
|
70
30
|
})),
|
|
31
|
+
metadata: t.Optional(t.Record(t.String(), t.Unknown(), {
|
|
32
|
+
description: "Small caller-defined metadata object stored on the job",
|
|
33
|
+
})),
|
|
71
34
|
waitSeconds: t.Optional(t.Number({
|
|
72
35
|
minimum: 0,
|
|
73
36
|
maximum: 120,
|
|
@@ -76,35 +39,17 @@ export function registerTranscriptionActions(rl) {
|
|
|
76
39
|
}, STRICT_OBJECT),
|
|
77
40
|
async execute(input, ctx) {
|
|
78
41
|
const fields = input;
|
|
79
|
-
const contentType =
|
|
80
|
-
const
|
|
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
|
-
}
|
|
42
|
+
const contentType = requireMediaType(fields.path, AUDIO_VIDEO_MEDIA_TYPES);
|
|
43
|
+
const sizeBytes = await statUploadFile(fields.path, SIGNED_UPLOAD_MAX_BYTES, "Media file");
|
|
88
44
|
const created = await request(ctx, "/v1/services/transcription/assets", {
|
|
89
45
|
method: "POST",
|
|
90
46
|
// The API key is the tenant authority; no organizationId is sent.
|
|
91
47
|
body: JSON.stringify({
|
|
92
48
|
contentType,
|
|
93
|
-
sizeBytes
|
|
49
|
+
sizeBytes,
|
|
94
50
|
}),
|
|
95
51
|
});
|
|
96
|
-
|
|
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
|
-
}
|
|
52
|
+
await putThroughGrant(created.upload, fields.path, contentType, sizeBytes, "Media upload");
|
|
108
53
|
await request(ctx, `/v1/services/transcription/assets/${pathSegment(created.asset.id)}/complete`, { method: "POST" });
|
|
109
54
|
const job = await request(ctx, "/v1/services/transcription/jobs", {
|
|
110
55
|
method: "POST",
|
|
@@ -113,6 +58,7 @@ export function registerTranscriptionActions(rl) {
|
|
|
113
58
|
language: fields.language,
|
|
114
59
|
diarize: fields.diarize,
|
|
115
60
|
name: fields.name,
|
|
61
|
+
metadata: fields.metadata,
|
|
116
62
|
}),
|
|
117
63
|
});
|
|
118
64
|
if (!fields.waitSeconds)
|
|
@@ -151,10 +97,10 @@ export function registerTranscriptionActions(rl) {
|
|
|
151
97
|
rl.registerAction("transcription.artifact.list", {
|
|
152
98
|
access: "read",
|
|
153
99
|
description: "List the available transcript artifacts for a job.",
|
|
154
|
-
inputSchema: t.Object({
|
|
100
|
+
inputSchema: t.Object({ id: Id }, STRICT_OBJECT),
|
|
155
101
|
async execute(input, ctx) {
|
|
156
|
-
const {
|
|
157
|
-
const body = await request(ctx, `/v1/services/transcription/jobs/${pathSegment(
|
|
102
|
+
const { id } = input;
|
|
103
|
+
const body = await request(ctx, `/v1/services/transcription/jobs/${pathSegment(id)}/artifacts`);
|
|
158
104
|
return body.artifacts;
|
|
159
105
|
},
|
|
160
106
|
});
|
|
@@ -164,14 +110,14 @@ export function registerTranscriptionActions(rl) {
|
|
|
164
110
|
"return subtitles; json returns the full timestamped document. Set " +
|
|
165
111
|
"downloadOnly=true to return the short-lived signed download grant.",
|
|
166
112
|
inputSchema: t.Object({
|
|
167
|
-
|
|
113
|
+
id: Id,
|
|
168
114
|
format: t.Optional(enumSchema("Transcript format", [...TRANSCRIPT_FORMAT])),
|
|
169
115
|
downloadOnly: t.Optional(t.Boolean({ description: "Return a signed download grant only" })),
|
|
170
116
|
}, STRICT_OBJECT),
|
|
171
117
|
async execute(input, ctx) {
|
|
172
118
|
const fields = input;
|
|
173
119
|
const format = fields.format ?? "txt";
|
|
174
|
-
const grant = await request(ctx, `/v1/services/transcription/jobs/${pathSegment(fields.
|
|
120
|
+
const grant = await request(ctx, `/v1/services/transcription/jobs/${pathSegment(fields.id)}/artifacts/${pathSegment(format)}/download`, { method: "POST" });
|
|
175
121
|
if (fields.downloadOnly ||
|
|
176
122
|
grant.artifact.sizeBytes > MAX_TRANSCRIPT_BYTES) {
|
|
177
123
|
return { format, ...grant };
|
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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,30 @@
|
|
|
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
|
+
const issueFields = {
|
|
4
|
+
title: t.String({
|
|
5
|
+
minLength: 1,
|
|
6
|
+
maxLength: 160,
|
|
7
|
+
pattern: "\\S",
|
|
8
|
+
description: "Issue title",
|
|
9
|
+
}),
|
|
10
|
+
description: t.Optional(t.String({ maxLength: 20_000, description: "Issue description" })),
|
|
11
|
+
projectId: t.Optional(idSchema("Project ID")),
|
|
12
|
+
parentIssueId: t.Optional(idSchema("Parent Issue ID")),
|
|
13
|
+
sortOrder: t.Optional(t.Integer({ minimum: 0 })),
|
|
14
|
+
status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
|
|
15
|
+
priority: t.Optional(enumSchema("Issue priority", ISSUE_PRIORITY)),
|
|
16
|
+
assigneeUserId: t.Optional(idSchema("Assignee user ID")),
|
|
17
|
+
startAt: t.Optional(timestampSchema("ISO-8601 start timestamp")),
|
|
18
|
+
dueAt: t.Optional(timestampSchema("ISO-8601 due timestamp")),
|
|
19
|
+
deploymentId: t.Optional(idSchema("Deployment ID")),
|
|
20
|
+
workspaceId: t.Optional(idSchema("Workspace ID")),
|
|
21
|
+
sessionId: t.Optional(idSchema("Session ID")),
|
|
22
|
+
traceId: t.Optional(idSchema("Trace ID")),
|
|
23
|
+
fingerprint: t.Optional(t.String({ minLength: 1, maxLength: 512, pattern: "\\S" })),
|
|
24
|
+
labels: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 64, pattern: "\\S" }))),
|
|
25
|
+
metadata: t.Optional(t.Record(t.String(), t.Unknown(), { description: "Issue metadata" })),
|
|
26
|
+
source: t.Optional(enumSchema("Issue source", ISSUE_SOURCE)),
|
|
27
|
+
};
|
|
3
28
|
const issueListFields = {
|
|
4
29
|
status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
|
|
5
30
|
projectId: t.Optional(idSchema("Project ID")),
|
|
@@ -14,25 +39,22 @@ const issueListFields = {
|
|
|
14
39
|
description: "Max results, default 50",
|
|
15
40
|
})),
|
|
16
41
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const query = params.toString();
|
|
27
|
-
return query ? `${path}?${query}` : path;
|
|
28
|
-
}
|
|
42
|
+
const eventListFields = {
|
|
43
|
+
id: idSchema("Issue ID"),
|
|
44
|
+
cursor: t.Optional(cursorSchema()),
|
|
45
|
+
limit: t.Optional(t.Integer({
|
|
46
|
+
minimum: 1,
|
|
47
|
+
maximum: 100,
|
|
48
|
+
description: "Max results, default 50",
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
29
51
|
export function registerIssueActions(rl) {
|
|
30
52
|
rl.registerAction("issue.list", {
|
|
31
53
|
access: "read",
|
|
32
54
|
description: "List the first page of Shift Labs Issues for the API key's organization.",
|
|
33
55
|
inputSchema: t.Object(issueListFields, STRICT_OBJECT),
|
|
34
56
|
async execute(input, ctx) {
|
|
35
|
-
const body = await request(ctx, `/v1/issues?${
|
|
57
|
+
const body = await request(ctx, `/v1/issues?${listParams(input)}`);
|
|
36
58
|
return body.issues;
|
|
37
59
|
},
|
|
38
60
|
});
|
|
@@ -41,7 +63,7 @@ export function registerIssueActions(rl) {
|
|
|
41
63
|
description: "List a cursor-paginated page of Shift Labs Issues.",
|
|
42
64
|
inputSchema: t.Object(issueListFields, STRICT_OBJECT),
|
|
43
65
|
async execute(input, ctx) {
|
|
44
|
-
return request(ctx, `/v1/issues?${
|
|
66
|
+
return request(ctx, `/v1/issues?${listParams(input)}`);
|
|
45
67
|
},
|
|
46
68
|
});
|
|
47
69
|
rl.registerAction("issue.get", {
|
|
@@ -57,38 +79,10 @@ export function registerIssueActions(rl) {
|
|
|
57
79
|
rl.registerAction("issue.create", {
|
|
58
80
|
access: "write",
|
|
59
81
|
description: "Create an Issue. Leave projectId empty for inbox work; parentIssueId requires a Project.",
|
|
60
|
-
inputSchema: t.Object(
|
|
61
|
-
title: t.String({
|
|
62
|
-
minLength: 1,
|
|
63
|
-
maxLength: 160,
|
|
64
|
-
pattern: "\\S",
|
|
65
|
-
description: "Issue title",
|
|
66
|
-
}),
|
|
67
|
-
description: t.Optional(t.String({ maxLength: 20_000, description: "Issue description" })),
|
|
68
|
-
projectId: t.Optional(idSchema("Project ID")),
|
|
69
|
-
parentIssueId: t.Optional(idSchema("Parent Issue ID")),
|
|
70
|
-
sortOrder: t.Optional(t.Integer({ minimum: 0 })),
|
|
71
|
-
status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
|
|
72
|
-
priority: t.Optional(enumSchema("Issue priority", ISSUE_PRIORITY)),
|
|
73
|
-
source: t.Optional(enumSchema("Issue source", ISSUE_SOURCE)),
|
|
74
|
-
assigneeUserId: t.Optional(idSchema("Assignee user ID")),
|
|
75
|
-
startAt: t.Optional(timestampSchema("ISO-8601 start timestamp")),
|
|
76
|
-
dueAt: t.Optional(timestampSchema("ISO-8601 due timestamp")),
|
|
77
|
-
deploymentId: t.Optional(idSchema("Deployment ID")),
|
|
78
|
-
workspaceId: t.Optional(idSchema("Workspace ID")),
|
|
79
|
-
sessionId: t.Optional(idSchema("Session ID")),
|
|
80
|
-
traceId: t.Optional(idSchema("Trace ID")),
|
|
81
|
-
fingerprint: t.Optional(t.String({ minLength: 1, maxLength: 512, pattern: "\\S" })),
|
|
82
|
-
labels: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 64, pattern: "\\S" }))),
|
|
83
|
-
metadata: t.Optional(t.Record(t.String(), t.Unknown(), { description: "Issue metadata" })),
|
|
84
|
-
}, STRICT_OBJECT),
|
|
82
|
+
inputSchema: t.Object(issueFields, STRICT_OBJECT),
|
|
85
83
|
async execute(input, ctx) {
|
|
86
84
|
const fields = input;
|
|
87
|
-
|
|
88
|
-
fields.projectId === undefined) {
|
|
89
|
-
throw new Error("projectId is required when parentIssueId is set");
|
|
90
|
-
}
|
|
91
|
-
assertDateOrder(fields.startAt, fields.dueAt, "Issue dueAt must not precede startAt");
|
|
85
|
+
assertIssueShape(fields);
|
|
92
86
|
const body = await request(ctx, "/v1/issues", {
|
|
93
87
|
method: "POST",
|
|
94
88
|
body: JSON.stringify(fields),
|
|
@@ -96,6 +90,19 @@ export function registerIssueActions(rl) {
|
|
|
96
90
|
return body.issue;
|
|
97
91
|
},
|
|
98
92
|
});
|
|
93
|
+
rl.registerAction("issue.report", {
|
|
94
|
+
access: "write",
|
|
95
|
+
description: "Report an Issue the way an agent should: source defaults to agent, " +
|
|
96
|
+
"and a repeated fingerprint folds into the existing Issue as a " +
|
|
97
|
+
"reported_again event instead of creating a duplicate.",
|
|
98
|
+
inputSchema: t.Object(issueFields, STRICT_OBJECT),
|
|
99
|
+
async execute(input, ctx) {
|
|
100
|
+
const fields = input;
|
|
101
|
+
assertIssueShape(fields);
|
|
102
|
+
const body = await request(ctx, "/v1/issues/report", { method: "POST", body: JSON.stringify(fields) });
|
|
103
|
+
return body.issue;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
99
106
|
rl.registerAction("issue.update", {
|
|
100
107
|
access: "write",
|
|
101
108
|
description: "Update an Issue, including Project assignment, one-level parentage, ordering, status, dates, or assignee.",
|
|
@@ -139,6 +146,66 @@ export function registerIssueActions(rl) {
|
|
|
139
146
|
return response.event;
|
|
140
147
|
},
|
|
141
148
|
});
|
|
149
|
+
rl.registerAction("issue.resolve", {
|
|
150
|
+
access: "write",
|
|
151
|
+
description: "Resolve an Issue, optionally with a closing comment recorded on the " +
|
|
152
|
+
"event timeline.",
|
|
153
|
+
inputSchema: t.Object({
|
|
154
|
+
id: idSchema("Issue ID"),
|
|
155
|
+
comment: t.Optional(t.String({
|
|
156
|
+
minLength: 1,
|
|
157
|
+
maxLength: 20_000,
|
|
158
|
+
pattern: "\\S",
|
|
159
|
+
description: "Resolution comment",
|
|
160
|
+
})),
|
|
161
|
+
}, STRICT_OBJECT),
|
|
162
|
+
async execute(input, ctx) {
|
|
163
|
+
const { id, ...fields } = input;
|
|
164
|
+
const body = await request(ctx, `/v1/issues/${pathSegment(id)}/resolve`, { method: "POST", body: JSON.stringify(fields) });
|
|
165
|
+
return body.issue;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
rl.registerAction("issue.reopen", {
|
|
169
|
+
access: "write",
|
|
170
|
+
description: "Reopen a resolved or closed Issue.",
|
|
171
|
+
inputSchema: t.Object({ id: idSchema("Issue ID") }, STRICT_OBJECT),
|
|
172
|
+
async execute(input, ctx) {
|
|
173
|
+
const { id } = input;
|
|
174
|
+
const body = await request(ctx, `/v1/issues/${pathSegment(id)}/reopen`, { method: "POST" });
|
|
175
|
+
return body.issue;
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
rl.registerAction("issue.close", {
|
|
179
|
+
access: "write",
|
|
180
|
+
description: "Close an Issue without resolving it — the outcome for work that is " +
|
|
181
|
+
"abandoned rather than finished.",
|
|
182
|
+
inputSchema: t.Object({ id: idSchema("Issue ID") }, STRICT_OBJECT),
|
|
183
|
+
async execute(input, ctx) {
|
|
184
|
+
const { id } = input;
|
|
185
|
+
const body = await request(ctx, `/v1/issues/${pathSegment(id)}/close`, { method: "POST" });
|
|
186
|
+
return body.issue;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
rl.registerAction("issue.event.list", {
|
|
190
|
+
access: "read",
|
|
191
|
+
description: "List the first page of an Issue's event timeline: status changes, " +
|
|
192
|
+
"comments, assignment and dependency history, newest first.",
|
|
193
|
+
inputSchema: t.Object(eventListFields, STRICT_OBJECT),
|
|
194
|
+
async execute(input, ctx) {
|
|
195
|
+
const { id, ...pagination } = input;
|
|
196
|
+
const body = await request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/events`, listParams(pagination)));
|
|
197
|
+
return body.events;
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
rl.registerAction("issue.event.listPage", {
|
|
201
|
+
access: "read",
|
|
202
|
+
description: "List a cursor-paginated page of an Issue's event timeline.",
|
|
203
|
+
inputSchema: t.Object(eventListFields, STRICT_OBJECT),
|
|
204
|
+
async execute(input, ctx) {
|
|
205
|
+
const { id, ...pagination } = input;
|
|
206
|
+
return request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/events`, listParams(pagination)));
|
|
207
|
+
},
|
|
208
|
+
});
|
|
142
209
|
rl.registerAction("issue.dependency.list", {
|
|
143
210
|
access: "read",
|
|
144
211
|
description: "List the first page of blocking and blocked-by links.",
|
|
@@ -148,7 +215,7 @@ export function registerIssueActions(rl) {
|
|
|
148
215
|
}, STRICT_OBJECT),
|
|
149
216
|
async execute(input, ctx) {
|
|
150
217
|
const { id, ...pagination } = input;
|
|
151
|
-
const body = await request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`,
|
|
218
|
+
const body = await request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`, listParams(pagination)));
|
|
152
219
|
return body.dependencies;
|
|
153
220
|
},
|
|
154
221
|
});
|
|
@@ -162,7 +229,7 @@ export function registerIssueActions(rl) {
|
|
|
162
229
|
}, STRICT_OBJECT),
|
|
163
230
|
async execute(input, ctx) {
|
|
164
231
|
const { id, ...pagination } = input;
|
|
165
|
-
return request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`,
|
|
232
|
+
return request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`, listParams(pagination)));
|
|
166
233
|
},
|
|
167
234
|
});
|
|
168
235
|
rl.registerAction("issue.dependency.add", {
|
|
@@ -185,16 +252,23 @@ export function registerIssueActions(rl) {
|
|
|
185
252
|
access: "write",
|
|
186
253
|
description: "Remove an Issue dependency link.",
|
|
187
254
|
inputSchema: t.Object({
|
|
188
|
-
|
|
255
|
+
id: idSchema("Issue ID"),
|
|
189
256
|
dependencyId: idSchema("Dependency ID"),
|
|
190
257
|
}, STRICT_OBJECT),
|
|
191
258
|
async execute(input, ctx) {
|
|
192
|
-
const {
|
|
193
|
-
await request(ctx, `/v1/issues/${pathSegment(
|
|
259
|
+
const { id, dependencyId } = input;
|
|
260
|
+
await request(ctx, `/v1/issues/${pathSegment(id)}/dependencies/${pathSegment(dependencyId)}`, { method: "DELETE" });
|
|
194
261
|
return { removed: true };
|
|
195
262
|
},
|
|
196
263
|
});
|
|
197
264
|
}
|
|
265
|
+
/** Shape rules the API enforces server-side, caught before the round-trip. */
|
|
266
|
+
function assertIssueShape(fields) {
|
|
267
|
+
if (fields.parentIssueId !== undefined && fields.projectId === undefined) {
|
|
268
|
+
throw new Error("projectId is required when parentIssueId is set");
|
|
269
|
+
}
|
|
270
|
+
assertDateOrder(fields.startAt, fields.dueAt, "Issue dueAt must not precede startAt");
|
|
271
|
+
}
|
|
198
272
|
function assertDateOrder(start, end, message) {
|
|
199
273
|
if (typeof start === "string" && typeof end === "string" && start > end) {
|
|
200
274
|
throw new Error(message);
|
|
@@ -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
|
-
|
|
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"];
|