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({ minLength: 1, maxLength: 120, description: "View name" }),
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(t.String({ minLength: 1 })),
8
- rootIssueId: t.Optional(t.String({ minLength: 1 })),
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(t.String({ minLength: 1 }), { maxItems: 20 })),
12
- labels: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 64 }), { maxItems: 20 })),
13
- startAfter: t.Optional(t.String()),
14
- dueBefore: t.Optional(t.String()),
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
- cursor: t.Optional(t.String({ minLength: 1, description: "Next-page cursor" })),
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: t.String() }),
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: t.String({ minLength: 1 }),
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: t.String({ minLength: 1 }),
73
- cursor: t.Optional(t.String({ minLength: 1 })),
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(input)),
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: t.String({ minLength: 1 }),
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: optionalNullableString(),
102
- rootIssueId: optionalNullableString(),
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(t.String({ minLength: 1 }), 20),
106
- labels: optionalNullableArray(t.String({ minLength: 1, maxLength: 64 }), 20),
107
- startAfter: optionalNullableString(),
108
- dueBefore: optionalNullableString(),
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
- }, { minProperties: 2 }),
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: t.String() }),
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 optionalNullableString() {
148
- return t.Optional(t.Union([t.String({ minLength: 1 }), t.Null()]));
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(t.String({ minLength: 1 })),
6
- parentIssueId: t.Optional(t.String({ minLength: 1 })),
7
- assigneeUserId: t.Optional(t.String({ minLength: 1 })),
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
- cursor: t.Optional(t.String({ minLength: 1, description: "Next-page cursor" })),
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(t.String({ minLength: 1 })),
69
- parentIssueId: t.Optional(t.String({ minLength: 1 })),
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(t.String({ minLength: 1 })),
75
- startAt: t.Optional(t.String({ description: "ISO-8601 start timestamp" })),
76
- dueAt: t.Optional(t.String({ description: "ISO-8601 due timestamp" })),
77
- deploymentId: t.Optional(t.String({ minLength: 1 })),
78
- workspaceId: t.Optional(t.String({ minLength: 1 })),
79
- sessionId: t.Optional(t.String({ minLength: 1 })),
80
- traceId: t.Optional(t.String({ minLength: 1 })),
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(input),
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: t.String({ minLength: 1, description: "Issue 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([t.String({ minLength: 1 }), t.Null()])),
101
- parentIssueId: t.Optional(t.Union([t.String({ minLength: 1 }), t.Null()])),
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([t.String({ minLength: 1 }), t.Null()])),
106
- startAt: t.Optional(t.Union([t.String(), t.Null()])),
107
- dueAt: t.Optional(t.Union([t.String(), t.Null()])),
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
- }, { minProperties: 2 }),
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: t.String({ minLength: 1, description: "Issue 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: t.String({ minLength: 1, description: "Issue 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: t.String({ minLength: 1, description: "Issue ID" }),
152
- cursor: t.Optional(t.String({ minLength: 1 })),
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: t.String({
165
- minLength: 1,
166
- description: "Issue that is blocked",
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: t.String({ minLength: 1 }),
184
- dependencyId: t.String({ minLength: 1 }),
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.Number({ description: "Max results, default 50" })),
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: t.String({ description: "Page 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/${encodeURIComponent(id)}`);
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: t.String({ description: "Lowercase kebab-case page slug" }),
37
- title: t.String({ description: "Page title" }),
38
- html: t.Optional(t.String({ description: "Hosted HTML content" })),
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: t.String({ description: "Page ID" }),
59
- slug: t.Optional(t.String({ description: "Lowercase kebab-case page slug" })),
60
- title: t.Optional(t.String({ description: "Page title" })),
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({ description: "Hosted HTML content" })),
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/${encodeURIComponent(id)}`, {
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: t.String({ description: "Page 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/${encodeURIComponent(id)}/publish`, { method: "POST" });
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: t.String({ description: "Page 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/${encodeURIComponent(id)}/archive`, { method: "POST" });
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: t.String({ description: "Page ID" }) }),
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/${encodeURIComponent(pageId)}/shares`);
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: t.String({ description: "Page ID" }),
108
- email: t.String({ description: "Viewer email address" }),
109
- expiresAt: t.Optional(t.String({ description: "Optional ISO expiration" })),
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/${encodeURIComponent(pageId)}/shares`, {
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/${encodeURIComponent(shareId)}`, {
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/${encodeURIComponent(pageId)}`);
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(t.String({ minLength: 1 })),
5
+ leadUserId: t.Optional(idSchema("Lead user ID")),
6
6
  includeArchived: t.Optional(t.Boolean()),
7
- cursor: t.Optional(t.String({ minLength: 1, description: "Next-page cursor" })),
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(t.String({ minLength: 1 })),
63
- startAt: t.Optional(t.String({ description: "ISO-8601 start timestamp" })),
64
- targetAt: t.Optional(t.String({ description: "ISO-8601 target timestamp" })),
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(input),
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: t.String({ minLength: 1, description: "Project 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([t.String({ minLength: 1 }), t.Null()])),
83
- startAt: t.Optional(t.Union([t.String(), t.Null()])),
84
- targetAt: t.Optional(t.Union([t.String(), t.Null()])),
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
- }, { minProperties: 2 }),
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}/`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runline",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Code mode for agents — turn any API or command into a callable action",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",