runline 0.14.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { enumSchema, ISSUE_PRIORITY, ISSUE_STATUS, ISSUE_VIEW_GROUP, ISSUE_VIEW_LAYOUT, ISSUE_VIEW_SORT, ISSUE_VIEW_VISIBILITY, pathSegment, request, } from "./shared.js";
|
|
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";
|
|
3
3
|
const viewFields = {
|
|
4
|
-
name: t.String({
|
|
4
|
+
name: t.String({
|
|
5
|
+
minLength: 1,
|
|
6
|
+
maxLength: 120,
|
|
7
|
+
pattern: "\\S",
|
|
8
|
+
description: "View name",
|
|
9
|
+
}),
|
|
5
10
|
visibility: t.Optional(t.Literal("organization")),
|
|
6
11
|
layout: t.Optional(enumSchema("View layout", ISSUE_VIEW_LAYOUT)),
|
|
7
|
-
projectId: t.Optional(
|
|
8
|
-
rootIssueId: t.Optional(
|
|
12
|
+
projectId: t.Optional(idSchema("Project ID")),
|
|
13
|
+
rootIssueId: t.Optional(idSchema("Root Issue ID")),
|
|
9
14
|
statuses: t.Optional(t.Array(enumSchema("Issue status", ISSUE_STATUS), { maxItems: 5 })),
|
|
10
15
|
priorities: t.Optional(t.Array(enumSchema("Issue priority", ISSUE_PRIORITY), { maxItems: 5 })),
|
|
11
|
-
assigneeUserIds: t.Optional(t.Array(
|
|
12
|
-
labels: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 64
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
assigneeUserIds: t.Optional(t.Array(idSchema("Assignee user ID"), { maxItems: 20 })),
|
|
17
|
+
labels: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 64, pattern: "\\S" }), {
|
|
18
|
+
maxItems: 20,
|
|
19
|
+
})),
|
|
20
|
+
startAfter: t.Optional(timestampSchema("ISO-8601 filter start timestamp")),
|
|
21
|
+
dueBefore: t.Optional(timestampSchema("ISO-8601 filter due timestamp")),
|
|
15
22
|
blocked: t.Optional(t.Boolean()),
|
|
16
23
|
groupBy: t.Optional(enumSchema("View grouping", ISSUE_VIEW_GROUP)),
|
|
17
24
|
sortBy: t.Optional(t.Array(enumSchema("View sort", ISSUE_VIEW_SORT), {
|
|
@@ -22,13 +29,14 @@ const viewFields = {
|
|
|
22
29
|
export function registerIssueViewActions(rl) {
|
|
23
30
|
const listFields = {
|
|
24
31
|
visibility: t.Optional(enumSchema("View visibility", ISSUE_VIEW_VISIBILITY)),
|
|
25
|
-
|
|
32
|
+
createdById: t.Optional(idSchema("Creator ID")),
|
|
33
|
+
cursor: t.Optional(cursorSchema()),
|
|
26
34
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
|
27
35
|
};
|
|
28
36
|
rl.registerAction("issueView.list", {
|
|
29
37
|
access: "read",
|
|
30
38
|
description: "List the first page of personal and organization Issue Views.",
|
|
31
|
-
inputSchema: t.Object(listFields),
|
|
39
|
+
inputSchema: t.Object(listFields, STRICT_OBJECT),
|
|
32
40
|
async execute(input, ctx) {
|
|
33
41
|
const body = await request(ctx, `/v1/issue-views?${listParams(input)}`);
|
|
34
42
|
return body.views;
|
|
@@ -37,7 +45,7 @@ export function registerIssueViewActions(rl) {
|
|
|
37
45
|
rl.registerAction("issueView.listPage", {
|
|
38
46
|
access: "read",
|
|
39
47
|
description: "List a cursor-paginated page of Issue Views.",
|
|
40
|
-
inputSchema: t.Object(listFields),
|
|
48
|
+
inputSchema: t.Object(listFields, STRICT_OBJECT),
|
|
41
49
|
async execute(input, ctx) {
|
|
42
50
|
return request(ctx, `/v1/issue-views?${listParams(input)}`);
|
|
43
51
|
},
|
|
@@ -45,7 +53,7 @@ export function registerIssueViewActions(rl) {
|
|
|
45
53
|
rl.registerAction("issueView.get", {
|
|
46
54
|
access: "read",
|
|
47
55
|
description: "Get an Issue View by ID.",
|
|
48
|
-
inputSchema: t.Object({ id:
|
|
56
|
+
inputSchema: t.Object({ id: idSchema("Issue View ID") }, STRICT_OBJECT),
|
|
49
57
|
async execute(input, ctx) {
|
|
50
58
|
const { id } = input;
|
|
51
59
|
const body = await request(ctx, `/v1/issue-views/${pathSegment(id)}`);
|
|
@@ -56,9 +64,9 @@ export function registerIssueViewActions(rl) {
|
|
|
56
64
|
access: "read",
|
|
57
65
|
description: "Execute the first page of a saved Issue View.",
|
|
58
66
|
inputSchema: t.Object({
|
|
59
|
-
id:
|
|
67
|
+
id: idSchema("Issue View ID"),
|
|
60
68
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
|
61
|
-
}),
|
|
69
|
+
}, STRICT_OBJECT),
|
|
62
70
|
async execute(input, ctx) {
|
|
63
71
|
const { id, ...pagination } = input;
|
|
64
72
|
const body = await request(ctx, withQuery(`/v1/issue-views/${pathSegment(id)}/issues`, listParams(pagination)));
|
|
@@ -69,10 +77,10 @@ export function registerIssueViewActions(rl) {
|
|
|
69
77
|
access: "read",
|
|
70
78
|
description: "Execute a cursor-paginated page of a saved Issue View.",
|
|
71
79
|
inputSchema: t.Object({
|
|
72
|
-
id:
|
|
73
|
-
cursor: t.Optional(
|
|
80
|
+
id: idSchema("Issue View ID"),
|
|
81
|
+
cursor: t.Optional(cursorSchema()),
|
|
74
82
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
|
75
|
-
}),
|
|
83
|
+
}, STRICT_OBJECT),
|
|
76
84
|
async execute(input, ctx) {
|
|
77
85
|
const { id, ...pagination } = input;
|
|
78
86
|
return request(ctx, withQuery(`/v1/issue-views/${pathSegment(id)}/issues`, listParams(pagination)));
|
|
@@ -81,11 +89,13 @@ export function registerIssueViewActions(rl) {
|
|
|
81
89
|
rl.registerAction("issueView.create", {
|
|
82
90
|
access: "write",
|
|
83
91
|
description: "Create a bounded saved Issue query. Views never own Issue membership.",
|
|
84
|
-
inputSchema: t.Object(viewFields),
|
|
92
|
+
inputSchema: t.Object(viewFields, STRICT_OBJECT),
|
|
85
93
|
async execute(input, ctx) {
|
|
94
|
+
const fields = input;
|
|
95
|
+
assertDateOrder(fields.startAfter, fields.dueBefore);
|
|
86
96
|
const body = await request(ctx, "/v1/issue-views", {
|
|
87
97
|
method: "POST",
|
|
88
|
-
body: JSON.stringify(toViewBody(
|
|
98
|
+
body: JSON.stringify(toViewBody(fields)),
|
|
89
99
|
});
|
|
90
100
|
return body.view;
|
|
91
101
|
},
|
|
@@ -94,27 +104,28 @@ export function registerIssueViewActions(rl) {
|
|
|
94
104
|
access: "write",
|
|
95
105
|
description: "Patch a saved Issue View. Null filter values remove that filter without replacing unrelated filters.",
|
|
96
106
|
inputSchema: t.Object({
|
|
97
|
-
id:
|
|
98
|
-
name: t.Optional(t.String({ minLength: 1, maxLength: 120 })),
|
|
107
|
+
id: idSchema("Issue View ID"),
|
|
108
|
+
name: t.Optional(t.String({ minLength: 1, maxLength: 120, pattern: "\\S" })),
|
|
99
109
|
visibility: t.Optional(t.Literal("organization")),
|
|
100
110
|
layout: t.Optional(enumSchema("View layout", ISSUE_VIEW_LAYOUT)),
|
|
101
|
-
projectId:
|
|
102
|
-
rootIssueId:
|
|
111
|
+
projectId: optionalNullableId("Project ID"),
|
|
112
|
+
rootIssueId: optionalNullableId("Root Issue ID"),
|
|
103
113
|
statuses: optionalNullableArray(enumSchema("Issue status", ISSUE_STATUS), 5),
|
|
104
114
|
priorities: optionalNullableArray(enumSchema("Issue priority", ISSUE_PRIORITY), 5),
|
|
105
|
-
assigneeUserIds: optionalNullableArray(
|
|
106
|
-
labels: optionalNullableArray(t.String({ minLength: 1, maxLength: 64 }), 20),
|
|
107
|
-
startAfter:
|
|
108
|
-
dueBefore:
|
|
115
|
+
assigneeUserIds: optionalNullableArray(idSchema("Assignee user ID"), 20),
|
|
116
|
+
labels: optionalNullableArray(t.String({ minLength: 1, maxLength: 64, pattern: "\\S" }), 20),
|
|
117
|
+
startAfter: optionalNullableTimestamp("ISO-8601 filter start timestamp"),
|
|
118
|
+
dueBefore: optionalNullableTimestamp("ISO-8601 filter due timestamp"),
|
|
109
119
|
blocked: t.Optional(t.Union([t.Boolean(), t.Null()])),
|
|
110
120
|
groupBy: t.Optional(t.Union([enumSchema("View grouping", ISSUE_VIEW_GROUP), t.Null()])),
|
|
111
121
|
sortBy: t.Optional(t.Array(enumSchema("View sort", ISSUE_VIEW_SORT), {
|
|
112
122
|
minItems: 1,
|
|
113
123
|
maxItems: 3,
|
|
114
124
|
})),
|
|
115
|
-
},
|
|
125
|
+
}, STRICT_UPDATE_OBJECT),
|
|
116
126
|
async execute(input, ctx) {
|
|
117
127
|
const { id, ...fields } = input;
|
|
128
|
+
assertDateOrder(fields.startAfter, fields.dueBefore);
|
|
118
129
|
const body = await request(ctx, `/v1/issue-views/${pathSegment(id)}`, { method: "PATCH", body: JSON.stringify(toViewBody(fields, false)) });
|
|
119
130
|
return body.view;
|
|
120
131
|
},
|
|
@@ -122,7 +133,7 @@ export function registerIssueViewActions(rl) {
|
|
|
122
133
|
rl.registerAction("issueView.delete", {
|
|
123
134
|
access: "write",
|
|
124
135
|
description: "Delete a saved Issue View without changing any Issues.",
|
|
125
|
-
inputSchema: t.Object({ id:
|
|
136
|
+
inputSchema: t.Object({ id: idSchema("Issue View ID") }, STRICT_OBJECT),
|
|
126
137
|
async execute(input, ctx) {
|
|
127
138
|
const { id } = input;
|
|
128
139
|
await request(ctx, `/v1/issue-views/${pathSegment(id)}`, {
|
|
@@ -144,8 +155,18 @@ function withQuery(path, params) {
|
|
|
144
155
|
const query = params.toString();
|
|
145
156
|
return query ? `${path}?${query}` : path;
|
|
146
157
|
}
|
|
147
|
-
function
|
|
148
|
-
|
|
158
|
+
function assertDateOrder(startAfter, dueBefore) {
|
|
159
|
+
if (typeof startAfter === "string" &&
|
|
160
|
+
typeof dueBefore === "string" &&
|
|
161
|
+
startAfter > dueBefore) {
|
|
162
|
+
throw new Error("View dueBefore must not precede startAfter");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function optionalNullableId(description) {
|
|
166
|
+
return t.Optional(t.Union([idSchema(description), t.Null()]));
|
|
167
|
+
}
|
|
168
|
+
function optionalNullableTimestamp(description) {
|
|
169
|
+
return t.Optional(t.Union([timestampSchema(description), t.Null()]));
|
|
149
170
|
}
|
|
150
171
|
function optionalNullableArray(item, maxItems) {
|
|
151
172
|
return t.Optional(t.Union([t.Array(item, { maxItems }), t.Null()]));
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { enumSchema, ISSUE_PRIORITY, ISSUE_SOURCE, ISSUE_STATUS, pathSegment, request, } from "./shared.js";
|
|
2
|
+
import { cursorSchema, enumSchema, ISSUE_PRIORITY, ISSUE_SOURCE, ISSUE_STATUS, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "./shared.js";
|
|
3
3
|
const issueListFields = {
|
|
4
4
|
status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
|
|
5
|
-
projectId: t.Optional(
|
|
6
|
-
parentIssueId: t.Optional(
|
|
7
|
-
assigneeUserId: t.Optional(
|
|
5
|
+
projectId: t.Optional(idSchema("Project ID")),
|
|
6
|
+
parentIssueId: t.Optional(idSchema("Parent Issue ID")),
|
|
7
|
+
assigneeUserId: t.Optional(idSchema("Assignee user ID")),
|
|
8
8
|
source: t.Optional(enumSchema("Issue source", ISSUE_SOURCE)),
|
|
9
|
-
|
|
9
|
+
includeArchived: t.Optional(t.Boolean()),
|
|
10
|
+
cursor: t.Optional(cursorSchema()),
|
|
10
11
|
limit: t.Optional(t.Integer({
|
|
11
12
|
minimum: 1,
|
|
12
13
|
maximum: 100,
|
|
@@ -29,7 +30,7 @@ export function registerIssueActions(rl) {
|
|
|
29
30
|
rl.registerAction("issue.list", {
|
|
30
31
|
access: "read",
|
|
31
32
|
description: "List the first page of Shift Labs Issues for the API key's organization.",
|
|
32
|
-
inputSchema: t.Object(issueListFields),
|
|
33
|
+
inputSchema: t.Object(issueListFields, STRICT_OBJECT),
|
|
33
34
|
async execute(input, ctx) {
|
|
34
35
|
const body = await request(ctx, `/v1/issues?${issueListParams(input)}`);
|
|
35
36
|
return body.issues;
|
|
@@ -38,7 +39,7 @@ export function registerIssueActions(rl) {
|
|
|
38
39
|
rl.registerAction("issue.listPage", {
|
|
39
40
|
access: "read",
|
|
40
41
|
description: "List a cursor-paginated page of Shift Labs Issues.",
|
|
41
|
-
inputSchema: t.Object(issueListFields),
|
|
42
|
+
inputSchema: t.Object(issueListFields, STRICT_OBJECT),
|
|
42
43
|
async execute(input, ctx) {
|
|
43
44
|
return request(ctx, `/v1/issues?${issueListParams(input)}`);
|
|
44
45
|
},
|
|
@@ -46,9 +47,7 @@ export function registerIssueActions(rl) {
|
|
|
46
47
|
rl.registerAction("issue.get", {
|
|
47
48
|
access: "read",
|
|
48
49
|
description: "Get a Shift Labs Issue by ID.",
|
|
49
|
-
inputSchema: t.Object({
|
|
50
|
-
id: t.String({ minLength: 1, description: "Issue ID" }),
|
|
51
|
-
}),
|
|
50
|
+
inputSchema: t.Object({ id: idSchema("Issue ID") }, STRICT_OBJECT),
|
|
52
51
|
async execute(input, ctx) {
|
|
53
52
|
const { id } = input;
|
|
54
53
|
const body = await request(ctx, `/v1/issues/${pathSegment(id)}`);
|
|
@@ -62,30 +61,37 @@ export function registerIssueActions(rl) {
|
|
|
62
61
|
title: t.String({
|
|
63
62
|
minLength: 1,
|
|
64
63
|
maxLength: 160,
|
|
64
|
+
pattern: "\\S",
|
|
65
65
|
description: "Issue title",
|
|
66
66
|
}),
|
|
67
67
|
description: t.Optional(t.String({ maxLength: 20_000, description: "Issue description" })),
|
|
68
|
-
projectId: t.Optional(
|
|
69
|
-
parentIssueId: t.Optional(
|
|
68
|
+
projectId: t.Optional(idSchema("Project ID")),
|
|
69
|
+
parentIssueId: t.Optional(idSchema("Parent Issue ID")),
|
|
70
70
|
sortOrder: t.Optional(t.Integer({ minimum: 0 })),
|
|
71
71
|
status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
|
|
72
72
|
priority: t.Optional(enumSchema("Issue priority", ISSUE_PRIORITY)),
|
|
73
73
|
source: t.Optional(enumSchema("Issue source", ISSUE_SOURCE)),
|
|
74
|
-
assigneeUserId: t.Optional(
|
|
75
|
-
startAt: t.Optional(
|
|
76
|
-
dueAt: t.Optional(
|
|
77
|
-
deploymentId: t.Optional(
|
|
78
|
-
workspaceId: t.Optional(
|
|
79
|
-
sessionId: t.Optional(
|
|
80
|
-
traceId: t.Optional(
|
|
81
|
-
fingerprint: t.Optional(t.String({ minLength: 1, maxLength: 512 })),
|
|
82
|
-
labels: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 64 }))),
|
|
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
83
|
metadata: t.Optional(t.Record(t.String(), t.Unknown(), { description: "Issue metadata" })),
|
|
84
|
-
}),
|
|
84
|
+
}, STRICT_OBJECT),
|
|
85
85
|
async execute(input, ctx) {
|
|
86
|
+
const fields = input;
|
|
87
|
+
if (fields.parentIssueId !== undefined &&
|
|
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");
|
|
86
92
|
const body = await request(ctx, "/v1/issues", {
|
|
87
93
|
method: "POST",
|
|
88
|
-
body: JSON.stringify(
|
|
94
|
+
body: JSON.stringify(fields),
|
|
89
95
|
});
|
|
90
96
|
return body.issue;
|
|
91
97
|
},
|
|
@@ -94,22 +100,23 @@ export function registerIssueActions(rl) {
|
|
|
94
100
|
access: "write",
|
|
95
101
|
description: "Update an Issue, including Project assignment, one-level parentage, ordering, status, dates, or assignee.",
|
|
96
102
|
inputSchema: t.Object({
|
|
97
|
-
id:
|
|
98
|
-
title: t.Optional(t.String({ minLength: 1, maxLength: 160 })),
|
|
103
|
+
id: idSchema("Issue ID"),
|
|
104
|
+
title: t.Optional(t.String({ minLength: 1, maxLength: 160, pattern: "\\S" })),
|
|
99
105
|
description: t.Optional(t.String({ maxLength: 20_000 })),
|
|
100
|
-
projectId: t.Optional(t.Union([
|
|
101
|
-
parentIssueId: t.Optional(t.Union([
|
|
106
|
+
projectId: t.Optional(t.Union([idSchema("Project ID"), t.Null()])),
|
|
107
|
+
parentIssueId: t.Optional(t.Union([idSchema("Parent Issue ID"), t.Null()])),
|
|
102
108
|
sortOrder: t.Optional(t.Integer({ minimum: 0 })),
|
|
103
109
|
status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
|
|
104
110
|
priority: t.Optional(enumSchema("Issue priority", ISSUE_PRIORITY)),
|
|
105
|
-
assigneeUserId: t.Optional(t.Union([
|
|
106
|
-
startAt: t.Optional(t.Union([
|
|
107
|
-
dueAt: t.Optional(t.Union([
|
|
108
|
-
labels: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 64 }))),
|
|
111
|
+
assigneeUserId: t.Optional(t.Union([idSchema("Assignee user ID"), t.Null()])),
|
|
112
|
+
startAt: t.Optional(t.Union([timestampSchema("ISO-8601 start timestamp"), t.Null()])),
|
|
113
|
+
dueAt: t.Optional(t.Union([timestampSchema("ISO-8601 due timestamp"), t.Null()])),
|
|
114
|
+
labels: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 64, pattern: "\\S" }))),
|
|
109
115
|
archived: t.Optional(t.Boolean()),
|
|
110
|
-
},
|
|
116
|
+
}, STRICT_UPDATE_OBJECT),
|
|
111
117
|
async execute(input, ctx) {
|
|
112
118
|
const { id, ...patch } = input;
|
|
119
|
+
assertDateOrder(patch.startAt, patch.dueAt, "Issue dueAt must not precede startAt");
|
|
113
120
|
const body = await request(ctx, `/v1/issues/${pathSegment(id)}`, { method: "PATCH", body: JSON.stringify(patch) });
|
|
114
121
|
return body.issue;
|
|
115
122
|
},
|
|
@@ -118,13 +125,14 @@ export function registerIssueActions(rl) {
|
|
|
118
125
|
access: "write",
|
|
119
126
|
description: "Add a comment to a Shift Labs Issue.",
|
|
120
127
|
inputSchema: t.Object({
|
|
121
|
-
id:
|
|
128
|
+
id: idSchema("Issue ID"),
|
|
122
129
|
body: t.String({
|
|
123
130
|
minLength: 1,
|
|
124
131
|
maxLength: 20_000,
|
|
132
|
+
pattern: "\\S",
|
|
125
133
|
description: "Comment body",
|
|
126
134
|
}),
|
|
127
|
-
}),
|
|
135
|
+
}, STRICT_OBJECT),
|
|
128
136
|
async execute(input, ctx) {
|
|
129
137
|
const { id, body } = input;
|
|
130
138
|
const response = await request(ctx, `/v1/issues/${pathSegment(id)}/comments`, { method: "POST", body: JSON.stringify({ body }) });
|
|
@@ -135,9 +143,9 @@ export function registerIssueActions(rl) {
|
|
|
135
143
|
access: "read",
|
|
136
144
|
description: "List the first page of blocking and blocked-by links.",
|
|
137
145
|
inputSchema: t.Object({
|
|
138
|
-
id:
|
|
146
|
+
id: idSchema("Issue ID"),
|
|
139
147
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
|
140
|
-
}),
|
|
148
|
+
}, STRICT_OBJECT),
|
|
141
149
|
async execute(input, ctx) {
|
|
142
150
|
const { id, ...pagination } = input;
|
|
143
151
|
const body = await request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`, issueListParams(pagination)));
|
|
@@ -148,10 +156,10 @@ export function registerIssueActions(rl) {
|
|
|
148
156
|
access: "read",
|
|
149
157
|
description: "List a cursor-paginated page of Issue dependencies.",
|
|
150
158
|
inputSchema: t.Object({
|
|
151
|
-
id:
|
|
152
|
-
cursor: t.Optional(
|
|
159
|
+
id: idSchema("Issue ID"),
|
|
160
|
+
cursor: t.Optional(cursorSchema()),
|
|
153
161
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
|
154
|
-
}),
|
|
162
|
+
}, STRICT_OBJECT),
|
|
155
163
|
async execute(input, ctx) {
|
|
156
164
|
const { id, ...pagination } = input;
|
|
157
165
|
return request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`, issueListParams(pagination)));
|
|
@@ -161,17 +169,14 @@ export function registerIssueActions(rl) {
|
|
|
161
169
|
access: "write",
|
|
162
170
|
description: "Declare that one Issue blocks another Issue.",
|
|
163
171
|
inputSchema: t.Object({
|
|
164
|
-
blockedIssueId:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}),
|
|
168
|
-
blockingIssueId: t.String({
|
|
169
|
-
minLength: 1,
|
|
170
|
-
description: "Issue that blocks it",
|
|
171
|
-
}),
|
|
172
|
-
}),
|
|
172
|
+
blockedIssueId: idSchema("Issue that is blocked"),
|
|
173
|
+
blockingIssueId: idSchema("Issue that blocks it"),
|
|
174
|
+
}, STRICT_OBJECT),
|
|
173
175
|
async execute(input, ctx) {
|
|
174
176
|
const { blockedIssueId, blockingIssueId } = input;
|
|
177
|
+
if (blockedIssueId === blockingIssueId) {
|
|
178
|
+
throw new Error("An Issue cannot block itself");
|
|
179
|
+
}
|
|
175
180
|
const body = await request(ctx, `/v1/issues/${pathSegment(blockedIssueId)}/dependencies`, { method: "POST", body: JSON.stringify({ blockingIssueId }) });
|
|
176
181
|
return body.dependency;
|
|
177
182
|
},
|
|
@@ -180,9 +185,9 @@ export function registerIssueActions(rl) {
|
|
|
180
185
|
access: "write",
|
|
181
186
|
description: "Remove an Issue dependency link.",
|
|
182
187
|
inputSchema: t.Object({
|
|
183
|
-
issueId:
|
|
184
|
-
dependencyId:
|
|
185
|
-
}),
|
|
188
|
+
issueId: idSchema("Issue ID"),
|
|
189
|
+
dependencyId: idSchema("Dependency ID"),
|
|
190
|
+
}, STRICT_OBJECT),
|
|
186
191
|
async execute(input, ctx) {
|
|
187
192
|
const { issueId, dependencyId } = input;
|
|
188
193
|
await request(ctx, `/v1/issues/${pathSegment(issueId)}/dependencies/${pathSegment(dependencyId)}`, { method: "DELETE" });
|
|
@@ -190,3 +195,8 @@ export function registerIssueActions(rl) {
|
|
|
190
195
|
},
|
|
191
196
|
});
|
|
192
197
|
}
|
|
198
|
+
function assertDateOrder(start, end, message) {
|
|
199
|
+
if (typeof start === "string" && typeof end === "string" && start > end) {
|
|
200
|
+
throw new Error(message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { enumSchema, PAGE_STATUS, PAGE_VISIBILITY, pageRenderUrl, request, } from "./shared.js";
|
|
2
|
+
import { enumSchema, idSchema, PAGE_STATUS, PAGE_VISIBILITY, pageRenderUrl, pathSegment, request, STRICT_OBJECT, timestampSchema, } from "./shared.js";
|
|
3
|
+
const PageSlug = t.String({
|
|
4
|
+
minLength: 1,
|
|
5
|
+
maxLength: 120,
|
|
6
|
+
pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
7
|
+
description: "Lowercase kebab-case page slug",
|
|
8
|
+
});
|
|
3
9
|
export function registerPageActions(rl) {
|
|
4
10
|
rl.registerAction("page.list", {
|
|
5
11
|
access: "read",
|
|
6
12
|
description: "List Shift Labs pages for the API key's organization.",
|
|
7
13
|
inputSchema: t.Object({
|
|
8
14
|
status: t.Optional(enumSchema("Page status", PAGE_STATUS)),
|
|
9
|
-
limit: t.Optional(t.
|
|
10
|
-
|
|
15
|
+
limit: t.Optional(t.Integer({
|
|
16
|
+
minimum: 1,
|
|
17
|
+
maximum: 100,
|
|
18
|
+
description: "Max results, default 50",
|
|
19
|
+
})),
|
|
20
|
+
}, STRICT_OBJECT),
|
|
11
21
|
async execute(input, ctx) {
|
|
12
22
|
const fields = (input ?? {});
|
|
13
23
|
const params = new URLSearchParams();
|
|
@@ -22,10 +32,10 @@ export function registerPageActions(rl) {
|
|
|
22
32
|
rl.registerAction("page.get", {
|
|
23
33
|
access: "read",
|
|
24
34
|
description: "Get a Shift Labs page by ID.",
|
|
25
|
-
inputSchema: t.Object({ id:
|
|
35
|
+
inputSchema: t.Object({ id: idSchema("Page ID") }, STRICT_OBJECT),
|
|
26
36
|
async execute(input, ctx) {
|
|
27
37
|
const { id } = input;
|
|
28
|
-
const body = await request(ctx, `/v1/pages/${
|
|
38
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(id)}`);
|
|
29
39
|
return body.page;
|
|
30
40
|
},
|
|
31
41
|
});
|
|
@@ -33,11 +43,19 @@ export function registerPageActions(rl) {
|
|
|
33
43
|
access: "write",
|
|
34
44
|
description: "Create a draft hosted HTML page. Agents can publish it with page.publish.",
|
|
35
45
|
inputSchema: t.Object({
|
|
36
|
-
slug:
|
|
37
|
-
title: t.String({
|
|
38
|
-
|
|
46
|
+
slug: PageSlug,
|
|
47
|
+
title: t.String({
|
|
48
|
+
minLength: 1,
|
|
49
|
+
maxLength: 160,
|
|
50
|
+
pattern: "\\S",
|
|
51
|
+
description: "Page title",
|
|
52
|
+
}),
|
|
53
|
+
html: t.Optional(t.String({
|
|
54
|
+
maxLength: 1_000_000,
|
|
55
|
+
description: "Hosted HTML content",
|
|
56
|
+
})),
|
|
39
57
|
visibility: t.Optional(enumSchema("Page visibility", PAGE_VISIBILITY)),
|
|
40
|
-
}),
|
|
58
|
+
}, STRICT_OBJECT),
|
|
41
59
|
async execute(input, ctx) {
|
|
42
60
|
const fields = input;
|
|
43
61
|
const body = await request(ctx, "/v1/pages", {
|
|
@@ -55,15 +73,23 @@ export function registerPageActions(rl) {
|
|
|
55
73
|
access: "write",
|
|
56
74
|
description: "Update a hosted HTML page's slug, title, visibility, or HTML.",
|
|
57
75
|
inputSchema: t.Object({
|
|
58
|
-
id:
|
|
59
|
-
slug: t.Optional(
|
|
60
|
-
title: t.Optional(t.String({
|
|
76
|
+
id: idSchema("Page ID"),
|
|
77
|
+
slug: t.Optional(PageSlug),
|
|
78
|
+
title: t.Optional(t.String({
|
|
79
|
+
minLength: 1,
|
|
80
|
+
maxLength: 160,
|
|
81
|
+
pattern: "\\S",
|
|
82
|
+
description: "Page title",
|
|
83
|
+
})),
|
|
61
84
|
visibility: t.Optional(enumSchema("Page visibility", PAGE_VISIBILITY)),
|
|
62
|
-
html: t.Optional(t.String({
|
|
63
|
-
|
|
85
|
+
html: t.Optional(t.String({
|
|
86
|
+
maxLength: 1_000_000,
|
|
87
|
+
description: "Hosted HTML content",
|
|
88
|
+
})),
|
|
89
|
+
}, STRICT_OBJECT),
|
|
64
90
|
async execute(input, ctx) {
|
|
65
91
|
const { id, ...fields } = input;
|
|
66
|
-
const body = await request(ctx, `/v1/pages/${
|
|
92
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(String(id))}`, {
|
|
67
93
|
method: "PATCH",
|
|
68
94
|
body: JSON.stringify(fields),
|
|
69
95
|
});
|
|
@@ -73,30 +99,30 @@ export function registerPageActions(rl) {
|
|
|
73
99
|
rl.registerAction("page.publish", {
|
|
74
100
|
access: "write",
|
|
75
101
|
description: "Publish a Shift Labs hosted HTML page.",
|
|
76
|
-
inputSchema: t.Object({ id:
|
|
102
|
+
inputSchema: t.Object({ id: idSchema("Page ID") }, STRICT_OBJECT),
|
|
77
103
|
async execute(input, ctx) {
|
|
78
104
|
const { id } = input;
|
|
79
|
-
const body = await request(ctx, `/v1/pages/${
|
|
105
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(id)}/publish`, { method: "POST" });
|
|
80
106
|
return body.page;
|
|
81
107
|
},
|
|
82
108
|
});
|
|
83
109
|
rl.registerAction("page.archive", {
|
|
84
110
|
access: "write",
|
|
85
111
|
description: "Archive a Shift Labs page.",
|
|
86
|
-
inputSchema: t.Object({ id:
|
|
112
|
+
inputSchema: t.Object({ id: idSchema("Page ID") }, STRICT_OBJECT),
|
|
87
113
|
async execute(input, ctx) {
|
|
88
114
|
const { id } = input;
|
|
89
|
-
const body = await request(ctx, `/v1/pages/${
|
|
115
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(id)}/archive`, { method: "POST" });
|
|
90
116
|
return body.page;
|
|
91
117
|
},
|
|
92
118
|
});
|
|
93
119
|
rl.registerAction("page.shares", {
|
|
94
120
|
access: "read",
|
|
95
121
|
description: "List shares for a Shift Labs page.",
|
|
96
|
-
inputSchema: t.Object({ pageId:
|
|
122
|
+
inputSchema: t.Object({ pageId: idSchema("Page ID") }, STRICT_OBJECT),
|
|
97
123
|
async execute(input, ctx) {
|
|
98
124
|
const { pageId } = input;
|
|
99
|
-
const body = await request(ctx, `/v1/pages/${
|
|
125
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(pageId)}/shares`);
|
|
100
126
|
return body.shares;
|
|
101
127
|
},
|
|
102
128
|
});
|
|
@@ -104,13 +130,17 @@ export function registerPageActions(rl) {
|
|
|
104
130
|
access: "write",
|
|
105
131
|
description: "Create a viewer share for a Shift Labs page.",
|
|
106
132
|
inputSchema: t.Object({
|
|
107
|
-
pageId:
|
|
108
|
-
email: t.String({
|
|
109
|
-
|
|
110
|
-
|
|
133
|
+
pageId: idSchema("Page ID"),
|
|
134
|
+
email: t.String({
|
|
135
|
+
minLength: 1,
|
|
136
|
+
format: "email",
|
|
137
|
+
description: "Viewer email address",
|
|
138
|
+
}),
|
|
139
|
+
expiresAt: t.Optional(timestampSchema("Optional ISO-8601 expiration")),
|
|
140
|
+
}, STRICT_OBJECT),
|
|
111
141
|
async execute(input, ctx) {
|
|
112
142
|
const { pageId, ...fields } = input;
|
|
113
|
-
const body = await request(ctx, `/v1/pages/${
|
|
143
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(String(pageId))}/shares`, {
|
|
114
144
|
method: "POST",
|
|
115
145
|
body: JSON.stringify(fields),
|
|
116
146
|
});
|
|
@@ -120,12 +150,10 @@ export function registerPageActions(rl) {
|
|
|
120
150
|
rl.registerAction("page.revokeShare", {
|
|
121
151
|
access: "write",
|
|
122
152
|
description: "Revoke a Shift Labs page share.",
|
|
123
|
-
inputSchema: t.Object({
|
|
124
|
-
shareId: t.String({ description: "Page share ID" }),
|
|
125
|
-
}),
|
|
153
|
+
inputSchema: t.Object({ shareId: idSchema("Page share ID") }, STRICT_OBJECT),
|
|
126
154
|
async execute(input, ctx) {
|
|
127
155
|
const { shareId } = input;
|
|
128
|
-
await request(ctx, `/v1/pages/shares/${
|
|
156
|
+
await request(ctx, `/v1/pages/shares/${pathSegment(shareId)}`, {
|
|
129
157
|
method: "DELETE",
|
|
130
158
|
});
|
|
131
159
|
return { success: true };
|
|
@@ -134,12 +162,10 @@ export function registerPageActions(rl) {
|
|
|
134
162
|
rl.registerAction("page.renderUrl", {
|
|
135
163
|
access: "read",
|
|
136
164
|
description: "Return the authenticated render URL for a page.",
|
|
137
|
-
inputSchema: t.Object({
|
|
138
|
-
pageId: t.String({ description: "Page ID" }),
|
|
139
|
-
}),
|
|
165
|
+
inputSchema: t.Object({ pageId: idSchema("Page ID") }, STRICT_OBJECT),
|
|
140
166
|
async execute(input, ctx) {
|
|
141
167
|
const { pageId } = input;
|
|
142
|
-
const body = await request(ctx, `/v1/pages/${
|
|
168
|
+
const body = await request(ctx, `/v1/pages/${pathSegment(pageId)}`);
|
|
143
169
|
return {
|
|
144
170
|
url: pageRenderUrl(body.page.organizationId, body.page.slug),
|
|
145
171
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
-
import { enumSchema, PROJECT_STATUS, pathSegment, request } from "./shared.js";
|
|
2
|
+
import { cursorSchema, enumSchema, idSchema, 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
|
-
leadUserId: t.Optional(
|
|
5
|
+
leadUserId: t.Optional(idSchema("Lead user ID")),
|
|
6
6
|
includeArchived: t.Optional(t.Boolean()),
|
|
7
|
-
cursor: t.Optional(
|
|
7
|
+
cursor: t.Optional(cursorSchema()),
|
|
8
8
|
limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
|
|
9
9
|
};
|
|
10
10
|
function listParams(input) {
|
|
@@ -19,7 +19,7 @@ export function registerProjectActions(rl) {
|
|
|
19
19
|
rl.registerAction("project.list", {
|
|
20
20
|
access: "read",
|
|
21
21
|
description: "List the first page of concrete Shift Labs Projects.",
|
|
22
|
-
inputSchema: t.Object(projectListFields),
|
|
22
|
+
inputSchema: t.Object(projectListFields, STRICT_OBJECT),
|
|
23
23
|
async execute(input, ctx) {
|
|
24
24
|
const body = await request(ctx, `/v1/projects?${listParams(input)}`);
|
|
25
25
|
return body.projects;
|
|
@@ -28,7 +28,7 @@ export function registerProjectActions(rl) {
|
|
|
28
28
|
rl.registerAction("project.listPage", {
|
|
29
29
|
access: "read",
|
|
30
30
|
description: "List a cursor-paginated page of concrete Projects.",
|
|
31
|
-
inputSchema: t.Object(projectListFields),
|
|
31
|
+
inputSchema: t.Object(projectListFields, STRICT_OBJECT),
|
|
32
32
|
async execute(input, ctx) {
|
|
33
33
|
return request(ctx, `/v1/projects?${listParams(input)}`);
|
|
34
34
|
},
|
|
@@ -36,9 +36,7 @@ export function registerProjectActions(rl) {
|
|
|
36
36
|
rl.registerAction("project.get", {
|
|
37
37
|
access: "read",
|
|
38
38
|
description: "Get a Project and its derived Issue progress.",
|
|
39
|
-
inputSchema: t.Object({
|
|
40
|
-
id: t.String({ minLength: 1, description: "Project ID" }),
|
|
41
|
-
}),
|
|
39
|
+
inputSchema: t.Object({ id: idSchema("Project ID") }, STRICT_OBJECT),
|
|
42
40
|
async execute(input, ctx) {
|
|
43
41
|
const { id } = input;
|
|
44
42
|
const [project, progress] = await Promise.all([
|
|
@@ -55,18 +53,21 @@ export function registerProjectActions(rl) {
|
|
|
55
53
|
name: t.String({
|
|
56
54
|
minLength: 1,
|
|
57
55
|
maxLength: 160,
|
|
56
|
+
pattern: "\\S",
|
|
58
57
|
description: "Project name",
|
|
59
58
|
}),
|
|
60
59
|
description: t.Optional(t.String({ maxLength: 20_000 })),
|
|
61
60
|
status: t.Optional(enumSchema("Project status", PROJECT_STATUS)),
|
|
62
|
-
leadUserId: t.Optional(
|
|
63
|
-
startAt: t.Optional(
|
|
64
|
-
targetAt: t.Optional(
|
|
65
|
-
}),
|
|
61
|
+
leadUserId: t.Optional(idSchema("Lead user ID")),
|
|
62
|
+
startAt: t.Optional(timestampSchema("ISO-8601 start timestamp")),
|
|
63
|
+
targetAt: t.Optional(timestampSchema("ISO-8601 target timestamp")),
|
|
64
|
+
}, STRICT_OBJECT),
|
|
66
65
|
async execute(input, ctx) {
|
|
66
|
+
const fields = input;
|
|
67
|
+
assertDateOrder(fields.startAt, fields.targetAt);
|
|
67
68
|
const body = await request(ctx, "/v1/projects", {
|
|
68
69
|
method: "POST",
|
|
69
|
-
body: JSON.stringify(
|
|
70
|
+
body: JSON.stringify(fields),
|
|
70
71
|
});
|
|
71
72
|
return body.project;
|
|
72
73
|
},
|
|
@@ -75,19 +76,27 @@ export function registerProjectActions(rl) {
|
|
|
75
76
|
access: "write",
|
|
76
77
|
description: "Update Project lifecycle, ownership, dates, or content.",
|
|
77
78
|
inputSchema: t.Object({
|
|
78
|
-
id:
|
|
79
|
-
name: t.Optional(t.String({ minLength: 1, maxLength: 160 })),
|
|
79
|
+
id: idSchema("Project ID"),
|
|
80
|
+
name: t.Optional(t.String({ minLength: 1, maxLength: 160, pattern: "\\S" })),
|
|
80
81
|
description: t.Optional(t.String({ maxLength: 20_000 })),
|
|
81
82
|
status: t.Optional(enumSchema("Project status", PROJECT_STATUS)),
|
|
82
|
-
leadUserId: t.Optional(t.Union([
|
|
83
|
-
startAt: t.Optional(t.Union([
|
|
84
|
-
targetAt: t.Optional(t.Union([
|
|
83
|
+
leadUserId: t.Optional(t.Union([idSchema("Lead user ID"), t.Null()])),
|
|
84
|
+
startAt: t.Optional(t.Union([timestampSchema("ISO-8601 start timestamp"), t.Null()])),
|
|
85
|
+
targetAt: t.Optional(t.Union([timestampSchema("ISO-8601 target timestamp"), t.Null()])),
|
|
85
86
|
archived: t.Optional(t.Boolean()),
|
|
86
|
-
},
|
|
87
|
+
}, STRICT_UPDATE_OBJECT),
|
|
87
88
|
async execute(input, ctx) {
|
|
88
89
|
const { id, ...patch } = input;
|
|
90
|
+
assertDateOrder(patch.startAt, patch.targetAt);
|
|
89
91
|
const body = await request(ctx, `/v1/projects/${pathSegment(id)}`, { method: "PATCH", body: JSON.stringify(patch) });
|
|
90
92
|
return body.project;
|
|
91
93
|
},
|
|
92
94
|
});
|
|
93
95
|
}
|
|
96
|
+
function assertDateOrder(startAt, targetAt) {
|
|
97
|
+
if (typeof startAt === "string" &&
|
|
98
|
+
typeof targetAt === "string" &&
|
|
99
|
+
startAt > targetAt) {
|
|
100
|
+
throw new Error("Project targetAt must not precede startAt");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import * as t from "typebox";
|
|
2
|
+
export const STRICT_OBJECT = { additionalProperties: false };
|
|
3
|
+
export const STRICT_UPDATE_OBJECT = {
|
|
4
|
+
additionalProperties: false,
|
|
5
|
+
minProperties: 2,
|
|
6
|
+
};
|
|
7
|
+
export function idSchema(description) {
|
|
8
|
+
return t.String({ minLength: 1, pattern: "\\S", description });
|
|
9
|
+
}
|
|
10
|
+
export function cursorSchema(description = "Next-page cursor") {
|
|
11
|
+
return t.String({ minLength: 1, maxLength: 512, description });
|
|
12
|
+
}
|
|
13
|
+
export function timestampSchema(description) {
|
|
14
|
+
return t.String({
|
|
15
|
+
format: "date-time",
|
|
16
|
+
pattern: "Z$",
|
|
17
|
+
description,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
2
20
|
const SHIFT_LABS_API_URL = "https://d1ood6y5zobtne.cloudfront.net";
|
|
3
21
|
export function baseUrl() {
|
|
4
22
|
return `${SHIFT_LABS_API_URL}/`;
|