@maudecode/kanvil 0.6.1 → 0.7.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.
Files changed (124) hide show
  1. package/apps/server/dist/{auth-CFjetlB4.mjs → auth-D43XyHdK.mjs} +227 -71
  2. package/apps/server/dist/client.mjs +2 -2
  3. package/apps/server/dist/{config-BQw6mPzE.mjs → config-CQVdVhXv.mjs} +1 -1
  4. package/apps/server/dist/index.mjs +161 -44
  5. package/apps/server/dist/migrate.mjs +1 -1
  6. package/apps/server/dist/recover.mjs +3 -3
  7. package/apps/server/dist/{src-BaTNDJB7.mjs → src-D70GmZ5R.mjs} +83 -26
  8. package/apps/server/drizzle/0040_upload_urls.sql +20 -0
  9. package/apps/server/drizzle/0041_project_repositories.sql +2 -0
  10. package/apps/server/drizzle/meta/0040_snapshot.json +7170 -0
  11. package/apps/server/drizzle/meta/0041_snapshot.json +7183 -0
  12. package/apps/server/drizzle/meta/_journal.json +14 -0
  13. package/apps/web/dist/assets/{CompositeItem-Dotj8JZL.js → CompositeItem-DLTRsada.js} +1 -1
  14. package/apps/web/dist/assets/{Trans-CE4EAAY1.js → Trans-DqX-qhvQ.js} +1 -1
  15. package/apps/web/dist/assets/admin-settings-BQzdZII5.js +1 -0
  16. package/apps/web/dist/assets/{archive-restore-BMtgjFUu.js → archive-restore-BO1qng5Y.js} +1 -1
  17. package/apps/web/dist/assets/az-AZ-SXqDBPA2.js +1 -0
  18. package/apps/web/dist/assets/{az-B-DxMc__.js → az-CEW7-a7J.js} +1 -1
  19. package/apps/web/dist/assets/configuration-D0zvwrXt.js +1 -0
  20. package/apps/web/dist/assets/{corner-left-up-O4OsTW-L.js → corner-left-up-Be7Z9Uhf.js} +1 -1
  21. package/apps/web/dist/assets/{de-Btbkq-aX.js → de-B-oGVMYL.js} +1 -1
  22. package/apps/web/dist/assets/de-DE-CB7aQ6b-.js +1 -0
  23. package/apps/web/dist/assets/default-CCI1vZxa.js +56 -0
  24. package/apps/web/dist/assets/delivery-settings-BrwSrFIN.js +1 -0
  25. package/apps/web/dist/assets/{el-DBVEX16k.js → el-Cp69kezM.js} +1 -1
  26. package/apps/web/dist/assets/el-GR-D1ntYXIt.js +1 -0
  27. package/apps/web/dist/assets/{ellipsis-CvEPDi9V.js → ellipsis-B0AE8ax3.js} +1 -1
  28. package/apps/web/dist/assets/{es-CMJoxZBk.js → es-CwOMVSTx.js} +1 -1
  29. package/apps/web/dist/assets/es-ES-CllsaxcB.js +1 -0
  30. package/apps/web/dist/assets/{forms-DbfnnPg_.js → forms-CkVpTx29.js} +1 -1
  31. package/apps/web/dist/assets/{fr-OyEmYoRn.js → fr-Cy4co9ic.js} +1 -1
  32. package/apps/web/dist/assets/fr-FR-DxTw6LgI.js +1 -0
  33. package/apps/web/dist/assets/{hi-CIgXSseB.js → hi-DbO9TKwg.js} +1 -1
  34. package/apps/web/dist/assets/hi-IN-CaQga-0l.js +1 -0
  35. package/apps/web/dist/assets/{highlight-B4K3yEEh.js → highlight-zMg1BK-r.js} +1 -1
  36. package/apps/web/dist/assets/{id-WV2oSAPB.js → id-B6NKsmCm.js} +1 -1
  37. package/apps/web/dist/assets/id-ID-DBfwAAqU.js +1 -0
  38. package/apps/web/dist/assets/index-CZuFd4LM.js +17 -0
  39. package/apps/web/dist/assets/index-DPT5LAs1.css +2 -0
  40. package/apps/web/dist/assets/{invitations-BTB8f2Xl.js → invitations-Dp40BfHj.js} +1 -1
  41. package/apps/web/dist/assets/it-IT-BckuH9Nl.js +1 -0
  42. package/apps/web/dist/assets/{it-BWSrRqus.js → it-ilGYCq2T.js} +1 -1
  43. package/apps/web/dist/assets/{item-HmFuRZet.js → item-DQR89bKm.js} +1 -1
  44. package/apps/web/dist/assets/{ja-BtOHWvUJ.js → ja-D-hJRTgP.js} +1 -1
  45. package/apps/web/dist/assets/ja-JP-BAGgJKZ4.js +1 -0
  46. package/apps/web/dist/assets/ko-KR-srIEsTP5.js +1 -0
  47. package/apps/web/dist/assets/{ko-DAryK0HN.js → ko-ajkkoJ_d.js} +1 -1
  48. package/apps/web/dist/assets/{label-settings-ssfEX2uR.js → label-settings-ChnKkA97.js} +1 -1
  49. package/apps/web/dist/assets/{markdown-blocks-B1kO6iku.js → markdown-blocks-CpzrwyQG.js} +1 -1
  50. package/apps/web/dist/assets/{member-directory-BA4ZT1qU.js → member-directory-XK0ZEGUq.js} +1 -1
  51. package/apps/web/dist/assets/{mk-DBbiGUk5.js → mk-C_sMyEHX.js} +1 -1
  52. package/apps/web/dist/assets/mk-MK-BTa1NMAF.js +1 -0
  53. package/apps/web/dist/assets/{nl-D4V-8WdF.js → nl-D9-P4UPR.js} +1 -1
  54. package/apps/web/dist/assets/nl-NL-CQduhDqs.js +1 -0
  55. package/apps/web/dist/assets/{pl-C--QrxO0.js → pl-CiQM7fSB.js} +1 -1
  56. package/apps/web/dist/assets/pl-PL-BdZMf7Ni.js +1 -0
  57. package/apps/web/dist/assets/project-lifecycle-VYecJuiP.js +1 -0
  58. package/apps/web/dist/assets/project-settings-Bs6YEpo9.js +1 -0
  59. package/apps/web/dist/assets/{pt-BR-DvFVKpmO.js → pt-BR-CP7dhuqF.js} +1 -1
  60. package/apps/web/dist/assets/pt-BR-DZEXJNHg.js +1 -0
  61. package/apps/web/dist/assets/public-project-DUfWDao9.js +1 -0
  62. package/apps/web/dist/assets/{rotate-ccw-CB36Zu3L.js → rotate-ccw-Cxh9ec2s.js} +1 -1
  63. package/apps/web/dist/assets/{ru-UmbPUOka.js → ru-DxJ6vF4E.js} +1 -1
  64. package/apps/web/dist/assets/ru-RU-CZKEe7P8.js +1 -0
  65. package/apps/web/dist/assets/{search-Cvetr1OK.js → search-DfbsqO7M.js} +6 -6
  66. package/apps/web/dist/assets/{send-CJDct4Oj.js → send-DrJl0BKm.js} +1 -1
  67. package/apps/web/dist/assets/settings-BN8uIiBG.js +1 -0
  68. package/apps/web/dist/assets/{sliders-horizontal-dmqWngxy.js → sliders-horizontal-DM47MLe7.js} +1 -1
  69. package/apps/web/dist/assets/{switch-BtScTpP9.js → switch-CJTVGtE9.js} +1 -1
  70. package/apps/web/dist/assets/{table-byFokzo-.js → table-BLLjFPWD.js} +1 -1
  71. package/apps/web/dist/assets/{ticket-detail-C3YA3Hzc.js → ticket-detail-BTy_3JeU.js} +2 -2
  72. package/apps/web/dist/assets/ticket-editor-Bb_lyxnJ.js +2 -0
  73. package/apps/web/dist/assets/{ticket-list-BXUorfmj.js → ticket-list-Botp5qf5.js} +5 -5
  74. package/apps/web/dist/assets/ticket-list-controls-Dmkno4pm.js +1 -0
  75. package/apps/web/dist/assets/{tr-CNpgQ4ka.js → tr-9IkdRLIk.js} +1 -1
  76. package/apps/web/dist/assets/tr-TR-Y8wBLO8I.js +1 -0
  77. package/apps/web/dist/assets/{uk-MQoTXdpk.js → uk-Btampsdw.js} +1 -1
  78. package/apps/web/dist/assets/uk-UA-CVpasGUS.js +1 -0
  79. package/apps/web/dist/assets/{upload-D1x_ib3B.js → upload-HARy4VC_.js} +1 -1
  80. package/apps/web/dist/assets/{useQueries-BnlNCoy4.js → useQueries-CTeEF--U.js} +1 -1
  81. package/apps/web/dist/assets/{vi-CEw4gTWc.js → vi-BVZzOn_n.js} +1 -1
  82. package/apps/web/dist/assets/vi-VN-BMy-iJD-.js +1 -0
  83. package/apps/web/dist/assets/{visual-editor-CpdMRn3e.js → visual-editor-DKBPjqV-.js} +1 -1
  84. package/apps/web/dist/assets/{workspace-settings-BickBtZx.js → workspace-settings-DqzBNw60.js} +1 -1
  85. package/apps/web/dist/assets/{zh-CN-ChzoXkfr.js → zh-CN-BoUvF2do.js} +1 -1
  86. package/apps/web/dist/assets/zh-CN-Cds_v0R8.js +1 -0
  87. package/apps/web/dist/assets/{zh-TW-bWBKARrK.js → zh-TW-CEIDGTWe.js} +1 -1
  88. package/apps/web/dist/assets/zh-TW-DCCYakXz.js +1 -0
  89. package/apps/web/dist/index.html +8 -8
  90. package/docs/api.md +33 -3
  91. package/docs/cli.md +1 -1
  92. package/package.json +1 -1
  93. package/apps/web/dist/assets/admin-settings-CO7nnD9T.js +0 -1
  94. package/apps/web/dist/assets/az-AZ-R8_HFWJr.js +0 -1
  95. package/apps/web/dist/assets/configuration-Bnt72HRu.js +0 -1
  96. package/apps/web/dist/assets/de-DE-BHgMj19Y.js +0 -1
  97. package/apps/web/dist/assets/default-DqsN66dq.js +0 -56
  98. package/apps/web/dist/assets/delivery-settings-Cji0N0m0.js +0 -1
  99. package/apps/web/dist/assets/el-GR-CQFGM5Sr.js +0 -1
  100. package/apps/web/dist/assets/es-ES-DZkcRVA4.js +0 -1
  101. package/apps/web/dist/assets/fr-FR-CRPAezXU.js +0 -1
  102. package/apps/web/dist/assets/hi-IN-2YFE4UQk.js +0 -1
  103. package/apps/web/dist/assets/id-ID-B-J0_QIW.js +0 -1
  104. package/apps/web/dist/assets/index-BAnwOrh1.css +0 -2
  105. package/apps/web/dist/assets/index-nltuyQtq.js +0 -17
  106. package/apps/web/dist/assets/it-IT-CJ0HYggO.js +0 -1
  107. package/apps/web/dist/assets/ja-JP-DDQS7D7b.js +0 -1
  108. package/apps/web/dist/assets/ko-KR-CkpoewEa.js +0 -1
  109. package/apps/web/dist/assets/mk-MK-C0RhS6XR.js +0 -1
  110. package/apps/web/dist/assets/nl-NL-BSKqlzcL.js +0 -1
  111. package/apps/web/dist/assets/pl-PL-CHz38SiU.js +0 -1
  112. package/apps/web/dist/assets/project-lifecycle-COZb9FqM.js +0 -1
  113. package/apps/web/dist/assets/project-settings-DwmTm9KA.js +0 -1
  114. package/apps/web/dist/assets/pt-BR-BotWHRiO.js +0 -1
  115. package/apps/web/dist/assets/public-project-B3hEkeCF.js +0 -1
  116. package/apps/web/dist/assets/ru-RU-DtOwNna6.js +0 -1
  117. package/apps/web/dist/assets/settings-3IXm9lJ-.js +0 -1
  118. package/apps/web/dist/assets/ticket-editor-CZnZQMG1.js +0 -2
  119. package/apps/web/dist/assets/ticket-list-controls-BxGKCZMo.js +0 -1
  120. package/apps/web/dist/assets/tr-TR-CdG2alU2.js +0 -1
  121. package/apps/web/dist/assets/uk-UA-Ceu5gorw.js +0 -1
  122. package/apps/web/dist/assets/vi-VN-CxshgV4W.js +0 -1
  123. package/apps/web/dist/assets/zh-CN-D7Yrjhd3.js +0 -1
  124. package/apps/web/dist/assets/zh-TW-DyF38kqx.js +0 -1
@@ -1,7 +1,7 @@
1
- import { a as auth_schema_exports, d as user$1, i as apikey, n as createDatabase, r as account, t as readConfig, u as session } from "./config-BQw6mPzE.mjs";
2
- import { A as hasAdminRole, B as memberSchema, Bt as searchSettingsSchema, C as errorSchema, Ft as instanceUpdateSchema, G as notificationReadInput, H as notificationIdsInput, I as isOperation, It as photoInputSchema, L as isPresetColor, Lt as photoResultSchema, M as importProviderDefinitions, Mt as adminInstanceSchema, Pt as brokerSettingsSchema, R as isPublicOperation, Rt as s3SettingsSchema, U as notificationPreferencesInput, W as notificationPreferencesSchema, Y as operations, at as publicOperations, c as credentialInput, g as defaultWorkflow, ht as scopes, j as importLimits, mt as roles, ot as pushDeviceInput, q as okSchema, s as closedStates, t as ApiError, wt as ticketExportFormat, xt as stateLabels } from "./src-BaTNDJB7.mjs";
1
+ import { a as auth_schema_exports, d as user$1, i as apikey, n as createDatabase, r as account, t as readConfig, u as session } from "./config-CQVdVhXv.mjs";
2
+ import { B as isPublicOperation, Bt as instanceUpdateSchema, G as notificationIdsInput, Gt as searchSettingsSchema, H as memberSchema, Ht as photoResultSchema, J as notificationReadInput, K as notificationPreferencesInput, Lt as adminInstanceSchema, M as hasAdminRole, N as importLimits, Ot as ticketExportFormat, P as importProviderDefinitions, Q as operations, R as isOperation, T as errorSchema, Tt as stateLabels, Ut as s3SettingsSchema, Vt as photoInputSchema, X as okSchema, _t as roles, ct as publicOperations, l as closedStates, lt as pushDeviceInput, q as notificationPreferencesSchema, t as ApiError, u as credentialInput, v as defaultWorkflow, vt as scopes, z as isPresetColor, zt as brokerSettingsSchema } from "./src-D70GmZ5R.mjs";
3
3
  import { t as migrateDatabase } from "./migrate-C9f4ozB4.mjs";
4
- import { $ as stagedUploads, A as requireSession, B as attachmentContent, C as defaultSenders, D as createAccess, E as createHttpSender, F as decodeImage, G as events, H as blobObjects, I as imageResponse, J as notificationPreferences, L as stagedUploadCutoff, M as createTicketSearch, N as createTicketIndex, O as createWriteLimiter, P as createBlobStorage, Q as pushDevices, R as createObjectStore, S as receiveRepositoryWebhook, T as DeliveryError, U as deliveryCheckpoints, V as attachments, W as eventClock, X as profilePhotos, Y as notifications, Z as projects, _ as one, a as deliverySecrets, at as matchesOIDCBinding, b as requireRow, c as ownerBlocker, d as createDomain, et as storageBackends, f as errorEnvelope, g as notify, h as json, i as createInstance, it as workspaces, j as readCalendarFeed, k as requireInstanceAdmin, l as appendEvents, m as intakeOnly, n as emailCodesAvailable, nt as tickets, o as otherActiveAdministrator, p as fail, q as members, r as signInProviders, rt as workspaceMembers, s as ownedWorkspaces, t as createAuth, tt as subscriptions, u as assertVersion, v as reachable, w as deliverBatch, x as processImports, y as recipientColumns, z as createPollingJob } from "./auth-CFjetlB4.mjs";
4
+ import { $ as pushDevices, A as requireInstanceAdmin, B as createPollingJob, C as defaultSenders, D as receiveUpload, E as createHttpSender, F as createBlobStorage, G as eventClock, H as attachments, I as decodeImage, J as members, K as events, L as imageResponse, M as readCalendarFeed, N as createTicketSearch, O as createAccess, P as createTicketIndex, Q as projects, R as stagedUploadCutoff, S as receiveRepositoryWebhook, T as DeliveryError, U as blobObjects, V as attachmentContent, W as deliveryCheckpoints, X as notifications, Y as notificationPreferences, Z as profilePhotos, _ as one, a as deliverySecrets, at as workspaces, b as requireRow, c as ownerBlocker, d as createDomain, et as stagedUploads, f as errorEnvelope, g as notify, h as json, i as createInstance, it as workspaceMembers, j as requireSession, k as createWriteLimiter, l as appendEvents, m as intakeOnly, n as emailCodesAvailable, nt as subscriptions, o as otherActiveAdministrator, ot as matchesOIDCBinding, p as fail, r as signInProviders, rt as tickets, s as ownedWorkspaces, t as createAuth, tt as storageBackends, u as assertVersion, v as reachable, w as deliverBatch, x as processImports, y as recipientColumns, z as createObjectStore } from "./auth-D43XyHdK.mjs";
5
5
  import { serve } from "@hono/node-server";
6
6
  import pg from "pg";
7
7
  import { drizzle } from "drizzle-orm/node-postgres";
@@ -9,8 +9,8 @@ import { and, asc, count, desc, eq, gt, ilike, inArray, isNotNull, isNull, notIn
9
9
  import { z } from "zod";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { createHash, createHmac, timingSafeEqual } from "node:crypto";
12
- import { Expo } from "expo-server-sdk";
13
12
  import { isDeepStrictEqual } from "node:util";
13
+ import { Expo } from "expo-server-sdk";
14
14
  import { EventEmitter } from "node:events";
15
15
  import { connect } from "amqplib";
16
16
  import { OpenAPIHono, createRoute, z as z$1 } from "@hono/zod-openapi";
@@ -359,7 +359,7 @@ function createCleanup() {
359
359
  }
360
360
  //#endregion
361
361
  //#region src/version.ts
362
- const applicationVersion = "0.6.1";
362
+ const applicationVersion = "0.7.0";
363
363
  //#endregion
364
364
  //#region src/realtime.ts
365
365
  async function createRealtime(config) {
@@ -3121,49 +3121,138 @@ const defaultAdapters = {
3121
3121
  };
3122
3122
  //#endregion
3123
3123
  //#region src/mcp.ts
3124
+ const operationNames = Object.keys(operations).filter(isOperation);
3125
+ const coreNames = operationNames.filter((name) => operations[name].mcp === "core");
3126
+ const instructions = "Ticket keys look like KAN-12, and a key's prefix or the project's linked git remote works as projectId. To work a ticket: claim_ticket, keep its status current with update_ticket, add_comment on what you did, add_evidence, then set status done and release_claim. Leave tickets with automationReady false to people. The core tools are listed. Reach every other operation with find_operation, then read_operation for reads or run_operation for writes. In replies, a missing key means null or empty; lists return summary rows unless view is full.";
3127
+ const defaultView = "summary";
3128
+ const toolInput = (name) => {
3129
+ const input = operations[name].input;
3130
+ return "view" in input.shape ? input.safeExtend({ view: z.enum([defaultView, "full"]).default(defaultView) }) : input;
3131
+ };
3132
+ const echoedFields = (name) => {
3133
+ const { shape } = toolInput(name);
3134
+ const { changes } = shape;
3135
+ return ["description", "acceptanceCriteria"].filter((field) => field in shape || changes instanceof z.ZodObject && field in changes.shape);
3136
+ };
3137
+ const isEmpty = (value) => value === null || typeof value === "object" && Object.keys(value).length === 0;
3138
+ const compact = (value, echoed) => {
3139
+ if (Array.isArray(value)) return value.map((item) => compact(item, echoed));
3140
+ if (typeof value !== "object" || value === null) return value;
3141
+ const dropped = "acceptanceCriteria" in value ? echoed : [];
3142
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !dropped.includes(key)).map(([key, field]) => [key, compact(field, echoed)]).filter(([, field]) => !isEmpty(field)));
3143
+ };
3144
+ const known = (name) => {
3145
+ if (!isOperation(name)) throw new ApiError(404, "not_found", `No operation is named ${name}`);
3146
+ return name;
3147
+ };
3148
+ const summary = (name) => ({
3149
+ name,
3150
+ description: operations[name].description,
3151
+ access: operations[name].method === "GET" ? "read" : "write"
3152
+ });
3153
+ function findOperation({ query = "", name }) {
3154
+ if (name !== void 0) {
3155
+ const operation = known(name), spec = operations[operation];
3156
+ return {
3157
+ ...summary(operation),
3158
+ method: spec.method,
3159
+ scope: spec.scope,
3160
+ inputSchema: z.toJSONSchema(toolInput(operation), { io: "input" })
3161
+ };
3162
+ }
3163
+ const terms = query.toLowerCase().split(/\s+/);
3164
+ return { operations: operationNames.map(summary).filter((row) => terms.every((term) => `${row.name} ${row.description}`.toLowerCase().includes(term))) };
3165
+ }
3166
+ const reply = async (name, work) => {
3167
+ try {
3168
+ return {
3169
+ content: [{
3170
+ type: "text",
3171
+ text: `${name} completed.`
3172
+ }],
3173
+ structuredContent: { result: await work() }
3174
+ };
3175
+ } catch (error) {
3176
+ return {
3177
+ isError: true,
3178
+ content: [{
3179
+ type: "text",
3180
+ text: JSON.stringify(errorEnvelope(error).body)
3181
+ }]
3182
+ };
3183
+ }
3184
+ };
3185
+ const runnerInput = {
3186
+ name: z.string().max(80).describe("Operation name from find_operation."),
3187
+ input: z.record(z.string(), z.unknown()).default({}).describe("The operation's input.")
3188
+ };
3124
3189
  function createMcp(auth, domain, access, config, limitWrite) {
3125
- const serve = (request, identity, limited = false) => createMcpHandler(() => {
3126
- const server = new McpServer({
3127
- name: "Kanvil",
3128
- version: applicationVersion
3190
+ const serve = (request, identity, limited = false) => {
3191
+ const execute = async (name, input) => {
3192
+ if (limited && operations[name].method !== "GET") limitWrite(identity.ownerId);
3193
+ const value = "view" in operations[name].input.shape ? {
3194
+ view: defaultView,
3195
+ ...input
3196
+ } : input;
3197
+ return compact(await domain.execute(name, value, identity), echoedFields(name));
3198
+ };
3199
+ const runner = (write) => (args) => reply(args.name, () => {
3200
+ const name = known(args.name);
3201
+ if (operations[name].method !== "GET" !== write) throw new ApiError(400, "invalid_request", `${name} ${write ? "only reads; use read_operation" : "writes; use run_operation"}`);
3202
+ return execute(name, args.input);
3129
3203
  });
3130
- for (const name of Object.keys(operations).filter(isOperation)) {
3131
- const spec = operations[name];
3132
- const inputSchema = spec.input.shape;
3133
- server.registerTool(name, {
3134
- description: spec.description,
3135
- inputSchema,
3136
- outputSchema: { result: spec.output },
3204
+ return createMcpHandler(() => {
3205
+ const server = new McpServer({
3206
+ name: "Kanvil",
3207
+ version: applicationVersion
3208
+ }, { instructions });
3209
+ for (const name of coreNames) {
3210
+ const spec = operations[name];
3211
+ const inputSchema = toolInput(name).shape;
3212
+ server.registerTool(name, {
3213
+ description: spec.description,
3214
+ inputSchema,
3215
+ annotations: {
3216
+ readOnlyHint: spec.method === "GET",
3217
+ destructiveHint: false,
3218
+ idempotentHint: spec.method === "GET",
3219
+ openWorldHint: false
3220
+ }
3221
+ }, (input) => reply(name, () => execute(name, input)));
3222
+ }
3223
+ server.registerTool("find_operation", {
3224
+ description: "Find an operation that is not a listed tool. `query` matches names and descriptions and marks each match as read or write; `name` returns one operation with its input JSON Schema, method, and scope.",
3225
+ inputSchema: {
3226
+ query: z.string().max(200).optional(),
3227
+ name: z.string().max(80).optional()
3228
+ },
3137
3229
  annotations: {
3138
- readOnlyHint: spec.method === "GET",
3139
- destructiveHint: /^(delete|remove|recover)_/.test(name),
3140
- idempotentHint: spec.method === "GET",
3230
+ readOnlyHint: true,
3231
+ idempotentHint: true,
3141
3232
  openWorldHint: false
3142
3233
  }
3143
- }, async (input) => {
3144
- try {
3145
- if (limited && spec.method !== "GET") limitWrite(identity.ownerId);
3146
- const result = await domain.execute(name, input, identity);
3147
- return {
3148
- content: [{
3149
- type: "text",
3150
- text: `${name} completed.`
3151
- }],
3152
- structuredContent: { result }
3153
- };
3154
- } catch (error) {
3155
- return {
3156
- isError: true,
3157
- content: [{
3158
- type: "text",
3159
- text: JSON.stringify(errorEnvelope(error).body)
3160
- }]
3161
- };
3234
+ }, (input) => reply("find_operation", () => findOperation(input)));
3235
+ server.registerTool("read_operation", {
3236
+ description: "Run a read operation found with find_operation.",
3237
+ inputSchema: runnerInput,
3238
+ annotations: {
3239
+ readOnlyHint: true,
3240
+ idempotentHint: true,
3241
+ openWorldHint: false
3162
3242
  }
3163
- });
3164
- }
3165
- return server;
3166
- }, { legacy: "stateless" }).fetch(request);
3243
+ }, runner(false));
3244
+ server.registerTool("run_operation", {
3245
+ description: "Run a write operation found with find_operation.",
3246
+ inputSchema: runnerInput,
3247
+ annotations: {
3248
+ readOnlyHint: false,
3249
+ destructiveHint: true,
3250
+ openWorldHint: false
3251
+ }
3252
+ }, runner(true));
3253
+ return server;
3254
+ }, { legacy: "stateless" }).fetch(request);
3255
+ };
3167
3256
  const oauth = requireMcpAuth(auth, async (request, claims) => {
3168
3257
  if (typeof claims.sub !== "string") throw new ApiError(401, "unauthorized", "A user subject is required");
3169
3258
  const granted = typeof claims.scope === "string" ? claims.scope.split(" ") : [];
@@ -3319,13 +3408,14 @@ async function initializeApplication(database, config, adapters, cleanup) {
3319
3408
  } }, { status: 400 });
3320
3409
  } });
3321
3410
  app.use("*", secureHeaders());
3322
- app.use("*", bodyLimit({
3411
+ const limitBody = bodyLimit({
3323
3412
  maxSize: 15e6,
3324
3413
  onError: () => Response.json({ error: {
3325
3414
  code: "payload_too_large",
3326
3415
  message: "The request exceeds the upload limit"
3327
3416
  } }, { status: 413 })
3328
- }));
3417
+ });
3418
+ app.use("*", (c, next) => c.req.path.startsWith("/api/uploads/") ? next() : limitBody(c, next));
3329
3419
  app.onError((error) => {
3330
3420
  if (error instanceof APIError && error.statusCode >= 400 && error.statusCode < 500) return Response.json({ error: {
3331
3421
  code: typeof error.body?.code === "string" ? error.body.code : "authentication_error",
@@ -3421,6 +3511,33 @@ async function initializeApplication(database, config, adapters, cleanup) {
3421
3511
  "Referrer-Policy": "no-referrer"
3422
3512
  } });
3423
3513
  });
3514
+ app.openAPIRegistry.registerPath({
3515
+ method: "put",
3516
+ path: "/api/uploads/{token}",
3517
+ request: {
3518
+ params: z$1.object({ token: z$1.string() }),
3519
+ body: { content: { "application/octet-stream": { schema: z$1.string() } } }
3520
+ },
3521
+ responses: {
3522
+ 200: { description: "The staged upload, or the attachment when the URL names a ticket" },
3523
+ 400: { description: "The body or its Content-Length is not the declared size" },
3524
+ 404: { description: "Unknown upload URL" },
3525
+ 410: { description: "The upload URL was already used or expired" }
3526
+ }
3527
+ });
3528
+ app.put("/api/uploads/:token", async (c) => Response.json(await receiveUpload(domain, c.req.param("token"), c.req.header("content-length"), async (limit) => {
3529
+ const chunks = [], reader = c.req.raw.body?.getReader();
3530
+ if (!reader) return Buffer.alloc(0);
3531
+ let total = 0;
3532
+ while (total <= limit) {
3533
+ const { done, value } = await reader.read();
3534
+ if (done) return Buffer.concat(chunks);
3535
+ chunks.push(value);
3536
+ total += value.length;
3537
+ }
3538
+ await reader.cancel();
3539
+ return Buffer.concat(chunks);
3540
+ })));
3424
3541
  app.openAPIRegistry.registerPath({
3425
3542
  method: "post",
3426
3543
  path: "/api/repository-hooks/{connectionId}",
@@ -1,4 +1,4 @@
1
- import { n as createDatabase, t as readConfig } from "./config-BQw6mPzE.mjs";
1
+ import { n as createDatabase, t as readConfig } from "./config-CQVdVhXv.mjs";
2
2
  import { t as migrateDatabase } from "./migrate-C9f4ozB4.mjs";
3
3
  //#region src/cli/migrate.ts
4
4
  const database = createDatabase(readConfig());
@@ -1,6 +1,6 @@
1
- import { d as user, n as createDatabase, r as account, t as readConfig, u as session } from "./config-BQw6mPzE.mjs";
2
- import { A as hasAdminRole } from "./src-BaTNDJB7.mjs";
3
- import { K as instanceSettings, p as fail, t as createAuth } from "./auth-CFjetlB4.mjs";
1
+ import { d as user, n as createDatabase, r as account, t as readConfig, u as session } from "./config-CQVdVhXv.mjs";
2
+ import { M as hasAdminRole } from "./src-D70GmZ5R.mjs";
3
+ import { p as fail, q as instanceSettings, t as createAuth } from "./auth-D43XyHdK.mjs";
4
4
  import { and, eq, isNull, sql } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
  import { parseArgs } from "node:util";
@@ -290,10 +290,15 @@ var en_US_default = {
290
290
  priority: "Priority",
291
291
  kind: "Kind",
292
292
  difficulty: "Difficulty",
293
+ automation: "Agent readiness",
293
294
  label: "Label",
294
295
  assigneeId: "Assignee",
295
296
  customField: "Field"
296
297
  },
298
+ automationFilter: {
299
+ ready: "Ready for agents",
300
+ manual: "Needs a person"
301
+ },
297
302
  sort: {
298
303
  priority: "Priority",
299
304
  updated: "Recently updated",
@@ -535,6 +540,7 @@ var en_US_default = {
535
540
  time_entry_conflict: "This actor already has a running entry on the ticket.",
536
541
  claim_conflict: "Another worker holds this ticket.",
537
542
  idempotency_conflict: "This request was already used for another change.",
543
+ duplicate_repository: "Another project in this workspace links this repository.",
538
544
  project_archived: "Restore the archived project before changing it.",
539
545
  rate_limited: "Too many changes in a short time. Wait and retry.",
540
546
  snapshot_required: "The live feed fell behind. Reload to continue.",
@@ -1162,6 +1168,7 @@ var en_US_default = {
1162
1168
  priority: "All priorities",
1163
1169
  kind: "All kinds",
1164
1170
  difficulty: "All difficulties",
1171
+ automation: "Any readiness",
1165
1172
  label: "All labels",
1166
1173
  assigneeId: "Anyone",
1167
1174
  customField: "Any field value"
@@ -1171,6 +1178,7 @@ var en_US_default = {
1171
1178
  priority: "Search priorities",
1172
1179
  kind: "Search kinds",
1173
1180
  difficulty: "Search difficulties",
1181
+ automation: "Search readiness",
1174
1182
  label: "Search labels",
1175
1183
  assigneeId: "Search people",
1176
1184
  customField: "Search fields"
@@ -1703,6 +1711,9 @@ var en_US_default = {
1703
1711
  changePrefixDescription: "Every ticket in {{name}} takes the new prefix and keeps its number.",
1704
1712
  changing: "Changing…",
1705
1713
  prefixChanged: "Tickets now use {{key}}",
1714
+ repositories: "Repositories",
1715
+ repositoriesDescription: "Agents can pass one of these git remotes as the project ID.",
1716
+ repositoriesInvalid: "Enter up to 20 git remotes, such as git@github.com:owner/repo.git.",
1706
1717
  publicLink: "Public link",
1707
1718
  publicDisclosure: "Anyone with the link can read every unarchived ticket's title, description, acceptance criteria, status, priority, kind, difficulty, labels, scopes, dates, and links to other tickets in this project. People, comments, activity, attachments, evidence, and claims stay private.",
1708
1719
  publicOn: "Public link on",
@@ -2937,6 +2948,7 @@ const scopes = [
2937
2948
  ];
2938
2949
  const attachmentLimit = 1e7;
2939
2950
  const stagedUploadLifetime = 864e5;
2951
+ const uploadUrlLifetime = 9e5;
2940
2952
  const stateLabels = labelTable(states, (value) => t(`common.status.${value}`));
2941
2953
  const priorityLabels = labelTable(priorities, (value) => t(`common.priority.${value}`));
2942
2954
  ticketSymbols.status.backlog.tone, ticketSymbols.status.todo.tone, ticketSymbols.status.in_progress.tone, ticketSymbols.status.in_review.tone, ticketSymbols.status.done.tone, ticketSymbols.status.cancelled.tone;
@@ -3053,9 +3065,30 @@ const placement = {
3053
3065
  after: id.optional()
3054
3066
  };
3055
3067
  const oneNeighbor = (input) => input.before === void 0 || input.after === void 0;
3056
- const projectInput = z.object({ projectId: id });
3068
+ const projectInput = z.object({ projectId: z.string().min(1).max(300) });
3057
3069
  const ticketInput = projectInput.extend({ ticketId: id });
3058
3070
  const projectKey = z.string().trim().regex(/^[A-Z][A-Z0-9]{1,9}$/);
3071
+ const remoteHost = "(?<host>[a-z0-9][a-z0-9.-]*)";
3072
+ const remotePath = "(?<path>[^?#]+)";
3073
+ const remoteForms = [
3074
+ new RegExp(`^(?<scheme>https|ssh)://(?:[^/]*@)?${remoteHost}(?::(?<port>\\d+))?/${remotePath}$`),
3075
+ new RegExp(`^[^/:@]+@${remoteHost}:${remotePath}$`),
3076
+ new RegExp(`^${remoteHost}(?::(?<port>\\d+))?/${remotePath}$`)
3077
+ ];
3078
+ const defaultRemotePorts = {
3079
+ https: "443",
3080
+ ssh: "22"
3081
+ };
3082
+ function normalizeRemote(value) {
3083
+ const remote = value.trim().toLowerCase();
3084
+ if (/\s/.test(remote)) return null;
3085
+ const { scheme, host, port, path } = remoteForms.map((form) => form.exec(remote)).find(Boolean)?.groups ?? {};
3086
+ const segments = path?.replace(/\/+$/, "").replace(/\.git$/, "").split("/").filter(Boolean);
3087
+ if (!host || !segments?.length) return null;
3088
+ return `${host}${port && port !== defaultRemotePorts[scheme ?? ""] ? `:${port}` : ""}/${segments.join("/")}`;
3089
+ }
3090
+ const repositoryRemote = z.string().max(300).refine((value) => normalizeRemote(value) !== null, "Enter a git remote URL").overwrite((value) => normalizeRemote(value) ?? value);
3091
+ const projectRepositories = z.array(repositoryRemote).max(20).overwrite((remotes) => [...new Set(remotes)]);
3059
3092
  const reason = z.object({
3060
3093
  code: z.string(),
3061
3094
  message: z.string(),
@@ -3072,15 +3105,19 @@ const filterFieldNames = [
3072
3105
  "priority",
3073
3106
  "kind",
3074
3107
  "difficulty",
3108
+ "automation",
3075
3109
  "label",
3076
3110
  "assigneeId",
3077
3111
  "customField"
3078
3112
  ];
3113
+ const automationFilters = ["ready", "manual"];
3114
+ labelTable(automationFilters, (value) => t(`common.automationFilter.${value}`));
3079
3115
  const filterFields = {
3080
3116
  status: z.enum(states),
3081
3117
  priority: z.enum(priorities),
3082
3118
  kind: z.enum(kinds),
3083
3119
  difficulty: z.enum(difficulties),
3120
+ automation: z.enum(automationFilters),
3084
3121
  label: z.string().max(40),
3085
3122
  assigneeId: z.union([id, z.literal("none")]),
3086
3123
  customField: z.string().regex(/^[0-9a-f-]{36}:(?:none|true|[0-9a-f-]{36})$/)
@@ -3098,6 +3135,8 @@ const filterShape = {
3098
3135
  kindNot: filterValues(filterFields.kind),
3099
3136
  difficulty: filterValues(filterFields.difficulty),
3100
3137
  difficultyNot: filterValues(filterFields.difficulty),
3138
+ automation: filterValues(filterFields.automation),
3139
+ automationNot: filterValues(filterFields.automation),
3101
3140
  label: filterValues(filterFields.label),
3102
3141
  labelNot: filterValues(filterFields.label),
3103
3142
  assigneeId: filterValues(filterFields.assigneeId),
@@ -3167,6 +3206,7 @@ const ticketFilter = z.object({
3167
3206
  ]).default("priority"),
3168
3207
  order: z.enum(["asc", "desc"]).optional()
3169
3208
  }).strict().superRefine(rejectConflicts);
3209
+ const listSort = ticketFilter.shape.sort.unwrap().optional();
3170
3210
  labelTable(ticketFilter.shape.sort.unwrap().options, (sort) => t(`common.sort.${sort}`));
3171
3211
  const defaultOrder = {
3172
3212
  priority: "asc",
@@ -3275,6 +3315,7 @@ const projectSchema = z.object({
3275
3315
  key: z.string(),
3276
3316
  name: z.string(),
3277
3317
  description: z.string(),
3318
+ repositories: z.array(z.string()).default([]),
3278
3319
  version,
3279
3320
  archivedAt: timestamp.nullable().default(null),
3280
3321
  createdAt: timestamp,
@@ -3445,6 +3486,11 @@ const attachmentSchema = z.object({
3445
3486
  createdAt: timestamp
3446
3487
  });
3447
3488
  const stagedUploadSchema = attachmentSchema.omit({ ticketId: true }).extend({ expiresAt: timestamp });
3489
+ const uploadUrlSchema = z.object({
3490
+ uploadId: z.uuid(),
3491
+ url: z.url(),
3492
+ expiresAt: timestamp
3493
+ });
3448
3494
  const httpUrl = z.url().regex(/^https?:\/\//i, "Use an HTTP or HTTPS URL");
3449
3495
  const evidenceFields = z.object({
3450
3496
  kind: z.enum([
@@ -4210,21 +4256,25 @@ const page = (item) => z.object({
4210
4256
  items: z.array(item),
4211
4257
  total: z.number().int().nonnegative()
4212
4258
  });
4213
- const fileFields = {
4259
+ const fileMetadata = {
4214
4260
  name: text.max(255).refine((value) => {
4215
4261
  for (const character of value) if (character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) return false;
4216
4262
  return true;
4217
4263
  }, "Use a filename without control characters"),
4218
- contentType: z.string().max(120).regex(/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i).default("application/octet-stream"),
4264
+ contentType: z.string().max(120).regex(/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i).default("application/octet-stream")
4265
+ };
4266
+ const fileFields = {
4267
+ ...fileMetadata,
4219
4268
  data: z.string().max(Math.ceil(attachmentLimit / 3) * 4)
4220
4269
  };
4221
- const op = (input, output, method, path, scope, description) => ({
4270
+ const op = (input, output, method, path, scope, description, options = {}) => ({
4222
4271
  input,
4223
4272
  output,
4224
4273
  method,
4225
4274
  path,
4226
4275
  scope,
4227
- description
4276
+ description,
4277
+ ...options
4228
4278
  });
4229
4279
  const operations = {
4230
4280
  list_workspaces: op(z.object({}), z.array(workspaceSchema), "GET", "/workspaces", "read", "List accessible workspaces and the current membership role."),
@@ -4414,8 +4464,9 @@ const operations = {
4414
4464
  publicReadEnabled: z.boolean().optional(),
4415
4465
  name: text.max(120),
4416
4466
  description: z.string().max(5e3),
4417
- key: projectKey.optional()
4418
- }).strict(), projectSchema, "PATCH", "/projects/{projectId}", "admin", "Update project details. A new `key` re-keys every ticket of the project; the previous prefix stays reserved for the project, and the project and every key issued under it keep resolving. `publicReadEnabled` shares or stops sharing the project's read-only public view (see the public operations)."),
4467
+ key: projectKey.optional(),
4468
+ repositories: projectRepositories.optional()
4469
+ }).strict(), projectSchema, "PATCH", "/projects/{projectId}", "admin", "Update project details. A new `key` re-keys every ticket of the project; the previous prefix stays reserved for the project, and the project and every key issued under it keep resolving. `publicReadEnabled` shares or stops sharing the project's read-only public view (see the public operations). `repositories` replaces the project's linked git remotes (at most 20), and any `projectId` then accepts one of them; within a workspace only one unarchived project can hold a remote."),
4419
4470
  move_project: op(projectInput.extend({
4420
4471
  ...mutation,
4421
4472
  expectedVersion: version,
@@ -4479,13 +4530,14 @@ const operations = {
4479
4530
  }).strict(), okSchema, "POST", "/projects/{projectId}/members", "admin", "Add or update an existing user's membership with a seeded role or a custom role of the project's workspace, or remove it with a null role. Needs `member.manage`; the role, and any role the person holds now, must grant nothing the caller lacks."),
4480
4531
  list_tickets: op(projectInput.extend({
4481
4532
  ...ticketFilter.shape,
4533
+ sort: listSort,
4482
4534
  ...pagination,
4483
4535
  relatedTo: id.optional(),
4484
4536
  stateId: z.uuid().optional(),
4485
4537
  scheduledFrom: timestamp.optional(),
4486
4538
  scheduledTo: timestamp.optional(),
4487
4539
  ...ticketView
4488
- }).superRefine(rejectConflicts), page(ticketRow), "GET", "/projects/{projectId}/tickets", "read", "Search tickets with server-side filters, stable ordering, and pagination. Each property filter takes one value or a repeated list (any of them); its `Not` twin excludes values. Double-quoted segments of `q` match as exact phrases. Pass `view: \"summary\"` when scanning results: rows then omit the description and acceptance criteria."),
4540
+ }).superRefine(rejectConflicts), page(ticketRow), "GET", "/projects/{projectId}/tickets", "read", "Search tickets with server-side filters, stable ordering, and pagination. Each property filter takes one value or a repeated list (any of them); its `Not` twin excludes values. `q` keeps only tickets whose key, title, description, acceptance criteria, labels, or scopes contain its text as written, or whose key or ID it names exactly; double-quoted segments are exact phrases that must each appear on word boundaries, so `\"IME\"` does not match `time` while unquoted `IME` does. With `q` and no `sort`, results order by relevance as in search_tickets; otherwise by `sort` (default `priority`). With `view: \"summary\"`, rows omit the description and acceptance criteria."),
4489
4541
  list_ticket_labels: op(projectInput.extend({ archived: ticketFilter.shape.archived }), z.array(z.object({
4490
4542
  name: z.string(),
4491
4543
  count: z.number().int().nonnegative()
@@ -4579,7 +4631,7 @@ const operations = {
4579
4631
  claim: claimSchema.nullable(),
4580
4632
  subscribed: z.boolean(),
4581
4633
  canReleaseClaim: z.boolean()
4582
- }), "GET", "/projects/{projectId}/tickets/{ticketId}", "read", "Read a ticket by ID or key, with readiness and ownership."),
4634
+ }), "GET", "/projects/{projectId}/tickets/{ticketId}", "read", "Read a ticket by ID or key, with readiness and ownership.", { mcp: "core" }),
4583
4635
  create_ticket: op(projectInput.extend({
4584
4636
  ...mutation,
4585
4637
  title: ticketFields.shape.title,
@@ -4601,14 +4653,14 @@ const operations = {
4601
4653
  archived: z.boolean().default(false),
4602
4654
  uploads: z.array(z.uuid()).max(50).default([]),
4603
4655
  customFields: customFieldValues.default({})
4604
- }).strict(), ticketSchema, "POST", "/projects/{projectId}/tickets", "write", "Create a ticket. `customFields` maps field IDs from list_custom_fields to values; omitted fields take their default, and a required field must end with a value. `uploads` lists your staged inline uploads in this project; they become its attachments in the same write, or nothing is created. Reuse requestId when retrying this write."),
4656
+ }).strict(), ticketSchema, "POST", "/projects/{projectId}/tickets", "write", "Create a ticket. `customFields` maps field IDs from list_custom_fields to values; omitted fields take their default, and a required field must end with a value. `uploads` lists your staged inline uploads in this project; they become its attachments in the same write, or nothing is created. Reuse requestId when retrying this write.", { mcp: "core" }),
4605
4657
  duplicate_ticket: op(ticketInput.extend(mutation).strict(), ticketSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/duplicate", "write", "Copy a ticket into a new To do ticket in the same project, linked to the original by a clones relationship. Copies requirements, properties, labels, scopes, and parent; resets agent readiness and never copies claims, comments, evidence, attachments, or children. Reuse requestId when retrying this write."),
4606
4658
  update_ticket: op(ticketInput.extend({
4607
4659
  ...mutation,
4608
4660
  expectedVersion: version,
4609
4661
  changes: ticketChanges,
4610
4662
  claimId: z.uuid().optional()
4611
- }).strict(), ticketSchema, "PATCH", "/projects/{projectId}/tickets/{ticketId}", "write", "Update a ticket with optimistic concurrency. Worker completion requires its active claim. `changes.customFields` sets only the named fields; null clears one."),
4663
+ }).strict(), ticketSchema, "PATCH", "/projects/{projectId}/tickets/{ticketId}", "write", "Update a ticket with optimistic concurrency. Worker completion requires its active claim. `changes.customFields` sets only the named fields; null clears one.", { mcp: "core" }),
4612
4664
  update_ticket_labels: op(ticketInput.extend({
4613
4665
  ...mutation,
4614
4666
  expectedVersion: version,
@@ -4638,7 +4690,7 @@ const operations = {
4638
4690
  resolve_ticket: op(z.object({ ticketId: id }).strict(), z.object({
4639
4691
  ticket: ticketSchema,
4640
4692
  project: projectRef
4641
- }), "GET", "/tickets/resolve", "read", "Find a ticket by ID, current key, or any earlier key from a prefix change or a move, across every accessible project. An unknown or inaccessible ticket reads as not found."),
4693
+ }), "GET", "/tickets/resolve", "read", "Find a ticket by ID, current key, or any earlier key from a prefix change or a move, across every accessible project. An unknown or inaccessible ticket reads as not found.", { mcp: "core" }),
4642
4694
  bulk_update_tickets: op(projectInput.extend({
4643
4695
  ...mutation,
4644
4696
  tickets: claimedTicketSelection,
@@ -4661,7 +4713,7 @@ const operations = {
4661
4713
  ticket: ticketRow,
4662
4714
  project: projectRef,
4663
4715
  snippet: z.string().optional()
4664
- })) }), "GET", "/tickets/search", "read", "Search unarchived tickets by key or text across every accessible project, optionally within one workspace. Results order by exact key match, then relevance, then recency. Double-quoted segments of `q` match as exact phrases. Pass `view: \"summary\"` when scanning results: each ticket is then a compact row and the result carries a `snippet` of the matching text."),
4716
+ })) }), "GET", "/tickets/search", "read", "Search unarchived tickets by key or text across every accessible project, optionally within one workspace. Results order by exact key match, then relevance, then recency. Double-quoted segments of `q` match as exact phrases on word boundaries, so `\"IME\"` does not match `time`. With `view: \"summary\"`, each ticket is a compact row and the result carries a `snippet` of the matching text.", { mcp: "core" }),
4665
4717
  search_workspace: op(z.object({
4666
4718
  q: z.string().trim().min(1).max(200),
4667
4719
  workspaceId: z.uuid().optional(),
@@ -4676,19 +4728,19 @@ const operations = {
4676
4728
  counts: z.record(z.enum(searchResultTypes), z.number().int().nonnegative()),
4677
4729
  engine: z.enum(["postgres", "index"]),
4678
4730
  indexing: z.boolean()
4679
- }), "GET", "/search", "read", "Search accessible tickets, projects, comments, and activity by text or ticket key, optionally within one workspace or one result type. Property filters restrict results to matching tickets. Results order by exact key match, then type, then ticket relevance, then recency. Double-quoted segments of `q` match ticket text as exact phrases. Pass `view: \"summary\"` when scanning results: ticket results then carry their summary row and each result a `snippet` of the matching text."),
4731
+ }), "GET", "/search", "read", "Search accessible tickets, projects, comments, and activity by text or ticket key, optionally within one workspace or one result type. Property filters restrict results to matching tickets. Results order by exact key match, then type, then ticket relevance, then recency. Double-quoted segments of `q` match ticket text as exact phrases on word boundaries. With `view: \"summary\"`, ticket results carry their summary row and each result a `snippet` of the matching text."),
4680
4732
  list_my_work: op(z.object({
4681
4733
  workspaceId: z.uuid().optional(),
4682
4734
  q: ticketFilter.shape.q,
4683
- sort: ticketFilter.shape.sort,
4735
+ sort: listSort,
4684
4736
  order: ticketFilter.shape.order,
4685
4737
  activity: z.coerce.number().int().min(0).max(50).default(0),
4686
4738
  ...pagination
4687
- }).strict(), page(ticketSchema).extend({ activity: z.array(eventSchema).default([]) }), "GET", "/my-work", "read", "List open, unarchived tickets assigned to the current identity across every accessible project, optionally within one workspace, with the most recent accessible activity when `activity` is set."),
4739
+ }).strict(), page(ticketSchema).extend({ activity: z.array(eventSchema).default([]) }), "GET", "/my-work", "read", "List open, unarchived tickets assigned to the current identity across every accessible project, optionally within one workspace, with the most recent accessible activity when `activity` is set. `q` and `sort` work as in list_tickets.", { mcp: "core" }),
4688
4740
  list_eligible_tickets: op(projectInput.extend(pagination), page(z.object({
4689
4741
  ticket: ticketSchema,
4690
4742
  readiness: readinessSchema
4691
- })), "GET", "/projects/{projectId}/eligibility", "read", "Inspect eligibility and explicit blocking reasons for open tickets."),
4743
+ })), "GET", "/projects/{projectId}/eligibility", "read", "Inspect eligibility and explicit blocking reasons for open tickets.", { mcp: "core" }),
4692
4744
  list_relations: op(ticketInput, z.array(relationSchema), "GET", "/projects/{projectId}/tickets/{ticketId}/relations", "read", "List incoming and outgoing ticket relationships with each side's key, title, and project. A ticket in a project the caller cannot read appears by ID and key alone, with `restricted: true`."),
4693
4745
  add_relation: op(ticketInput.extend({
4694
4746
  ...mutation,
@@ -4704,7 +4756,7 @@ const operations = {
4704
4756
  add_comment: op(ticketInput.extend({
4705
4757
  ...mutation,
4706
4758
  body: text.max(5e4)
4707
- }).strict(), commentSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/comments", "write", "Add a comment with authenticated attribution. Mention a project member with `[@Name](mention:<user id>)` from list_members; members who can read the project are notified."),
4759
+ }).strict(), commentSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/comments", "write", "Add a comment with authenticated attribution. Mention a project member with `[@Name](mention:<user id>)` from list_members; members who can read the project are notified.", { mcp: "core" }),
4708
4760
  update_comment: op(projectInput.extend({
4709
4761
  ...mutation,
4710
4762
  commentId: z.uuid(),
@@ -4720,11 +4772,16 @@ const operations = {
4720
4772
  upload_attachment: op(ticketInput.extend({
4721
4773
  ...mutation,
4722
4774
  ...fileFields
4723
- }).strict(), attachmentSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/attachments", "write", "Upload up to 10 MB of base64-encoded bytes. The original filename is metadata, never a filesystem path. An intake credential can attach up to 10 files to a quarantined ticket it created, within 1 GB across its quarantined tickets."),
4775
+ }).strict(), attachmentSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/attachments", "write", "Upload up to 10 MB of base64-encoded bytes. The original filename is metadata, never a filesystem path. An intake credential can attach up to 10 files to a quarantined ticket it created, within 1 GB across its quarantined tickets. For a file on disk, use create_upload_url instead of inlining it."),
4724
4776
  stage_upload: op(projectInput.extend({
4725
4777
  ...mutation,
4726
4778
  ...fileFields
4727
- }).strict(), stagedUploadSchema, "POST", "/projects/{projectId}/uploads", "write", "Stage up to 10 MB of base64-encoded bytes for a ticket that does not exist yet. Only you can read it, at its attachment content address, until create_ticket claims it; unclaimed uploads expire after 24 hours."),
4779
+ }).strict(), stagedUploadSchema, "POST", "/projects/{projectId}/uploads", "write", "Stage up to 10 MB of base64-encoded bytes for a ticket that does not exist yet. Only you can read it, at its attachment content address, until create_ticket claims it; unclaimed uploads expire after 24 hours. For a file on disk, use create_upload_url instead of inlining it."),
4780
+ create_upload_url: op(projectInput.extend({
4781
+ ...fileMetadata,
4782
+ ticketId: id.optional(),
4783
+ size: z.number().int().nonnegative().max(attachmentLimit)
4784
+ }).strict(), uploadUrlSchema, "POST", "/projects/{projectId}/upload-urls", "write", "Get a single-use URL for one file of up to 10 MB, so its bytes never pass through the tool call. PUT exactly `size` bytes to `url` within 15 minutes, for example `curl --upload-file <path> <url>`; the URL needs no other credential and reads nothing. With `ticketId` the file becomes that ticket's attachment; without it, it is a staged upload that create_ticket `uploads` claims by `uploadId`."),
4728
4785
  discard_upload: op(projectInput.extend({
4729
4786
  ...mutation,
4730
4787
  uploadId: z.uuid()
@@ -4739,7 +4796,7 @@ const operations = {
4739
4796
  ...mutation,
4740
4797
  ...evidenceFields.shape,
4741
4798
  claimId: z.uuid().optional()
4742
- }).strict(), evidenceSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/evidence", "write", "Attach evidence with its commit and provenance."),
4799
+ }).strict(), evidenceSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/evidence", "write", "Attach evidence with its commit and provenance.", { mcp: "core" }),
4743
4800
  list_time_entries: op(ticketInput.extend(pagination), page(timeEntrySchema), "GET", "/projects/{projectId}/tickets/{ticketId}/time-entries", "read", "List observed time entries, newest first. Running entries have null endAt and durationMs."),
4744
4801
  get_time_entry: op(ticketInput.extend({ timeEntryId: z.uuid() }), timeEntrySchema, "GET", "/projects/{projectId}/tickets/{ticketId}/time-entries/{timeEntryId}", "read", "Read one attributed time entry within its current ticket and project."),
4745
4802
  create_time_entry: op(ticketInput.extend({
@@ -4785,18 +4842,18 @@ const operations = {
4785
4842
  claim_ticket: op(ticketInput.extend({
4786
4843
  ...mutation,
4787
4844
  workerId: text.max(200),
4788
- expectedVersion: version,
4845
+ expectedVersion: version.optional(),
4789
4846
  leaseSeconds: claimLeaseSecondsSchema.optional()
4790
- }).strict(), claimSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim", "claim", "Atomically claim ready work. Expired claims require explicit recovery."),
4847
+ }).strict(), claimSchema.extend({ ticket: ticketSchema }), "POST", "/projects/{projectId}/tickets/{ticketId}/claim", "claim", "Atomically claim ready work and return the claim with the claimed ticket. expectedVersion is optional; readiness always applies. Expired claims require explicit recovery.", { mcp: "core" }),
4791
4848
  renew_claim: op(ticketInput.extend({
4792
4849
  ...mutation,
4793
4850
  claimId: z.uuid(),
4794
4851
  leaseSeconds: claimLeaseSecondsSchema.optional()
4795
- }).strict(), claimSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/renew", "claim", "Renew the caller's unexpired claim."),
4852
+ }).strict(), claimSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/renew", "claim", "Renew the caller's unexpired claim.", { mcp: "core" }),
4796
4853
  release_claim: op(ticketInput.extend({
4797
4854
  ...mutation,
4798
4855
  claimId: z.uuid()
4799
- }).strict(), okSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/release", "claim", "Release the caller's claim."),
4856
+ }).strict(), okSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/release", "claim", "Release the caller's claim.", { mcp: "core" }),
4800
4857
  recover_claim: op(ticketInput.extend({
4801
4858
  ...mutation,
4802
4859
  claimId: z.uuid().optional(),
@@ -5084,4 +5141,4 @@ function parseOutput(name, value) {
5084
5141
  return operations[name].output.parse(value);
5085
5142
  }
5086
5143
  //#endregion
5087
- export { permissionLabels as $, hasAdminRole as A, workflowOrder as At, memberSchema as B, searchSettingsSchema as Bt, errorSchema as C, streamErrorSchema as Ct, filterKey as D, ticketPredicates as Dt, eventSchema as E, ticketImportSchema as Et, isHexColor as F, instanceUpdateSchema as Ft, notificationReadInput as G, notificationIdsInput as H, isOperation as I, photoInputSchema as It, operationPermission as J, notificationsSchema as K, isPresetColor as L, photoResultSchema as Lt, importProviderDefinitions as M, adminInstanceSchema as Mt, importSecretFields as N, authenticationPolicySchema as Nt, filterList as O, ticketSchema as Ot, invitationStatuses as P, brokerSettingsSchema as Pt, parsePublicOutput as Q, isPublicOperation as R, s3SettingsSchema as Rt, difficulties as S, states as St, eventLabels as T, ticketFilter as Tt, notificationPreferencesInput as U, mentionScheme as V, notificationPreferencesSchema as W, parseCustomFieldFilter as X, operations as Y, parseOutput as Z, deletedAccountName as _, seededPermissions as _t, checkpointSchema as a, publicOperations as at, destinationEventKeys as b, stagedUploadLifetime as bt, credentialInput as c, repositoryProviderDefinitions as ct, customFieldSchema as d, resourceProvider as dt, permissionScope as et, customFieldValue as f, roleCategory as ft, defaultWorkflow as g, searchResultTypes as gt, defaultOrder as h, scopes as ht, channelRouteAllows as i, projectSchema as it, importLimits as j, workspaceSchema as jt, formatEvent as k, ticketSummarySchema as kt, credentialProjectsSchema as l, repositoryProviders as lt, defaultClaimMaxLeaseSeconds as m, roles as mt, attachmentLimit as n, priorities as nt, claimLeaseSecondsSchema as o, pushDeviceInput as ot, customFieldValueSchema as p, roleLabels as pt, okSchema as q, channelKinds as r, priorityLabels as rt, closedStates as s, remoteKinds as st, ApiError as t, permissions as tt, customFieldFilterTokens as u, repositorySecretFields as ut, deliverySecretFields as v, signInMethodLabels as vt, eventCategories as w, ticketExportFormat as wt, destinationKinds as x, stateLabels as xt, deliverySettingsSchema as y, snapshotSchema as yt, isSeededRole as z, searchAuthentication as zt };
5144
+ export { parseCustomFieldFilter as $, filterList as A, ticketImportSchema as At, isPublicOperation as B, instanceUpdateSchema as Bt, destinationKinds as C, stagedUploadLifetime as Ct, eventLabels as D, streamErrorSchema as Dt, eventCategories as E, states as Et, importSecretFields as F, workflowOrder as Ft, notificationIdsInput as G, searchSettingsSchema as Gt, memberSchema as H, photoResultSchema as Ht, invitationStatuses as I, workspaceSchema as It, notificationReadInput as J, notificationPreferencesInput as K, isHexColor as L, adminInstanceSchema as Lt, hasAdminRole as M, ticketSchema as Mt, importLimits as N, ticketSummarySchema as Nt, eventSchema as O, ticketExportFormat as Ot, importProviderDefinitions as P, uploadUrlLifetime as Pt, operations as Q, isOperation as R, authenticationPolicySchema as Rt, destinationEventKeys as S, snapshotSchema as St, errorSchema as T, stateLabels as Tt, mentionScheme as U, s3SettingsSchema as Ut, isSeededRole as V, photoInputSchema as Vt, normalizeRemote as W, searchAuthentication as Wt, okSchema as X, notificationsSchema as Y, operationPermission as Z, defaultOrder as _, roles as _t, channelRouteAllows as a, priorities as at, deliverySecretFields as b, seededPermissions as bt, claimSchema as c, publicOperations as ct, credentialProjectsSchema as d, repositoryProviderDefinitions as dt, parseOutput as et, customFieldFilterTokens as f, repositoryProviders as ft, defaultClaimMaxLeaseSeconds as g, roleLabels as gt, customFieldValueSchema as h, roleCategory as ht, channelKinds as i, permissions as it, formatEvent as j, ticketPredicates as jt, filterKey as k, ticketFilter as kt, closedStates as l, pushDeviceInput as lt, customFieldValue as m, resourceProvider as mt, attachmentLimit as n, permissionLabels as nt, checkpointSchema as o, priorityLabels as ot, customFieldSchema as p, repositorySecretFields as pt, notificationPreferencesSchema as q, attachmentSchema as r, permissionScope as rt, claimLeaseSecondsSchema as s, projectSchema as st, ApiError as t, parsePublicOutput as tt, credentialInput as u, remoteKinds as ut, defaultWorkflow as v, scopes as vt, difficulties as w, stagedUploadSchema as wt, deliverySettingsSchema as x, signInMethodLabels as xt, deletedAccountName as y, searchResultTypes as yt, isPresetColor as z, brokerSettingsSchema as zt };