@alasano/pi-linear 0.0.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.
@@ -0,0 +1,365 @@
1
+ import { defineTool } from '@mariozechner/pi-coding-agent';
2
+ import { Type } from '@sinclair/typebox';
3
+ import { withLinearAuth, linearGraphQL } from '../client';
4
+ import { PaginationParams, FilterParam, SortParam, RawInputParam } from '../params';
5
+ import { PROJECT_SELECTION } from '../selections';
6
+ import type { JsonObject } from '../types';
7
+ import { compactObject, asObject, asObjectArray, asString } from '../util';
8
+
9
+ export function projectTools() {
10
+ return [
11
+ defineTool({
12
+ name: 'linear_list_projects',
13
+ label: 'Linear List Projects',
14
+ description: 'List projects. Supports full projects query args and raw filter/sort.',
15
+ parameters: Type.Object({
16
+ ...PaginationParams,
17
+ ...FilterParam,
18
+ ...SortParam,
19
+ }),
20
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
21
+ return withLinearAuth(ctx, signal, async (apiKey) => {
22
+ const variables = compactObject({
23
+ after: params.after,
24
+ before: params.before,
25
+ filter: asObject(params.filter),
26
+ first: params.first ?? 20,
27
+ includeArchived: params.includeArchived,
28
+ last: params.last,
29
+ orderBy: params.orderBy,
30
+ sort: asObjectArray(params.sort),
31
+ });
32
+
33
+ const data = await linearGraphQL<{
34
+ projects: { nodes: Array<JsonObject> };
35
+ }>(
36
+ apiKey,
37
+ `query ListProjects(
38
+ $after: String
39
+ $before: String
40
+ $filter: ProjectFilter
41
+ $first: Int
42
+ $includeArchived: Boolean
43
+ $last: Int
44
+ $orderBy: PaginationOrderBy
45
+ $sort: [ProjectSortInput!]
46
+ ) {
47
+ projects(
48
+ after: $after
49
+ before: $before
50
+ filter: $filter
51
+ first: $first
52
+ includeArchived: $includeArchived
53
+ last: $last
54
+ orderBy: $orderBy
55
+ sort: $sort
56
+ ) {
57
+ nodes {
58
+ ${PROJECT_SELECTION}
59
+ }
60
+ }
61
+ }`,
62
+ variables,
63
+ signal,
64
+ );
65
+
66
+ const projects = data.projects.nodes;
67
+ return {
68
+ content: [{ type: 'text', text: JSON.stringify({ projects }, null, 2) }],
69
+ details: { projects },
70
+ };
71
+ });
72
+ },
73
+ }),
74
+ defineTool({
75
+ name: 'linear_get_project',
76
+ label: 'Linear Get Project',
77
+ description: 'Get a specific project by id.',
78
+ parameters: Type.Object({
79
+ projectId: Type.String({ description: 'Project id.' }),
80
+ }),
81
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
82
+ return withLinearAuth(ctx, signal, async (apiKey) => {
83
+ const data = await linearGraphQL<{ project: JsonObject | null }>(
84
+ apiKey,
85
+ `query GetProject($id: String!) {
86
+ project(id: $id) {
87
+ ${PROJECT_SELECTION}
88
+ }
89
+ }`,
90
+ { id: params.projectId },
91
+ signal,
92
+ );
93
+
94
+ const project = data.project;
95
+ return {
96
+ content: [
97
+ { type: 'text', text: JSON.stringify({ project: project ?? null }, null, 2) },
98
+ ],
99
+ details: { project: project ?? null },
100
+ };
101
+ });
102
+ },
103
+ }),
104
+ defineTool({
105
+ name: 'linear_save_project',
106
+ label: 'Linear Save Project',
107
+ description:
108
+ 'Create or update a project. If projectId/id is provided, uses projectUpdate; otherwise uses projectCreate.',
109
+ parameters: Type.Object({
110
+ projectId: Type.Optional(Type.String({ description: 'Project id for update mode.' })),
111
+ id: Type.Optional(Type.String({ description: 'ProjectCreateInput.id' })),
112
+ name: Type.Optional(Type.String()),
113
+ description: Type.Optional(Type.String()),
114
+ content: Type.Optional(Type.String()),
115
+ color: Type.Optional(Type.String()),
116
+ icon: Type.Optional(Type.String()),
117
+ convertedFromIssueId: Type.Optional(Type.String()),
118
+ labelIds: Type.Optional(Type.Array(Type.String())),
119
+ lastAppliedTemplateId: Type.Optional(Type.String()),
120
+ leadId: Type.Optional(Type.String()),
121
+ memberIds: Type.Optional(Type.Array(Type.String())),
122
+ priority: Type.Optional(Type.Number()),
123
+ prioritySortOrder: Type.Optional(Type.Number()),
124
+ sortOrder: Type.Optional(Type.Number()),
125
+ startDate: Type.Optional(Type.String()),
126
+ startDateResolution: Type.Optional(Type.String()),
127
+ statusId: Type.Optional(Type.String()),
128
+ targetDate: Type.Optional(Type.String()),
129
+ targetDateResolution: Type.Optional(Type.String()),
130
+ teamIds: Type.Optional(Type.Array(Type.String())),
131
+ templateId: Type.Optional(Type.String()),
132
+ useDefaultTemplate: Type.Optional(Type.Boolean()),
133
+ canceledAt: Type.Optional(Type.String()),
134
+ completedAt: Type.Optional(Type.String()),
135
+ frequencyResolution: Type.Optional(Type.String()),
136
+ projectUpdateRemindersPausedUntilAt: Type.Optional(Type.String()),
137
+ slackIssueComments: Type.Optional(Type.Boolean()),
138
+ slackIssueStatuses: Type.Optional(Type.Boolean()),
139
+ slackNewIssue: Type.Optional(Type.Boolean()),
140
+ trashed: Type.Optional(Type.Boolean()),
141
+ updateReminderFrequency: Type.Optional(Type.Number()),
142
+ updateReminderFrequencyInWeeks: Type.Optional(Type.Number()),
143
+ updateRemindersDay: Type.Optional(Type.String()),
144
+ updateRemindersHour: Type.Optional(Type.Integer()),
145
+ slackChannelName: Type.Optional(Type.String()),
146
+ ...RawInputParam,
147
+ }),
148
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
149
+ return withLinearAuth(ctx, signal, async (apiKey) => {
150
+ const rawInput = asObject(params.input) || {};
151
+ const updateId = asString(params.projectId) || asString(rawInput.id);
152
+
153
+ const input = {
154
+ ...rawInput,
155
+ ...compactObject({
156
+ canceledAt: params.canceledAt,
157
+ color: params.color,
158
+ completedAt: params.completedAt,
159
+ content: params.content,
160
+ convertedFromIssueId: params.convertedFromIssueId,
161
+ description: params.description,
162
+ frequencyResolution: params.frequencyResolution,
163
+ icon: params.icon,
164
+ id: params.id,
165
+ labelIds: params.labelIds,
166
+ lastAppliedTemplateId: params.lastAppliedTemplateId,
167
+ leadId: params.leadId,
168
+ memberIds: params.memberIds,
169
+ name: params.name,
170
+ priority: params.priority,
171
+ prioritySortOrder: params.prioritySortOrder,
172
+ projectUpdateRemindersPausedUntilAt: params.projectUpdateRemindersPausedUntilAt,
173
+ slackIssueComments: params.slackIssueComments,
174
+ slackIssueStatuses: params.slackIssueStatuses,
175
+ slackNewIssue: params.slackNewIssue,
176
+ sortOrder: params.sortOrder,
177
+ startDate: params.startDate,
178
+ startDateResolution: params.startDateResolution,
179
+ statusId: params.statusId,
180
+ targetDate: params.targetDate,
181
+ targetDateResolution: params.targetDateResolution,
182
+ teamIds: params.teamIds,
183
+ templateId: params.templateId,
184
+ trashed: params.trashed,
185
+ updateReminderFrequency: params.updateReminderFrequency,
186
+ updateReminderFrequencyInWeeks: params.updateReminderFrequencyInWeeks,
187
+ updateRemindersDay: params.updateRemindersDay,
188
+ updateRemindersHour: params.updateRemindersHour,
189
+ useDefaultTemplate: params.useDefaultTemplate,
190
+ }),
191
+ };
192
+
193
+ if (updateId) {
194
+ if (Object.keys(input).length === 0) {
195
+ throw new Error('No project update fields were provided.');
196
+ }
197
+
198
+ const data = await linearGraphQL<{
199
+ projectUpdate: { success: boolean; project?: JsonObject | null };
200
+ }>(
201
+ apiKey,
202
+ `mutation UpdateProject($id: String!, $input: ProjectUpdateInput!) {
203
+ projectUpdate(id: $id, input: $input) {
204
+ success
205
+ project {
206
+ ${PROJECT_SELECTION}
207
+ }
208
+ }
209
+ }`,
210
+ { id: updateId, input },
211
+ signal,
212
+ );
213
+
214
+ if (!data.projectUpdate.success || !data.projectUpdate.project) {
215
+ throw new Error('Linear projectUpdate did not succeed.');
216
+ }
217
+
218
+ const project = data.projectUpdate.project;
219
+ return {
220
+ content: [{ type: 'text', text: JSON.stringify({ project }, null, 2) }],
221
+ details: { project },
222
+ };
223
+ }
224
+
225
+ if (!asString(input.name)) {
226
+ throw new Error('Project name is required for projectCreate (name).');
227
+ }
228
+
229
+ if (!Array.isArray(input.teamIds) || input.teamIds.length === 0) {
230
+ throw new Error('teamIds is required for projectCreate and must be a non-empty array.');
231
+ }
232
+
233
+ const data = await linearGraphQL<{
234
+ projectCreate: { success: boolean; project?: JsonObject | null };
235
+ }>(
236
+ apiKey,
237
+ `mutation CreateProject($input: ProjectCreateInput!, $slackChannelName: String) {
238
+ projectCreate(input: $input, slackChannelName: $slackChannelName) {
239
+ success
240
+ project {
241
+ ${PROJECT_SELECTION}
242
+ }
243
+ }
244
+ }`,
245
+ {
246
+ input,
247
+ slackChannelName: params.slackChannelName,
248
+ },
249
+ signal,
250
+ );
251
+
252
+ if (!data.projectCreate.success || !data.projectCreate.project) {
253
+ throw new Error('Linear projectCreate did not succeed.');
254
+ }
255
+
256
+ const project = data.projectCreate.project;
257
+ return {
258
+ content: [{ type: 'text', text: JSON.stringify({ project }, null, 2) }],
259
+ details: { project },
260
+ };
261
+ });
262
+ },
263
+ }),
264
+ defineTool({
265
+ name: 'linear_delete_project',
266
+ label: 'Linear Delete Project',
267
+ description: 'Delete a project by id.',
268
+ parameters: Type.Object({
269
+ projectId: Type.String(),
270
+ }),
271
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
272
+ return withLinearAuth(ctx, signal, async (apiKey) => {
273
+ const data = await linearGraphQL<{
274
+ projectDelete: { success: boolean };
275
+ }>(
276
+ apiKey,
277
+ `mutation DeleteProject($id: String!) {
278
+ projectDelete(id: $id) {
279
+ success
280
+ }
281
+ }`,
282
+ { id: params.projectId },
283
+ signal,
284
+ );
285
+
286
+ if (!data.projectDelete.success) {
287
+ throw new Error('Linear projectDelete did not succeed.');
288
+ }
289
+
290
+ return {
291
+ content: [{ type: 'text', text: JSON.stringify({ success: true }, null, 2) }],
292
+ details: { success: true },
293
+ };
294
+ });
295
+ },
296
+ }),
297
+ defineTool({
298
+ name: 'linear_archive_project',
299
+ label: 'Linear Archive Project',
300
+ description: 'Archive a project by id. Use trash=true to trash instead.',
301
+ parameters: Type.Object({
302
+ projectId: Type.String(),
303
+ trash: Type.Optional(Type.Boolean()),
304
+ }),
305
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
306
+ return withLinearAuth(ctx, signal, async (apiKey) => {
307
+ const data = await linearGraphQL<{
308
+ projectArchive: { success: boolean };
309
+ }>(
310
+ apiKey,
311
+ `mutation ArchiveProject($id: String!, $trash: Boolean) {
312
+ projectArchive(id: $id, trash: $trash) {
313
+ success
314
+ }
315
+ }`,
316
+ { id: params.projectId, trash: params.trash },
317
+ signal,
318
+ );
319
+
320
+ if (!data.projectArchive.success) {
321
+ throw new Error('Linear projectArchive did not succeed.');
322
+ }
323
+
324
+ return {
325
+ content: [{ type: 'text', text: JSON.stringify({ success: true }, null, 2) }],
326
+ details: { success: true },
327
+ };
328
+ });
329
+ },
330
+ }),
331
+ defineTool({
332
+ name: 'linear_unarchive_project',
333
+ label: 'Linear Unarchive Project',
334
+ description: 'Unarchive a project by id.',
335
+ parameters: Type.Object({
336
+ projectId: Type.String(),
337
+ }),
338
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
339
+ return withLinearAuth(ctx, signal, async (apiKey) => {
340
+ const data = await linearGraphQL<{
341
+ projectUnarchive: { success: boolean };
342
+ }>(
343
+ apiKey,
344
+ `mutation UnarchiveProject($id: String!) {
345
+ projectUnarchive(id: $id) {
346
+ success
347
+ }
348
+ }`,
349
+ { id: params.projectId },
350
+ signal,
351
+ );
352
+
353
+ if (!data.projectUnarchive.success) {
354
+ throw new Error('Linear projectUnarchive did not succeed.');
355
+ }
356
+
357
+ return {
358
+ content: [{ type: 'text', text: JSON.stringify({ success: true }, null, 2) }],
359
+ details: { success: true },
360
+ };
361
+ });
362
+ },
363
+ }),
364
+ ];
365
+ }
@@ -0,0 +1,107 @@
1
+ import { defineTool } from '@mariozechner/pi-coding-agent';
2
+ import { Type } from '@sinclair/typebox';
3
+ import { withLinearAuth, linearGraphQL } from '../client';
4
+ import { PaginationParams, FilterParam } from '../params';
5
+ import { TEAM_SELECTION } from '../selections';
6
+ import type { LinearTeam, JsonObject } from '../types';
7
+ import { compactObject, asObject } from '../util';
8
+
9
+ export function teamTools() {
10
+ return [
11
+ defineTool({
12
+ name: 'linear_list_teams',
13
+ label: 'Linear List Teams',
14
+ description:
15
+ 'List Linear teams and states. Supports full teams query args: after, before, filter, first, includeArchived, last, orderBy.',
16
+ parameters: Type.Object({
17
+ ...PaginationParams,
18
+ ...FilterParam,
19
+ }),
20
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
21
+ return withLinearAuth(ctx, signal, async (apiKey) => {
22
+ const variables = compactObject({
23
+ after: params.after,
24
+ before: params.before,
25
+ filter: asObject(params.filter),
26
+ first: params.first,
27
+ includeArchived: params.includeArchived,
28
+ last: params.last,
29
+ orderBy: params.orderBy,
30
+ });
31
+
32
+ const data = await linearGraphQL<{ teams: { nodes: LinearTeam[] } }>(
33
+ apiKey,
34
+ `query ListTeams(
35
+ $after: String
36
+ $before: String
37
+ $filter: TeamFilter
38
+ $first: Int
39
+ $includeArchived: Boolean
40
+ $last: Int
41
+ $orderBy: PaginationOrderBy
42
+ ) {
43
+ teams(
44
+ after: $after
45
+ before: $before
46
+ filter: $filter
47
+ first: $first
48
+ includeArchived: $includeArchived
49
+ last: $last
50
+ orderBy: $orderBy
51
+ ) {
52
+ nodes {
53
+ id
54
+ key
55
+ name
56
+ states(first: 50) {
57
+ nodes {
58
+ id
59
+ name
60
+ type
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }`,
66
+ variables,
67
+ signal,
68
+ );
69
+
70
+ const teams = data.teams.nodes;
71
+ return {
72
+ content: [{ type: 'text', text: JSON.stringify({ teams }, null, 2) }],
73
+ details: { teams },
74
+ };
75
+ });
76
+ },
77
+ }),
78
+ defineTool({
79
+ name: 'linear_get_team',
80
+ label: 'Linear Get Team',
81
+ description: 'Get a specific team by id.',
82
+ parameters: Type.Object({
83
+ teamId: Type.String(),
84
+ }),
85
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
86
+ return withLinearAuth(ctx, signal, async (apiKey) => {
87
+ const data = await linearGraphQL<{ team: JsonObject | null }>(
88
+ apiKey,
89
+ `query GetTeam($id: String!) {
90
+ team(id: $id) {
91
+ ${TEAM_SELECTION}
92
+ }
93
+ }`,
94
+ { id: params.teamId },
95
+ signal,
96
+ );
97
+
98
+ const team = data.team;
99
+ return {
100
+ content: [{ type: 'text', text: JSON.stringify({ team }, null, 2) }],
101
+ details: { team },
102
+ };
103
+ });
104
+ },
105
+ }),
106
+ ];
107
+ }
@@ -0,0 +1,107 @@
1
+ import { defineTool } from '@mariozechner/pi-coding-agent';
2
+ import { Type } from '@sinclair/typebox';
3
+ import { withLinearAuth, linearGraphQL } from '../client';
4
+ import { PaginationParams, FilterParam, SortParam } from '../params';
5
+ import { USER_SELECTION } from '../selections';
6
+ import type { JsonObject } from '../types';
7
+ import { compactObject, asObject, asObjectArray } from '../util';
8
+
9
+ export function userTools() {
10
+ return [
11
+ defineTool({
12
+ name: 'linear_list_users',
13
+ label: 'Linear List Users',
14
+ description: 'List users. Supports full users query args.',
15
+ parameters: Type.Object({
16
+ ...PaginationParams,
17
+ ...FilterParam,
18
+ ...SortParam,
19
+ includeDisabled: Type.Optional(Type.Boolean()),
20
+ }),
21
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
22
+ return withLinearAuth(ctx, signal, async (apiKey) => {
23
+ const variables = compactObject({
24
+ after: params.after,
25
+ before: params.before,
26
+ filter: asObject(params.filter),
27
+ first: params.first ?? 50,
28
+ includeArchived: params.includeArchived,
29
+ includeDisabled: params.includeDisabled,
30
+ last: params.last,
31
+ orderBy: params.orderBy,
32
+ sort: asObjectArray(params.sort),
33
+ });
34
+
35
+ const data = await linearGraphQL<{
36
+ users: { nodes: Array<JsonObject> };
37
+ }>(
38
+ apiKey,
39
+ `query ListUsers(
40
+ $after: String
41
+ $before: String
42
+ $filter: UserFilter
43
+ $first: Int
44
+ $includeArchived: Boolean
45
+ $includeDisabled: Boolean
46
+ $last: Int
47
+ $orderBy: PaginationOrderBy
48
+ $sort: [UserSortInput!]
49
+ ) {
50
+ users(
51
+ after: $after
52
+ before: $before
53
+ filter: $filter
54
+ first: $first
55
+ includeArchived: $includeArchived
56
+ includeDisabled: $includeDisabled
57
+ last: $last
58
+ orderBy: $orderBy
59
+ sort: $sort
60
+ ) {
61
+ nodes {
62
+ ${USER_SELECTION}
63
+ }
64
+ }
65
+ }`,
66
+ variables,
67
+ signal,
68
+ );
69
+
70
+ const users = data.users.nodes;
71
+ return {
72
+ content: [{ type: 'text', text: JSON.stringify({ users }, null, 2) }],
73
+ details: { users },
74
+ };
75
+ });
76
+ },
77
+ }),
78
+ defineTool({
79
+ name: 'linear_get_user',
80
+ label: 'Linear Get User',
81
+ description: 'Get a specific user by id.',
82
+ parameters: Type.Object({
83
+ userId: Type.String(),
84
+ }),
85
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
86
+ return withLinearAuth(ctx, signal, async (apiKey) => {
87
+ const data = await linearGraphQL<{ user: JsonObject | null }>(
88
+ apiKey,
89
+ `query GetUser($id: String!) {
90
+ user(id: $id) {
91
+ ${USER_SELECTION}
92
+ }
93
+ }`,
94
+ { id: params.userId },
95
+ signal,
96
+ );
97
+
98
+ const user = data.user;
99
+ return {
100
+ content: [{ type: 'text', text: JSON.stringify({ user }, null, 2) }],
101
+ details: { user },
102
+ };
103
+ });
104
+ },
105
+ }),
106
+ ];
107
+ }
@@ -0,0 +1,33 @@
1
+ import { defineTool } from '@mariozechner/pi-coding-agent';
2
+ import { Type } from '@sinclair/typebox';
3
+ import { switchWorkspace, type WorkspaceCredentials } from '../client';
4
+
5
+ export function workspaceTools(creds: WorkspaceCredentials) {
6
+ const names = Object.keys(creds.workspaces);
7
+ if (names.length < 2) return [];
8
+
9
+ return [
10
+ defineTool({
11
+ name: 'linear_switch_workspace',
12
+ label: 'Linear Switch Workspace',
13
+ description: `Switch active Linear workspace. Available: ${names.join(', ')}. Currently active: ${creds.activeWorkspace || 'none'}.`,
14
+ parameters: Type.Object({
15
+ name: Type.String({
16
+ description: `Workspace name to switch to. One of: ${names.join(', ')}`,
17
+ }),
18
+ }),
19
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
20
+ const updated = await switchWorkspace(params.name);
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: JSON.stringify({ active: updated.activeWorkspace }, null, 2),
26
+ },
27
+ ],
28
+ details: { active: updated.activeWorkspace },
29
+ };
30
+ },
31
+ }),
32
+ ];
33
+ }
@@ -0,0 +1,31 @@
1
+ export type JsonObject = Record<string, unknown>;
2
+
3
+ export type LinearGraphQLError = {
4
+ message: string;
5
+ };
6
+
7
+ export type LinearIssue = {
8
+ id: string;
9
+ identifier: string;
10
+ number?: number | null;
11
+ title: string;
12
+ description?: string | null;
13
+ priority?: number | null;
14
+ url?: string | null;
15
+ branchName?: string | null;
16
+ dueDate?: string | null;
17
+ createdAt?: string;
18
+ updatedAt?: string;
19
+ state?: { id: string; name: string; type?: string | null } | null;
20
+ team?: { id: string; key: string; name: string } | null;
21
+ assignee?: { id: string; name?: string | null; email?: string | null } | null;
22
+ };
23
+
24
+ export type LinearTeam = {
25
+ id: string;
26
+ key: string;
27
+ name: string;
28
+ states?: {
29
+ nodes: Array<{ id: string; name: string; type?: string | null }>;
30
+ };
31
+ };
@@ -0,0 +1,38 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import type { JsonObject } from './types';
3
+
4
+ export function asString(value: unknown): string | undefined {
5
+ if (typeof value !== 'string') return undefined;
6
+ const trimmed = value.trim();
7
+ return trimmed ? trimmed : undefined;
8
+ }
9
+
10
+ export function asObject(value: unknown): JsonObject | undefined {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
12
+ return value as JsonObject;
13
+ }
14
+
15
+ export function asObjectArray(value: unknown): JsonObject[] | undefined {
16
+ if (!Array.isArray(value)) return undefined;
17
+ const mapped = value.map(asObject).filter((item): item is JsonObject => Boolean(item));
18
+ return mapped.length ? mapped : [];
19
+ }
20
+
21
+ export function compactObject<T extends Record<string, unknown>>(input: T): Partial<T> {
22
+ const output: Partial<T> = {};
23
+ for (const [key, value] of Object.entries(input)) {
24
+ if (value !== undefined) {
25
+ output[key as keyof T] = value as T[keyof T];
26
+ }
27
+ }
28
+ return output;
29
+ }
30
+
31
+ export function mergeFilters(base?: JsonObject, extra?: JsonObject): JsonObject | undefined {
32
+ if (base && extra) {
33
+ return { and: [base, extra] };
34
+ }
35
+ return base || extra;
36
+ }
37
+
38
+ export const GenericObjectSchema = Type.Record(Type.String(), Type.Any());