runline 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ import * as t from "typebox";
2
+ import { CRM_BASE, CRM_PROPERTY_DATA_TYPE, enumSchema, idSchema, listParams, pathSegment, recordTypeSchema, request, STRICT_OBJECT, withQuery, } from "./shared.js";
3
+ export function registerRecordActions(rl) {
4
+ rl.registerAction("record.changeEvents", {
5
+ access: "read",
6
+ description: "List the immutable change-event history (created/updated snapshots) for a CRM record.",
7
+ inputSchema: t.Object({ recordId: idSchema("CRM record ID") }, STRICT_OBJECT),
8
+ async execute(input, ctx) {
9
+ const { recordId } = input;
10
+ const body = await request(ctx, `${CRM_BASE}/records/${pathSegment(recordId)}/change-events`);
11
+ return body.changeEvents;
12
+ },
13
+ });
14
+ rl.registerAction("propertyDefinition.list", {
15
+ access: "read",
16
+ description: "List governed custom-property definitions, optionally for one record type.",
17
+ inputSchema: t.Object({ recordType: t.Optional(recordTypeSchema()) }, STRICT_OBJECT),
18
+ async execute(input, ctx) {
19
+ const body = await request(ctx, withQuery(`${CRM_BASE}/property-definitions`, listParams(input)));
20
+ return body.propertyDefinitions;
21
+ },
22
+ });
23
+ rl.registerAction("propertyDefinition.create", {
24
+ access: "write",
25
+ description: "Define a governed custom property for a record type. Requires a CRM admin grant. Enum types require options.",
26
+ inputSchema: t.Object({
27
+ recordType: recordTypeSchema(),
28
+ key: t.String({
29
+ maxLength: 100,
30
+ pattern: "^[a-z][a-z0-9_]*$",
31
+ description: "snake_case property key",
32
+ }),
33
+ label: t.String({
34
+ minLength: 1,
35
+ maxLength: 200,
36
+ pattern: "\\S",
37
+ description: "Human-readable label",
38
+ }),
39
+ dataType: enumSchema("Property data type", CRM_PROPERTY_DATA_TYPE),
40
+ options: t.Optional(t.Array(t.String({ minLength: 1, maxLength: 500, pattern: "\\S" }), {
41
+ description: "Allowed values for enum and multi_enum types",
42
+ })),
43
+ validation: t.Optional(t.Object({
44
+ minLength: t.Optional(t.Integer({ minimum: 0 })),
45
+ maxLength: t.Optional(t.Integer({ minimum: 0 })),
46
+ minimum: t.Optional(t.Number()),
47
+ maximum: t.Optional(t.Number()),
48
+ pattern: t.Optional(t.String({ maxLength: 1_000 })),
49
+ }, { additionalProperties: false })),
50
+ filterable: t.Optional(t.Boolean()),
51
+ }, STRICT_OBJECT),
52
+ async execute(input, ctx) {
53
+ const fields = input;
54
+ const isEnum = fields.dataType === "enum" || fields.dataType === "multi_enum";
55
+ const options = fields.options ?? [];
56
+ if (isEnum && options.length === 0) {
57
+ throw new Error("Enum property definitions require options");
58
+ }
59
+ if (!isEnum && options.length > 0) {
60
+ throw new Error("Options are only allowed on enum property types");
61
+ }
62
+ if (new Set(options).size !== options.length) {
63
+ throw new Error("Duplicate property option");
64
+ }
65
+ if (fields.validation?.pattern) {
66
+ try {
67
+ new RegExp(fields.validation.pattern);
68
+ }
69
+ catch {
70
+ throw new Error("Property validation pattern is not a valid regex");
71
+ }
72
+ }
73
+ const body = await request(ctx, `${CRM_BASE}/property-definitions`, {
74
+ method: "POST",
75
+ body: JSON.stringify(input),
76
+ });
77
+ return body.propertyDefinition;
78
+ },
79
+ });
80
+ }
@@ -0,0 +1,122 @@
1
+ import * as t from "typebox";
2
+ import { cursorSchema, enumSchema, idSchema, timestampSchema, } from "../../_shared/shiftCloud.js";
3
+ export { enumSchema, idSchema, listParams, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, withQuery, } from "../../_shared/shiftCloud.js";
4
+ export const CRM_BASE = "/v1/crm";
5
+ export const CRM_ACCESS_ROLE = ["user", "admin"];
6
+ export const CRM_RECORD_TYPE = [
7
+ "account",
8
+ "person",
9
+ "opportunity",
10
+ "activity",
11
+ "task",
12
+ ];
13
+ export const CRM_IDENTITY_KIND = [
14
+ "email",
15
+ "phone",
16
+ "whatsapp",
17
+ "linkedin",
18
+ "other",
19
+ ];
20
+ export const CRM_PIPELINE_OUTCOME = ["open", "won", "lost"];
21
+ export const CRM_TASK_STATUS = [
22
+ "todo",
23
+ "in_progress",
24
+ "blocked",
25
+ "done",
26
+ "cancelled",
27
+ ];
28
+ export const CRM_TASK_PRIORITY = ["low", "medium", "high", "urgent"];
29
+ export const CRM_ACTIVITY_DIRECTION = [
30
+ "inbound",
31
+ "outbound",
32
+ "internal",
33
+ ];
34
+ export const CRM_ACTIVITY_STATUS = [
35
+ "planned",
36
+ "completed",
37
+ "cancelled",
38
+ ];
39
+ export const CRM_PROPERTY_DATA_TYPE = [
40
+ "string",
41
+ "number",
42
+ "boolean",
43
+ "date",
44
+ "datetime",
45
+ "enum",
46
+ "multi_enum",
47
+ "json",
48
+ ];
49
+ function propertiesSchema(description) {
50
+ return t.Record(t.String(), t.Unknown(), { description });
51
+ }
52
+ function externalRefSchema() {
53
+ return t.Object({
54
+ sourceSystem: t.String({
55
+ minLength: 1,
56
+ maxLength: 100,
57
+ pattern: "\\S",
58
+ description: "Upstream system name, e.g. hubspot",
59
+ }),
60
+ sourceId: t.String({
61
+ minLength: 1,
62
+ maxLength: 500,
63
+ pattern: "\\S",
64
+ description: "Stable ID in the upstream system",
65
+ }),
66
+ sourceUrl: t.Optional(t.String({ minLength: 1, maxLength: 2_000 })),
67
+ attributes: t.Optional(propertiesSchema("Source-specific attributes")),
68
+ }, { additionalProperties: false });
69
+ }
70
+ function relationshipSchema() {
71
+ return t.Object({
72
+ toRecordId: idSchema("Target CRM record ID"),
73
+ relationshipType: t.String({
74
+ minLength: 1,
75
+ maxLength: 100,
76
+ pattern: "\\S",
77
+ description: "Relationship type, e.g. participant, attendee, subject",
78
+ }),
79
+ role: t.Optional(t.String({ minLength: 1, maxLength: 200, pattern: "\\S" })),
80
+ attributes: t.Optional(propertiesSchema("Governed link attributes")),
81
+ }, { additionalProperties: false });
82
+ }
83
+ /** Shared create-time record fields (owner, custom properties, links). */
84
+ export function createRecordFields() {
85
+ return {
86
+ ownerId: t.Optional(idSchema("Owning Shift user ID")),
87
+ customProperties: t.Optional(propertiesSchema("Governed custom properties; every key must have a property definition")),
88
+ externalRefs: t.Optional(t.Array(externalRefSchema())),
89
+ relationships: t.Optional(t.Array(relationshipSchema())),
90
+ };
91
+ }
92
+ /** Shared update-time record fields (tri-state PATCH semantics). */
93
+ export function updateRecordFields() {
94
+ return {
95
+ ownerId: t.Optional(t.Union([idSchema("Owning Shift user ID"), t.Null()])),
96
+ customProperties: t.Optional(propertiesSchema("Replacement governed custom properties")),
97
+ externalRefs: t.Optional(t.Array(externalRefSchema())),
98
+ relationships: t.Optional(t.Array(relationshipSchema())),
99
+ archived: t.Optional(t.Boolean({ description: "Archive or restore" })),
100
+ };
101
+ }
102
+ export function paginationFields(maxLimit = 200) {
103
+ return {
104
+ cursor: t.Optional(cursorSchema()),
105
+ limit: t.Optional(t.Integer({
106
+ minimum: 1,
107
+ maximum: maxLimit,
108
+ description: "Max results, default 50",
109
+ })),
110
+ };
111
+ }
112
+ export function recordTypeSchema(description = "CRM record type") {
113
+ return enumSchema(description, CRM_RECORD_TYPE);
114
+ }
115
+ export function assertTimeOrder(start, end, message) {
116
+ if (typeof start === "string" && typeof end === "string" && start > end) {
117
+ throw new Error(message);
118
+ }
119
+ }
120
+ export function nullableTimestamp(description) {
121
+ return t.Optional(t.Union([timestampSchema(description), t.Null()]));
122
+ }
@@ -0,0 +1,79 @@
1
+ import * as t from "typebox";
2
+ import { CRM_BASE, CRM_TASK_PRIORITY, CRM_TASK_STATUS, createRecordFields, enumSchema, idSchema, listParams, nullableTimestamp, paginationFields, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, timestampSchema, updateRecordFields, withQuery, } from "./shared.js";
3
+ const taskListFields = {
4
+ linkedRecordId: t.Optional(idSchema("Filter by linked CRM record ID")),
5
+ status: t.Optional(enumSchema("Task status", CRM_TASK_STATUS)),
6
+ includeArchived: t.Optional(t.Boolean()),
7
+ ...paginationFields(),
8
+ };
9
+ export function registerTaskActions(rl) {
10
+ rl.registerAction("task.list", {
11
+ access: "read",
12
+ description: "List the first page of CRM tasks.",
13
+ inputSchema: t.Object(taskListFields, STRICT_OBJECT),
14
+ async execute(input, ctx) {
15
+ const body = await request(ctx, withQuery(`${CRM_BASE}/tasks`, listParams(input)));
16
+ return body.tasks;
17
+ },
18
+ });
19
+ rl.registerAction("task.listPage", {
20
+ access: "read",
21
+ description: "List a cursor-paginated page of CRM tasks.",
22
+ inputSchema: t.Object(taskListFields, STRICT_OBJECT),
23
+ async execute(input, ctx) {
24
+ return request(ctx, withQuery(`${CRM_BASE}/tasks`, listParams(input)));
25
+ },
26
+ });
27
+ rl.registerAction("task.get", {
28
+ access: "read",
29
+ description: "Get a CRM task by record ID.",
30
+ inputSchema: t.Object({ id: idSchema("Task record ID") }, STRICT_OBJECT),
31
+ async execute(input, ctx) {
32
+ const { id } = input;
33
+ const body = await request(ctx, `${CRM_BASE}/tasks/${pathSegment(id)}`);
34
+ return body.task;
35
+ },
36
+ });
37
+ rl.registerAction("task.create", {
38
+ access: "write",
39
+ description: "Create a CRM follow-up task, optionally linked to records via relationships.",
40
+ inputSchema: t.Object({
41
+ title: t.String({
42
+ minLength: 1,
43
+ maxLength: 500,
44
+ pattern: "\\S",
45
+ description: "Task title",
46
+ }),
47
+ status: t.Optional(enumSchema("Task status", CRM_TASK_STATUS)),
48
+ priority: t.Optional(enumSchema("Task priority", CRM_TASK_PRIORITY)),
49
+ dueAt: t.Optional(timestampSchema("ISO-8601 due timestamp")),
50
+ assignedTo: t.Optional(idSchema("Assigned Shift user ID")),
51
+ ...createRecordFields(),
52
+ }, STRICT_OBJECT),
53
+ async execute(input, ctx) {
54
+ const body = await request(ctx, `${CRM_BASE}/tasks`, {
55
+ method: "POST",
56
+ body: JSON.stringify(input),
57
+ });
58
+ return body.task;
59
+ },
60
+ });
61
+ rl.registerAction("task.update", {
62
+ access: "write",
63
+ description: "Update or archive a CRM task: title, status, priority, due date, or assignee.",
64
+ inputSchema: t.Object({
65
+ id: idSchema("Task record ID"),
66
+ title: t.Optional(t.String({ minLength: 1, maxLength: 500, pattern: "\\S" })),
67
+ status: t.Optional(enumSchema("Task status", CRM_TASK_STATUS)),
68
+ priority: t.Optional(enumSchema("Task priority", CRM_TASK_PRIORITY)),
69
+ dueAt: nullableTimestamp("ISO-8601 due timestamp"),
70
+ assignedTo: t.Optional(t.Union([idSchema("Assigned Shift user ID"), t.Null()])),
71
+ ...updateRecordFields(),
72
+ }, STRICT_UPDATE_OBJECT),
73
+ async execute(input, ctx) {
74
+ const { id, ...patch } = input;
75
+ const body = await request(ctx, `${CRM_BASE}/tasks/${pathSegment(id)}`, { method: "PATCH", body: JSON.stringify(patch) });
76
+ return body.task;
77
+ },
78
+ });
79
+ }
@@ -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()]));