xyne-plugin-spaces 0.3.0 → 0.4.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.
package/bun.lock CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "lockfileVersion": 1,
3
+ "configVersion": 0,
3
4
  "workspaces": {
4
5
  "": {
5
6
  "name": "xyne-spaces",
6
7
  "dependencies": {
7
- "xyne-plugin": "^1.2.0",
8
+ "xyne-plugin": "^1.2.10",
8
9
  },
9
10
  "devDependencies": {
10
11
  "typescript": "^5.9.3",
@@ -14,7 +15,7 @@
14
15
  "packages": {
15
16
  "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
16
17
 
17
- "xyne-plugin": ["xyne-plugin@1.2.0", "", { "dependencies": { "zod": "^3.25.0" } }, "sha512-ecJJFMka/KvOlhF28CcRVfgmfHv6kE8JIn3cUlUCQ39b2j+9asM+/z5TOGkGHOXqSbtd5hl36zgtgvq8EA6h7A=="],
18
+ "xyne-plugin": ["xyne-plugin@1.2.14", "", { "dependencies": { "zod": "^3.25.0" } }, "sha512-A5oPZDspJUfDGpXkFrAOuiDVqJziRpJDteddJHysG6yYCot8gVe5eaZnPKrxnARliHl7smBfHEI5WTGUOlPTXg=="],
18
19
 
19
20
  "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
20
21
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "xyne-plugin-spaces",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
7
7
  "typecheck": "tsc --noEmit"
8
8
  },
9
9
  "dependencies": {
10
- "xyne-plugin": "^1.2.10"
10
+ "xyne-plugin": "^1.2.17"
11
11
  },
12
12
  "devDependencies": {
13
13
  "typescript": "^5.9.3"
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Plugin, tool, z } from "xyne-plugin"
2
2
  import { spacesAuth, spacesFetch } from "./auth"
3
- import { queryTickets, queryMessages, queryMessageDetail, queryChannels, queryUsers, queryUserActivity } from "./query"
3
+ import { queryTickets, queryMessages, queryMessageDetail, queryChannels, queryUsers, queryUserActivity, queryProjects, queryBoards } from "./query"
4
4
 
5
5
  function getToken(ctx: { credentials?: { type: string; data?: Record<string, unknown> } }): string {
6
6
  const creds = ctx.credentials
@@ -212,17 +212,19 @@ const spacesMessageDetail = tool({
212
212
 
213
213
  const spacesChannels = tool({
214
214
  description:
215
- "List channels in Spaces. Can filter by visibility (PUBLIC/PRIVATE) and scope type (DEFAULT/DM/TICKET/GROUP_DM). " +
216
- "Returns channel name, participant count, project, and last activity.",
215
+ "List channels in Spaces. Can filter by visibility (PUBLIC/PRIVATE), scope type (DEFAULT/DM/TICKET/GROUP_DM), " +
216
+ "and participant name. Returns channel name, members, conversation ID, and last activity. " +
217
+ "To find a DM between two people, use scopeType='DM' and participantName to filter by one of them.",
217
218
  auth: spacesAuth,
218
219
  args: {
219
220
  visibility: z.enum(["PUBLIC", "PRIVATE"]).optional().describe("Filter by visibility"),
220
221
  scopeType: z.enum(["DEFAULT", "DM", "TICKET", "DOCUMENT", "GROUP_DM"]).optional().describe("Filter by scope type"),
222
+ participantName: z.string().optional().describe("Filter channels by participant name (partial match)"),
221
223
  limit: z.number().min(1).max(50).default(20).describe("Max channels (default 20)"),
222
224
  },
223
225
  async execute(args, ctx) {
224
226
  const token = getToken(ctx)
225
- return queryChannels(token, args.limit, args.visibility, args.scopeType)
227
+ return queryChannels(token, args.limit, args.visibility, args.scopeType, args.participantName)
226
228
  },
227
229
  })
228
230
 
@@ -317,6 +319,155 @@ const spacesWebFetch = tool({
317
319
  },
318
320
  })
319
321
 
322
+ // ── Projects ────────────────────────────────────────────────────────
323
+
324
+ const spacesProjects = tool({
325
+ description:
326
+ "Search and list projects to find project IDs for creating tickets. " +
327
+ "Can filter by name with pagination support.",
328
+ auth: spacesAuth,
329
+ args: {
330
+ search: z.string().optional().describe("Filter by project name (partial match)"),
331
+ limit: z.number().min(1).max(50).default(20).describe("Max results (default 20)"),
332
+ offset: z.number().min(0).default(0).describe("Pagination offset"),
333
+ },
334
+ async execute(args, ctx) {
335
+ const token = getToken(ctx)
336
+ return queryProjects(token, args.search, args.limit, args.offset)
337
+ },
338
+ })
339
+
340
+ // ── Boards ──────────────────────────────────────────────────────────
341
+
342
+ const spacesBoards = tool({
343
+ description:
344
+ "Search and list boards to find board IDs for creating tickets. " +
345
+ "Can filter by name or project ID with pagination support.",
346
+ auth: spacesAuth,
347
+ args: {
348
+ search: z.string().optional().describe("Filter by board name (partial match)"),
349
+ projectId: z.string().optional().describe("Filter by project ID (use spaces-projects to find project IDs)"),
350
+ limit: z.number().min(1).max(50).default(20).describe("Max results (default 20)"),
351
+ offset: z.number().min(0).default(0).describe("Pagination offset"),
352
+ },
353
+ async execute(args, ctx) {
354
+ const token = getToken(ctx)
355
+ return queryBoards(token, args.search, args.projectId, args.limit, args.offset)
356
+ },
357
+ })
358
+
359
+ // ── Send Message ────────────────────────────────────────────────────
360
+
361
+ const spacesSendMessage = tool({
362
+ description:
363
+ "Send a message in Spaces by replying to an existing conversation thread. " +
364
+ "Use the conversationId from spaces-tickets or spaces-messages results (NOT channelId or ticketId).",
365
+ auth: spacesAuth,
366
+ args: {
367
+ conversationId: z.string().describe("The conversationId to send the message to (from spaces-tickets or spaces-messages results)"),
368
+ content: z.string().describe("Message content to send"),
369
+ },
370
+ async execute(args, ctx) {
371
+ const token = getToken(ctx)
372
+ const data = (await spacesFetch(`/chat/postMessage/${args.conversationId}`, token, {
373
+ method: "POST",
374
+ body: JSON.stringify({ content: args.content }),
375
+ })) as { messageId: string; conversationId: string; content: string; sender?: { name: string } }
376
+
377
+ return `Message sent. messageId: ${data.messageId}, conversationId: ${data.conversationId}`
378
+ },
379
+ })
380
+
381
+ // ── Create Ticket ───────────────────────────────────────────────────
382
+
383
+ const spacesCreateTicket = tool({
384
+ description:
385
+ "Create a new ticket in Spaces. Requires projectId, boardId, and channelId — " +
386
+ "use spaces-projects, spaces-boards, and spaces-channels to look these up first.",
387
+ auth: spacesAuth,
388
+ args: {
389
+ title: z.string().describe("Ticket title"),
390
+ description: z.string().describe("Ticket description"),
391
+ projectId: z.string().describe("Project ID (use spaces-projects to find)"),
392
+ boardId: z.string().describe("Board ID (use spaces-boards to find)"),
393
+ channelId: z.string().describe("Channel ID (use spaces-channels to find)"),
394
+ priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).optional().describe("Ticket priority"),
395
+ assignedTo: z.string().optional().describe("User ID to assign (use spaces-users to find)"),
396
+ eta: z.string().optional().describe("Due date as ISO 8601 string"),
397
+ tags: z.array(z.string()).optional().describe("Tags to apply"),
398
+ },
399
+ async execute(args, ctx) {
400
+ const token = getToken(ctx)
401
+ const body: Record<string, unknown> = {
402
+ title: args.title,
403
+ description: args.description,
404
+ projectId: args.projectId,
405
+ boardId: args.boardId,
406
+ channelId: args.channelId,
407
+ }
408
+ if (args.priority) body["priority"] = args.priority
409
+ if (args.assignedTo) body["assignedTo"] = args.assignedTo
410
+ if (args.eta) body["eta"] = args.eta
411
+ if (args.tags) body["tags"] = args.tags
412
+
413
+ const data = (await spacesFetch("/ticket/create", token, {
414
+ method: "POST",
415
+ body: JSON.stringify(body),
416
+ })) as { id: string; xyneId: string; conversationId: string; title: string; priority: string; status: string }
417
+
418
+ return [
419
+ `Ticket created:`,
420
+ ` xyneId: ${data.xyneId}`,
421
+ ` ID: ${data.id}`,
422
+ ` Status: ${data.status}`,
423
+ ` Priority: ${data.priority}`,
424
+ ` ConversationID: ${data.conversationId}`,
425
+ ].join("\n")
426
+ },
427
+ })
428
+
429
+ // ── Schedule Call ───────────────────────────────────────────────────
430
+
431
+ const spacesScheduleCall = tool({
432
+ description:
433
+ "Schedule a call in Spaces. Must provide either a channelId or targetUserIds (list of user IDs to invite).",
434
+ auth: spacesAuth,
435
+ args: {
436
+ title: z.string().describe("Call title"),
437
+ startsAt: z.string().describe("Start time as ISO 8601 string (e.g. '2026-03-28T10:00:00Z')"),
438
+ endsAt: z.string().describe("End time as ISO 8601 string"),
439
+ channelId: z.string().optional().describe("Channel ID to schedule the call in"),
440
+ targetUserIds: z.array(z.string()).optional().describe("User IDs to invite (use spaces-users to find)"),
441
+ },
442
+ async execute(args, ctx) {
443
+ if (!args.channelId && (!args.targetUserIds || args.targetUserIds.length === 0)) {
444
+ throw new Error("Must provide either channelId or targetUserIds")
445
+ }
446
+
447
+ const token = getToken(ctx)
448
+ const body: Record<string, unknown> = {
449
+ title: args.title,
450
+ startsAt: new Date(args.startsAt).getTime(),
451
+ endsAt: new Date(args.endsAt).getTime(),
452
+ }
453
+ if (args.channelId) body["channelId"] = args.channelId
454
+ if (args.targetUserIds) body["targetUserIds"] = args.targetUserIds
455
+
456
+ const data = (await spacesFetch("/calls/schedule", token, {
457
+ method: "POST",
458
+ body: JSON.stringify(body),
459
+ })) as { success: boolean; callId?: string; externalId?: string; channelId?: string }
460
+
461
+ if (!data.success) return "Failed to schedule call."
462
+ return [
463
+ `Call scheduled:`,
464
+ ` callId: ${data.callId}`,
465
+ ` externalId: ${data.externalId}`,
466
+ ` channelId: ${data.channelId}`,
467
+ ].join("\n")
468
+ },
469
+ })
470
+
320
471
  // ── Plugin ───────────────────────────────────────────────────────────
321
472
 
322
473
  const spacesTools = {
@@ -328,6 +479,11 @@ const spacesTools = {
328
479
  "spaces-channels": spacesChannels,
329
480
  "spaces-users": spacesUsers,
330
481
  "spaces-activity": spacesUserActivity,
482
+ "spaces-projects": spacesProjects,
483
+ "spaces-boards": spacesBoards,
484
+ "spaces-send-message": spacesSendMessage,
485
+ "spaces-create-ticket": spacesCreateTicket,
486
+ "spaces-schedule-call": spacesScheduleCall,
331
487
  "webfetch": spacesWebFetch,
332
488
  }
333
489
 
@@ -368,6 +524,11 @@ const plugin: Plugin = async (_input) => ({
368
524
  "- **Channels**: Browse channels by visibility and scope",
369
525
  "- **Users**: Look up users by name or email",
370
526
  "- **Activity**: View user activity feed (mentions, replies, assignments)",
527
+ "- **Projects**: Search/list projects (needed for ticket creation)",
528
+ "- **Boards**: Search/list boards by name or project (needed for ticket creation)",
529
+ "- **Send Message**: Reply to an existing conversation thread",
530
+ "- **Create Ticket**: Create a new ticket with title, description, project, board, channel",
531
+ "- **Schedule Call**: Schedule a call in a channel or with specific users",
371
532
  "",
372
533
  "## Guidelines",
373
534
  "- Use spaces-search for broad queries across all connected apps",
package/src/query.ts CHANGED
@@ -221,14 +221,19 @@ interface ChannelRow {
221
221
  scopeType: string
222
222
  visibility: string
223
223
  participantCount: number
224
+ conversationId?: string
224
225
  lastActivityAt?: string
225
226
  project?: { name: string } | null
227
+ participants?: Array<{ user?: { name: string } | null }> | null
226
228
  }
227
229
 
228
- export async function queryChannels(token: string, limit: number, visibility?: string, scopeType?: string): Promise<string> {
230
+ export async function queryChannels(token: string, limit: number, visibility?: string, scopeType?: string, participantName?: string): Promise<string> {
229
231
  const where: Record<string, unknown> = {}
230
232
  if (visibility) where["visibility"] = { equals: visibility }
231
233
  if (scopeType) where["scopeType"] = { equals: scopeType }
234
+ if (participantName) {
235
+ where["participants"] = { some: { user: { name: { contains: participantName } } } }
236
+ }
232
237
 
233
238
  const rows = (await query(token, {
234
239
  model: "channel",
@@ -238,6 +243,7 @@ export async function queryChannels(token: string, limit: number, visibility?: s
238
243
  take: limit,
239
244
  include: {
240
245
  project: { select: { name: true } },
246
+ participants: { select: { user: { select: { name: true } } } },
241
247
  },
242
248
  })) as ChannelRow[]
243
249
 
@@ -246,8 +252,12 @@ export async function queryChannels(token: string, limit: number, visibility?: s
246
252
  const lines = rows.map((c) => {
247
253
  const parts = [`#${c.name} (${c.scopeType}, ${c.visibility})`]
248
254
  if (c.description) parts.push(` ${c.description}`)
249
- parts.push(` Participants: ${c.participantCount}${c.project ? ` · Project: ${c.project.name}` : ""}`)
255
+ const memberNames = c.participants?.map((p) => p.user?.name).filter(Boolean) ?? []
256
+ if (memberNames.length > 0) parts.push(` Members: ${memberNames.join(", ")}`)
257
+ else parts.push(` Participants: ${c.participantCount}`)
258
+ if (c.project) parts.push(` Project: ${c.project.name}`)
250
259
  if (c.lastActivityAt) parts.push(` Last active: ${new Date(c.lastActivityAt).toLocaleString()}`)
260
+ if (c.conversationId) parts.push(` ConversationID: ${c.conversationId}`)
251
261
  parts.push(` ID: ${c.id}`)
252
262
  return parts.join("\n")
253
263
  })
@@ -336,3 +346,82 @@ export async function queryUserActivity(
336
346
 
337
347
  return `${rows.length} activity entries:\n\n${lines.join("\n")}`
338
348
  }
349
+
350
+ // ── Projects ────────────────────────────────────────────────────────
351
+
352
+ interface ProjectRow {
353
+ id: string
354
+ name: string
355
+ description?: string
356
+ createdAt?: string
357
+ updatedAt?: string
358
+ }
359
+
360
+ export async function queryProjects(token: string, search?: string, limit?: number, offset?: number): Promise<string> {
361
+ const where: Record<string, unknown> = {}
362
+ if (search) where["name"] = { contains: search }
363
+
364
+ const rows = (await query(token, {
365
+ model: "project",
366
+ operation: "findMany",
367
+ where,
368
+ orderBy: [{ updatedAt: "desc" }],
369
+ take: limit ?? 20,
370
+ skip: offset ?? 0,
371
+ })) as ProjectRow[]
372
+
373
+ if (rows.length === 0) return search ? `No projects found matching "${search}".` : "No projects found."
374
+
375
+ const lines = rows.map((p) => {
376
+ const parts = [p.name]
377
+ if (p.description) parts.push(` ${p.description}`)
378
+ parts.push(` ID: ${p.id}`)
379
+ if (p.updatedAt) parts.push(` Updated: ${new Date(p.updatedAt).toLocaleString()}`)
380
+ return parts.join("\n")
381
+ })
382
+
383
+ return `${rows.length} project(s):\n\n${lines.join("\n\n")}`
384
+ }
385
+
386
+ // ── Boards ──────────────────────────────────────────────────────────
387
+
388
+ interface BoardRow {
389
+ id: string
390
+ name: string
391
+ description?: string
392
+ projectId?: string
393
+ project?: { name: string } | null
394
+ createdAt?: string
395
+ updatedAt?: string
396
+ }
397
+
398
+ export async function queryBoards(token: string, search?: string, projectId?: string, limit?: number, offset?: number): Promise<string> {
399
+ const where: Record<string, unknown> = {}
400
+ if (search) where["name"] = { contains: search }
401
+ if (projectId) where["projectId"] = { equals: projectId }
402
+
403
+ const rows = (await query(token, {
404
+ model: "board",
405
+ operation: "findMany",
406
+ where,
407
+ orderBy: [{ updatedAt: "desc" }],
408
+ take: limit ?? 20,
409
+ skip: offset ?? 0,
410
+ include: {
411
+ project: { select: { name: true } },
412
+ },
413
+ })) as BoardRow[]
414
+
415
+ if (rows.length === 0) return search ? `No boards found matching "${search}".` : "No boards found."
416
+
417
+ const lines = rows.map((b) => {
418
+ const parts = [b.name]
419
+ if (b.description) parts.push(` ${b.description}`)
420
+ if (b.project) parts.push(` Project: ${b.project.name}`)
421
+ parts.push(` ID: ${b.id}`)
422
+ if (b.updatedAt) parts.push(` Updated: ${new Date(b.updatedAt).toLocaleString()}`)
423
+ return parts.join("\n")
424
+ })
425
+
426
+ return `${rows.length} board(s):\n\n${lines.join("\n\n")}`
427
+ }