runline 0.13.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,6 +1,8 @@
1
1
  import * as t from "typebox";
2
+ import { registerIssueViewActions } from "./issue-views.js";
2
3
  import { registerIssueActions } from "./issues.js";
3
4
  import { registerPageActions } from "./pages.js";
5
+ import { registerProjectActions } from "./projects.js";
4
6
  import { registerTranscriptionActions } from "./transcription.js";
5
7
  export default function shiftLabs(rl) {
6
8
  rl.setName("shiftLabs");
@@ -15,7 +17,9 @@ export default function shiftLabs(rl) {
15
17
  env: "SHIFT_LABS_ORG_ID",
16
18
  })),
17
19
  }));
20
+ registerProjectActions(rl);
18
21
  registerIssueActions(rl);
22
+ registerIssueViewActions(rl);
19
23
  registerPageActions(rl);
20
24
  registerTranscriptionActions(rl);
21
25
  }
@@ -0,0 +1,200 @@
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";
3
+ const viewFields = {
4
+ name: t.String({
5
+ minLength: 1,
6
+ maxLength: 120,
7
+ pattern: "\\S",
8
+ description: "View name",
9
+ }),
10
+ visibility: t.Optional(t.Literal("organization")),
11
+ layout: t.Optional(enumSchema("View layout", ISSUE_VIEW_LAYOUT)),
12
+ projectId: t.Optional(idSchema("Project ID")),
13
+ rootIssueId: t.Optional(idSchema("Root Issue ID")),
14
+ statuses: t.Optional(t.Array(enumSchema("Issue status", ISSUE_STATUS), { maxItems: 5 })),
15
+ priorities: t.Optional(t.Array(enumSchema("Issue priority", ISSUE_PRIORITY), { maxItems: 5 })),
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")),
22
+ blocked: t.Optional(t.Boolean()),
23
+ groupBy: t.Optional(enumSchema("View grouping", ISSUE_VIEW_GROUP)),
24
+ sortBy: t.Optional(t.Array(enumSchema("View sort", ISSUE_VIEW_SORT), {
25
+ minItems: 1,
26
+ maxItems: 3,
27
+ })),
28
+ };
29
+ export function registerIssueViewActions(rl) {
30
+ const listFields = {
31
+ visibility: t.Optional(enumSchema("View visibility", ISSUE_VIEW_VISIBILITY)),
32
+ createdById: t.Optional(idSchema("Creator ID")),
33
+ cursor: t.Optional(cursorSchema()),
34
+ limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
35
+ };
36
+ rl.registerAction("issueView.list", {
37
+ access: "read",
38
+ description: "List the first page of personal and organization Issue Views.",
39
+ inputSchema: t.Object(listFields, STRICT_OBJECT),
40
+ async execute(input, ctx) {
41
+ const body = await request(ctx, `/v1/issue-views?${listParams(input)}`);
42
+ return body.views;
43
+ },
44
+ });
45
+ rl.registerAction("issueView.listPage", {
46
+ access: "read",
47
+ description: "List a cursor-paginated page of Issue Views.",
48
+ inputSchema: t.Object(listFields, STRICT_OBJECT),
49
+ async execute(input, ctx) {
50
+ return request(ctx, `/v1/issue-views?${listParams(input)}`);
51
+ },
52
+ });
53
+ rl.registerAction("issueView.get", {
54
+ access: "read",
55
+ description: "Get an Issue View by ID.",
56
+ inputSchema: t.Object({ id: idSchema("Issue View ID") }, STRICT_OBJECT),
57
+ async execute(input, ctx) {
58
+ const { id } = input;
59
+ const body = await request(ctx, `/v1/issue-views/${pathSegment(id)}`);
60
+ return body.view;
61
+ },
62
+ });
63
+ rl.registerAction("issueView.issues", {
64
+ access: "read",
65
+ description: "Execute the first page of a saved Issue View.",
66
+ inputSchema: t.Object({
67
+ id: idSchema("Issue View ID"),
68
+ limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
69
+ }, STRICT_OBJECT),
70
+ async execute(input, ctx) {
71
+ const { id, ...pagination } = input;
72
+ const body = await request(ctx, withQuery(`/v1/issue-views/${pathSegment(id)}/issues`, listParams(pagination)));
73
+ return body.issues;
74
+ },
75
+ });
76
+ rl.registerAction("issueView.issuesPage", {
77
+ access: "read",
78
+ description: "Execute a cursor-paginated page of a saved Issue View.",
79
+ inputSchema: t.Object({
80
+ id: idSchema("Issue View ID"),
81
+ cursor: t.Optional(cursorSchema()),
82
+ limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
83
+ }, STRICT_OBJECT),
84
+ async execute(input, ctx) {
85
+ const { id, ...pagination } = input;
86
+ return request(ctx, withQuery(`/v1/issue-views/${pathSegment(id)}/issues`, listParams(pagination)));
87
+ },
88
+ });
89
+ rl.registerAction("issueView.create", {
90
+ access: "write",
91
+ description: "Create a bounded saved Issue query. Views never own Issue membership.",
92
+ inputSchema: t.Object(viewFields, STRICT_OBJECT),
93
+ async execute(input, ctx) {
94
+ const fields = input;
95
+ assertDateOrder(fields.startAfter, fields.dueBefore);
96
+ const body = await request(ctx, "/v1/issue-views", {
97
+ method: "POST",
98
+ body: JSON.stringify(toViewBody(fields)),
99
+ });
100
+ return body.view;
101
+ },
102
+ });
103
+ rl.registerAction("issueView.update", {
104
+ access: "write",
105
+ description: "Patch a saved Issue View. Null filter values remove that filter without replacing unrelated filters.",
106
+ inputSchema: t.Object({
107
+ id: idSchema("Issue View ID"),
108
+ name: t.Optional(t.String({ minLength: 1, maxLength: 120, pattern: "\\S" })),
109
+ visibility: t.Optional(t.Literal("organization")),
110
+ layout: t.Optional(enumSchema("View layout", ISSUE_VIEW_LAYOUT)),
111
+ projectId: optionalNullableId("Project ID"),
112
+ rootIssueId: optionalNullableId("Root Issue ID"),
113
+ statuses: optionalNullableArray(enumSchema("Issue status", ISSUE_STATUS), 5),
114
+ priorities: optionalNullableArray(enumSchema("Issue priority", ISSUE_PRIORITY), 5),
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"),
119
+ blocked: t.Optional(t.Union([t.Boolean(), t.Null()])),
120
+ groupBy: t.Optional(t.Union([enumSchema("View grouping", ISSUE_VIEW_GROUP), t.Null()])),
121
+ sortBy: t.Optional(t.Array(enumSchema("View sort", ISSUE_VIEW_SORT), {
122
+ minItems: 1,
123
+ maxItems: 3,
124
+ })),
125
+ }, STRICT_UPDATE_OBJECT),
126
+ async execute(input, ctx) {
127
+ const { id, ...fields } = input;
128
+ assertDateOrder(fields.startAfter, fields.dueBefore);
129
+ const body = await request(ctx, `/v1/issue-views/${pathSegment(id)}`, { method: "PATCH", body: JSON.stringify(toViewBody(fields, false)) });
130
+ return body.view;
131
+ },
132
+ });
133
+ rl.registerAction("issueView.delete", {
134
+ access: "write",
135
+ description: "Delete a saved Issue View without changing any Issues.",
136
+ inputSchema: t.Object({ id: idSchema("Issue View ID") }, STRICT_OBJECT),
137
+ async execute(input, ctx) {
138
+ const { id } = input;
139
+ await request(ctx, `/v1/issue-views/${pathSegment(id)}`, {
140
+ method: "DELETE",
141
+ });
142
+ return { deleted: true };
143
+ },
144
+ });
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
+ 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()]));
170
+ }
171
+ function optionalNullableArray(item, maxItems) {
172
+ return t.Optional(t.Union([t.Array(item, { maxItems }), t.Null()]));
173
+ }
174
+ function toViewBody(input, applyDefaults = true) {
175
+ const { projectId, rootIssueId, statuses, priorities, assigneeUserIds, labels, startAfter, dueBefore, blocked, ...view } = input;
176
+ const filters = Object.fromEntries(Object.entries({
177
+ projectId,
178
+ rootIssueId,
179
+ statuses,
180
+ priorities,
181
+ assigneeUserIds,
182
+ labels,
183
+ startAfter,
184
+ dueBefore,
185
+ blocked,
186
+ }).filter(([, value]) => value !== undefined));
187
+ return {
188
+ ...view,
189
+ ...(applyDefaults
190
+ ? {
191
+ visibility: view.visibility ?? "organization",
192
+ layout: view.layout ?? "list",
193
+ }
194
+ : {}),
195
+ ...(Object.keys(filters).length > 0 ? { filters } : {}),
196
+ ...(applyDefaults && view.sortBy === undefined
197
+ ? { sortBy: ["updatedAt"] }
198
+ : {}),
199
+ };
200
+ }
@@ -1,74 +1,202 @@
1
1
  import * as t from "typebox";
2
- import { enumSchema, ISSUE_KIND, ISSUE_PRIORITY, ISSUE_SEVERITY, ISSUE_SOURCE, ISSUE_STATUS, 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
+ const issueListFields = {
4
+ status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
5
+ projectId: t.Optional(idSchema("Project ID")),
6
+ parentIssueId: t.Optional(idSchema("Parent Issue ID")),
7
+ assigneeUserId: t.Optional(idSchema("Assignee user ID")),
8
+ source: t.Optional(enumSchema("Issue source", ISSUE_SOURCE)),
9
+ includeArchived: t.Optional(t.Boolean()),
10
+ cursor: t.Optional(cursorSchema()),
11
+ limit: t.Optional(t.Integer({
12
+ minimum: 1,
13
+ maximum: 100,
14
+ description: "Max results, default 50",
15
+ })),
16
+ };
17
+ function issueListParams(input) {
18
+ const params = new URLSearchParams();
19
+ for (const [key, value] of Object.entries((input ?? {}))) {
20
+ if (value !== undefined)
21
+ params.set(key, String(value));
22
+ }
23
+ return params;
24
+ }
25
+ function withQuery(path, params) {
26
+ const query = params.toString();
27
+ return query ? `${path}?${query}` : path;
28
+ }
3
29
  export function registerIssueActions(rl) {
4
30
  rl.registerAction("issue.list", {
5
31
  access: "read",
6
- description: "List Shift Labs issues for the API key's organization.",
7
- inputSchema: t.Object({
8
- status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
9
- assigneeUserId: t.Optional(t.String()),
10
- source: t.Optional(enumSchema("Issue source", ISSUE_SOURCE)),
11
- limit: t.Optional(t.Number({ description: "Max results, default 50" })),
12
- }),
32
+ description: "List the first page of Shift Labs Issues for the API key's organization.",
33
+ inputSchema: t.Object(issueListFields, STRICT_OBJECT),
13
34
  async execute(input, ctx) {
14
- const fields = (input ?? {});
15
- const params = new URLSearchParams();
16
- for (const key of ["status", "assigneeUserId", "source", "limit"]) {
17
- const value = fields[key];
18
- if (value !== undefined)
19
- params.set(key, String(value));
20
- }
21
- const body = await request(ctx, `/v1/issues?${params}`);
35
+ const body = await request(ctx, `/v1/issues?${issueListParams(input)}`);
22
36
  return body.issues;
23
37
  },
24
38
  });
39
+ rl.registerAction("issue.listPage", {
40
+ access: "read",
41
+ description: "List a cursor-paginated page of Shift Labs Issues.",
42
+ inputSchema: t.Object(issueListFields, STRICT_OBJECT),
43
+ async execute(input, ctx) {
44
+ return request(ctx, `/v1/issues?${issueListParams(input)}`);
45
+ },
46
+ });
25
47
  rl.registerAction("issue.get", {
26
48
  access: "read",
27
- description: "Get a Shift Labs issue by ID.",
28
- inputSchema: t.Object({ id: t.String({ description: "Issue ID" }) }),
49
+ description: "Get a Shift Labs Issue by ID.",
50
+ inputSchema: t.Object({ id: idSchema("Issue ID") }, STRICT_OBJECT),
29
51
  async execute(input, ctx) {
30
52
  const { id } = input;
31
- const body = await request(ctx, `/v1/issues/${encodeURIComponent(id)}`);
53
+ const body = await request(ctx, `/v1/issues/${pathSegment(id)}`);
32
54
  return body.issue;
33
55
  },
34
56
  });
35
57
  rl.registerAction("issue.create", {
36
58
  access: "write",
37
- description: "Create a Shift Labs issue. The issue starts in triage.",
59
+ description: "Create an Issue. Leave projectId empty for inbox work; parentIssueId requires a Project.",
38
60
  inputSchema: t.Object({
39
- title: t.String({ description: "Issue title" }),
40
- description: t.Optional(t.String({ description: "Issue description" })),
41
- kind: t.Optional(enumSchema("Issue kind", ISSUE_KIND)),
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)),
42
72
  priority: t.Optional(enumSchema("Issue priority", ISSUE_PRIORITY)),
43
- severity: t.Optional(enumSchema("Issue severity", ISSUE_SEVERITY)),
44
73
  source: t.Optional(enumSchema("Issue source", ISSUE_SOURCE)),
45
- deploymentId: t.Optional(t.String()),
46
- workspaceId: t.Optional(t.String()),
47
- sessionId: t.Optional(t.String()),
48
- traceId: t.Optional(t.String()),
49
- fingerprint: t.Optional(t.String()),
50
- labels: t.Optional(t.Array(t.String())),
51
- metadata: t.Optional(t.Object({}, { description: "Issue metadata" })),
52
- }),
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),
53
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");
54
92
  const body = await request(ctx, "/v1/issues", {
55
93
  method: "POST",
56
- body: JSON.stringify(input),
94
+ body: JSON.stringify(fields),
57
95
  });
58
96
  return body.issue;
59
97
  },
60
98
  });
99
+ rl.registerAction("issue.update", {
100
+ access: "write",
101
+ description: "Update an Issue, including Project assignment, one-level parentage, ordering, status, dates, or assignee.",
102
+ inputSchema: t.Object({
103
+ id: idSchema("Issue ID"),
104
+ title: t.Optional(t.String({ minLength: 1, maxLength: 160, pattern: "\\S" })),
105
+ description: t.Optional(t.String({ maxLength: 20_000 })),
106
+ projectId: t.Optional(t.Union([idSchema("Project ID"), t.Null()])),
107
+ parentIssueId: t.Optional(t.Union([idSchema("Parent Issue ID"), t.Null()])),
108
+ sortOrder: t.Optional(t.Integer({ minimum: 0 })),
109
+ status: t.Optional(enumSchema("Issue status", ISSUE_STATUS)),
110
+ priority: t.Optional(enumSchema("Issue priority", ISSUE_PRIORITY)),
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" }))),
115
+ archived: t.Optional(t.Boolean()),
116
+ }, STRICT_UPDATE_OBJECT),
117
+ async execute(input, ctx) {
118
+ const { id, ...patch } = input;
119
+ assertDateOrder(patch.startAt, patch.dueAt, "Issue dueAt must not precede startAt");
120
+ const body = await request(ctx, `/v1/issues/${pathSegment(id)}`, { method: "PATCH", body: JSON.stringify(patch) });
121
+ return body.issue;
122
+ },
123
+ });
61
124
  rl.registerAction("issue.comment", {
62
125
  access: "write",
63
- description: "Add a comment to a Shift Labs issue.",
126
+ description: "Add a comment to a Shift Labs Issue.",
64
127
  inputSchema: t.Object({
65
- id: t.String({ description: "Issue ID" }),
66
- body: t.String({ description: "Comment body" }),
67
- }),
128
+ id: idSchema("Issue ID"),
129
+ body: t.String({
130
+ minLength: 1,
131
+ maxLength: 20_000,
132
+ pattern: "\\S",
133
+ description: "Comment body",
134
+ }),
135
+ }, STRICT_OBJECT),
68
136
  async execute(input, ctx) {
69
137
  const { id, body } = input;
70
- const response = await request(ctx, `/v1/issues/${encodeURIComponent(id)}/comments`, { method: "POST", body: JSON.stringify({ body }) });
138
+ const response = await request(ctx, `/v1/issues/${pathSegment(id)}/comments`, { method: "POST", body: JSON.stringify({ body }) });
71
139
  return response.event;
72
140
  },
73
141
  });
142
+ rl.registerAction("issue.dependency.list", {
143
+ access: "read",
144
+ description: "List the first page of blocking and blocked-by links.",
145
+ inputSchema: t.Object({
146
+ id: idSchema("Issue ID"),
147
+ limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
148
+ }, STRICT_OBJECT),
149
+ async execute(input, ctx) {
150
+ const { id, ...pagination } = input;
151
+ const body = await request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`, issueListParams(pagination)));
152
+ return body.dependencies;
153
+ },
154
+ });
155
+ rl.registerAction("issue.dependency.listPage", {
156
+ access: "read",
157
+ description: "List a cursor-paginated page of Issue dependencies.",
158
+ inputSchema: t.Object({
159
+ id: idSchema("Issue ID"),
160
+ cursor: t.Optional(cursorSchema()),
161
+ limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
162
+ }, STRICT_OBJECT),
163
+ async execute(input, ctx) {
164
+ const { id, ...pagination } = input;
165
+ return request(ctx, withQuery(`/v1/issues/${pathSegment(id)}/dependencies`, issueListParams(pagination)));
166
+ },
167
+ });
168
+ rl.registerAction("issue.dependency.add", {
169
+ access: "write",
170
+ description: "Declare that one Issue blocks another Issue.",
171
+ inputSchema: t.Object({
172
+ blockedIssueId: idSchema("Issue that is blocked"),
173
+ blockingIssueId: idSchema("Issue that blocks it"),
174
+ }, STRICT_OBJECT),
175
+ async execute(input, ctx) {
176
+ const { blockedIssueId, blockingIssueId } = input;
177
+ if (blockedIssueId === blockingIssueId) {
178
+ throw new Error("An Issue cannot block itself");
179
+ }
180
+ const body = await request(ctx, `/v1/issues/${pathSegment(blockedIssueId)}/dependencies`, { method: "POST", body: JSON.stringify({ blockingIssueId }) });
181
+ return body.dependency;
182
+ },
183
+ });
184
+ rl.registerAction("issue.dependency.remove", {
185
+ access: "write",
186
+ description: "Remove an Issue dependency link.",
187
+ inputSchema: t.Object({
188
+ issueId: idSchema("Issue ID"),
189
+ dependencyId: idSchema("Dependency ID"),
190
+ }, STRICT_OBJECT),
191
+ async execute(input, ctx) {
192
+ const { issueId, dependencyId } = input;
193
+ await request(ctx, `/v1/issues/${pathSegment(issueId)}/dependencies/${pathSegment(dependencyId)}`, { method: "DELETE" });
194
+ return { removed: true };
195
+ },
196
+ });
197
+ }
198
+ function assertDateOrder(start, end, message) {
199
+ if (typeof start === "string" && typeof end === "string" && start > end) {
200
+ throw new Error(message);
201
+ }
74
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
  };
@@ -0,0 +1,102 @@
1
+ import * as t from "typebox";
2
+ import { cursorSchema, enumSchema, idSchema, PROJECT_STATUS, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, } from "./shared.js";
3
+ const projectListFields = {
4
+ status: t.Optional(enumSchema("Project status", PROJECT_STATUS)),
5
+ leadUserId: t.Optional(idSchema("Lead user ID")),
6
+ includeArchived: t.Optional(t.Boolean()),
7
+ cursor: t.Optional(cursorSchema()),
8
+ limit: t.Optional(t.Integer({ minimum: 1, maximum: 100 })),
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
+ export function registerProjectActions(rl) {
19
+ rl.registerAction("project.list", {
20
+ access: "read",
21
+ description: "List the first page of concrete Shift Labs Projects.",
22
+ inputSchema: t.Object(projectListFields, STRICT_OBJECT),
23
+ async execute(input, ctx) {
24
+ const body = await request(ctx, `/v1/projects?${listParams(input)}`);
25
+ return body.projects;
26
+ },
27
+ });
28
+ rl.registerAction("project.listPage", {
29
+ access: "read",
30
+ description: "List a cursor-paginated page of concrete Projects.",
31
+ inputSchema: t.Object(projectListFields, STRICT_OBJECT),
32
+ async execute(input, ctx) {
33
+ return request(ctx, `/v1/projects?${listParams(input)}`);
34
+ },
35
+ });
36
+ rl.registerAction("project.get", {
37
+ access: "read",
38
+ description: "Get a Project and its derived Issue progress.",
39
+ inputSchema: t.Object({ id: idSchema("Project ID") }, STRICT_OBJECT),
40
+ async execute(input, ctx) {
41
+ const { id } = input;
42
+ const [project, progress] = await Promise.all([
43
+ request(ctx, `/v1/projects/${pathSegment(id)}`),
44
+ request(ctx, `/v1/projects/${pathSegment(id)}/progress`),
45
+ ]);
46
+ return { project: project.project, progress: progress.progress };
47
+ },
48
+ });
49
+ rl.registerAction("project.create", {
50
+ access: "write",
51
+ description: "Create a concrete Project container.",
52
+ inputSchema: t.Object({
53
+ name: t.String({
54
+ minLength: 1,
55
+ maxLength: 160,
56
+ pattern: "\\S",
57
+ description: "Project name",
58
+ }),
59
+ description: t.Optional(t.String({ maxLength: 20_000 })),
60
+ status: t.Optional(enumSchema("Project status", PROJECT_STATUS)),
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),
65
+ async execute(input, ctx) {
66
+ const fields = input;
67
+ assertDateOrder(fields.startAt, fields.targetAt);
68
+ const body = await request(ctx, "/v1/projects", {
69
+ method: "POST",
70
+ body: JSON.stringify(fields),
71
+ });
72
+ return body.project;
73
+ },
74
+ });
75
+ rl.registerAction("project.update", {
76
+ access: "write",
77
+ description: "Update Project lifecycle, ownership, dates, or content.",
78
+ inputSchema: t.Object({
79
+ id: idSchema("Project ID"),
80
+ name: t.Optional(t.String({ minLength: 1, maxLength: 160, pattern: "\\S" })),
81
+ description: t.Optional(t.String({ maxLength: 20_000 })),
82
+ status: t.Optional(enumSchema("Project status", PROJECT_STATUS)),
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()])),
86
+ archived: t.Optional(t.Boolean()),
87
+ }, STRICT_UPDATE_OBJECT),
88
+ async execute(input, ctx) {
89
+ const { id, ...patch } = input;
90
+ assertDateOrder(patch.startAt, patch.targetAt);
91
+ const body = await request(ctx, `/v1/projects/${pathSegment(id)}`, { method: "PATCH", body: JSON.stringify(patch) });
92
+ return body.project;
93
+ },
94
+ });
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}/`;
@@ -31,18 +49,17 @@ export function pageRenderUrl(organizationId, slug) {
31
49
  return new URL(`/pages/${pathSegment(organizationId)}/${pathSegment(slug)}`, baseUrl()).toString();
32
50
  }
33
51
  export const ISSUE_STATUS = [
34
- "triage",
35
- "open",
52
+ "backlog",
53
+ "todo",
36
54
  "in_progress",
37
- "resolved",
38
- "closed",
55
+ "done",
56
+ "canceled",
39
57
  ];
40
- export const ISSUE_KIND = [
41
- "bug",
42
- "support",
43
- "feature_request",
44
- "incident",
45
- "task",
58
+ export const PROJECT_STATUS = [
59
+ "planned",
60
+ "active",
61
+ "completed",
62
+ "canceled",
46
63
  ];
47
64
  export const ISSUE_PRIORITY = [
48
65
  "none",
@@ -51,7 +68,6 @@ export const ISSUE_PRIORITY = [
51
68
  "high",
52
69
  "urgent",
53
70
  ];
54
- export const ISSUE_SEVERITY = ["info", "warning", "error", "critical"];
55
71
  export const ISSUE_SOURCE = [
56
72
  "user",
57
73
  "agent",
@@ -59,6 +75,21 @@ export const ISSUE_SOURCE = [
59
75
  "api",
60
76
  "integration",
61
77
  ];
78
+ export const ISSUE_VIEW_VISIBILITY = ["personal", "organization"];
79
+ export const ISSUE_VIEW_LAYOUT = ["list", "board", "timeline"];
80
+ export const ISSUE_VIEW_GROUP = [
81
+ "status",
82
+ "assignee",
83
+ "priority",
84
+ "project",
85
+ ];
86
+ export const ISSUE_VIEW_SORT = [
87
+ "manual",
88
+ "priority",
89
+ "startAt",
90
+ "dueAt",
91
+ "updatedAt",
92
+ ];
62
93
  export const PAGE_STATUS = ["draft", "published", "archived"];
63
94
  export const PAGE_VISIBILITY = ["org", "invited"];
64
95
  export const TRANSCRIPTION_LANGUAGE = ["auto", "en", "he"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runline",
3
- "version": "0.13.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",