@runeya/runeya 2.0.145 → 2.0.147

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 (194) hide show
  1. package/agent/index.js +36 -25
  2. package/agent/index.js.map +1 -1
  3. package/agent-manager-46C4NIQ2.js +13 -0
  4. package/chat-CMNDMXIP.js +28 -0
  5. package/{chunk-ONVAH65J.js → chunk-2NE52CL5.js} +422 -2172
  6. package/chunk-2NE52CL5.js.map +1 -0
  7. package/chunk-4D3GCBPO.js +462 -0
  8. package/chunk-4D3GCBPO.js.map +1 -0
  9. package/{chunk-NCO4OOYT.js → chunk-4LJQIQMN.js} +2 -2
  10. package/{chunk-QF56Z6LI.js → chunk-5XG3X2TM.js} +29 -1481
  11. package/chunk-5XG3X2TM.js.map +1 -0
  12. package/chunk-6IBPK6UE.js +1063 -0
  13. package/chunk-6IBPK6UE.js.map +1 -0
  14. package/chunk-7QXP4FXD.js +969 -0
  15. package/chunk-7QXP4FXD.js.map +1 -0
  16. package/chunk-DA2DZBTL.js +43 -0
  17. package/chunk-DA2DZBTL.js.map +1 -0
  18. package/chunk-DFPZ4MTI.js +1041 -0
  19. package/chunk-DFPZ4MTI.js.map +1 -0
  20. package/{chunk-Q4RPY3VW.js → chunk-JNK6H5KH.js} +185 -1014
  21. package/chunk-JNK6H5KH.js.map +1 -0
  22. package/{chunk-FE53XX4M.js → chunk-NPMSBYXC.js} +3 -3
  23. package/{chunk-KS7A5CTW.js → chunk-VPSQJOSN.js} +12 -2
  24. package/{chunk-KS7A5CTW.js.map → chunk-VPSQJOSN.js.map} +1 -1
  25. package/chunk-WDHLEO5S.js +809 -0
  26. package/chunk-WDHLEO5S.js.map +1 -0
  27. package/{companion-HBYSY2D5.js → companion-XKMNRIJB.js} +10 -5
  28. package/environment-resolver-ZDTF3MHI.js +18 -0
  29. package/external-NMKQPROK.js +387 -0
  30. package/external-NMKQPROK.js.map +1 -0
  31. package/index.js +15 -6
  32. package/index.js.map +1 -1
  33. package/{install-gate-JAEK4BDZ.js → install-gate-OJJEUU4E.js} +2 -2
  34. package/mcp-ready-TAPBAOZY.js +9 -0
  35. package/mcp-server/external.d.ts +44 -0
  36. package/mcp-server/external.js +2679 -0
  37. package/mcp-server/external.js.map +1 -0
  38. package/mcp-server/index.js +159 -39
  39. package/mcp-server/index.js.map +1 -1
  40. package/mcp-server/launch.d.ts +25 -1
  41. package/mcp-server/launch.js +22 -1
  42. package/mcp-server/launch.js.map +1 -1
  43. package/package.json +1 -1
  44. package/project-store-FLXTAD3J.js +23 -0
  45. package/project-store-FLXTAD3J.js.map +1 -0
  46. package/settings-manager-XPLBO35P.js +10 -0
  47. package/settings-manager-XPLBO35P.js.map +1 -0
  48. package/{src-IODFUZ7A.js → src-KWO3CKZU.js} +1083 -551
  49. package/src-KWO3CKZU.js.map +1 -0
  50. package/{vault-resolver-PN3TYCLJ.js → vault-resolver-E7KPKA5C.js} +2 -2
  51. package/vault-resolver-E7KPKA5C.js.map +1 -0
  52. package/web/assets/{AgentScalarChatInterface.vue-npQgDxeP.js → AgentScalarChatInterface.vue-DKBinB_l.js} +2 -2
  53. package/web/assets/AgentTerminalPage-CixU7x6O.js +1 -0
  54. package/web/assets/AiChatModals-BvGUd-QD.css +1 -0
  55. package/web/assets/AiChatModals-C-hXImkO.js +4 -0
  56. package/web/assets/AppLayout-At9wk0sx.js +1 -0
  57. package/web/assets/{AppLayout-tsw5Ow0w.css → AppLayout-BlhwflMq.css} +1 -1
  58. package/web/assets/AuditPage-BkqbHxjY.js +1 -0
  59. package/web/assets/CloudAccountPage-D5C2SCS9.js +1 -0
  60. package/web/assets/CloudMessagingPage-Bi57GGHO.js +2 -0
  61. package/web/assets/DashboardPage-dEQop9ql.js +1 -0
  62. package/web/assets/DirectoryPickerDialog-DIMSeDhw.js +1 -0
  63. package/web/assets/{EditorIcon-CypGLV6w.js → EditorIcon-W3Szca4J.js} +1 -1
  64. package/web/assets/EnvironmentForm-C5emavNh.js +1 -0
  65. package/web/assets/EnvironmentFormPage-BIoy8fsW.js +1 -0
  66. package/web/assets/EnvironmentListPage-BDYeh7XF.js +1 -0
  67. package/web/assets/{FlagChip-R_T5qYpL.js → FlagChip-DPtIBCjQ.js} +1 -1
  68. package/web/assets/KanbanBoardPage-BlCcjBwV.js +4 -0
  69. package/web/assets/KanbanListPage-DbXONxfC.js +1 -0
  70. package/web/assets/LaunchProjectPage-Cg92hHwQ.js +1 -0
  71. package/web/assets/LaunchServicesPage-CO8gOB9D.js +1 -0
  72. package/web/assets/LaunchServicesPage-h5o3zLcy.css +1 -0
  73. package/web/assets/{LaunchShell-CGNYr-do.js → LaunchShell-CDb21ihZ.js} +1 -1
  74. package/web/assets/LogViewer-DsusKk2b.js +3 -0
  75. package/web/assets/OrgKeyPrompt-CX9K7r9I.js +1 -0
  76. package/web/assets/ProcessControls-B7hWVqiR.css +1 -0
  77. package/web/assets/ProcessControls-TEZutuxq.js +1 -0
  78. package/web/assets/ProcessDetailPage-Cj23FECq.js +1 -0
  79. package/web/assets/ProcessMetrics-1P6iE2Il.js +1 -0
  80. package/web/assets/ProjectCreateDialog-D3aMWKfw.js +1 -0
  81. package/web/assets/{ProjectLocationGate-bUd_tFpI.js → ProjectLocationGate-DU0Jj8Nw.js} +1 -1
  82. package/web/assets/ProjectPublishDialog-D7sNKei3.js +1 -0
  83. package/web/assets/{RandomJobTestPage-BFNC827R.js → RandomJobTestPage-DQUMRHyf.js} +1 -1
  84. package/web/assets/{ScenarioEditorPage-vMoRt0Qn.css → ScenarioEditorPage-BSjSX4bw.css} +1 -1
  85. package/web/assets/ScenarioEditorPage-DvCdcY9m.js +14 -0
  86. package/web/assets/SendToChatDialog-BdWKjYmv.js +1 -0
  87. package/web/assets/ServiceControlsBar-B_J5m-Kc.js +1 -0
  88. package/web/assets/ServiceDetailPage-DhJcjFfY.js +2 -0
  89. package/web/assets/{ServiceDetailPage-COdaCZ6r.css → ServiceDetailPage-DnSf5cm9.css} +1 -1
  90. package/web/assets/ServiceFormPage-fp8NCHSv.js +1 -0
  91. package/web/assets/SettingsPage-NH2fQap4.css +1 -0
  92. package/web/assets/SettingsPage-s28BodWe.js +2086 -0
  93. package/web/assets/UnlockPage-BXXIPHW_.js +1 -0
  94. package/web/assets/VaultMigrationPage-BfNuKtkJ.js +1 -0
  95. package/web/assets/{VersionBadge-DBquJl76.js → VersionBadge-CW7czacg.js} +1 -1
  96. package/web/assets/{WikiPage-GtwpJPqf.js → WikiPage-BiwpCM3E.js} +1 -1
  97. package/web/assets/WikiView-CMch_06Z.js +1 -0
  98. package/web/assets/{add-scalar-classes-DhSSqZ5l.js → add-scalar-classes-Ckanjq1h.js} +14 -14
  99. package/web/assets/{agent.store-Co44dMZf.js → agent.store-piokQ9Dt.js} +1 -1
  100. package/web/assets/{auth-Bk1GP5sB.js → auth-BT_jf-5e.js} +2 -2
  101. package/web/assets/cloud-chat.store-CDKh4tIy.js +1 -0
  102. package/web/assets/{cloud-client-0zD_NI6q.js → cloud-client-CzPdZLDq.js} +2 -2
  103. package/web/assets/{cloud-session-Cjw54Y--.js → cloud-session-BYfn6YM4.js} +2 -2
  104. package/web/assets/floating-ui.dom-DF3oyfHd.js +1 -0
  105. package/web/assets/i18n-D0FT7rCj.js +1 -0
  106. package/web/assets/index-3LTzgeU-.css +1 -0
  107. package/web/assets/index-Ci2CNnit.js +309 -0
  108. package/web/assets/modal-iRaP1izE.js +5 -0
  109. package/web/assets/notification.store-CvvuTBVz.js +1 -0
  110. package/web/assets/{project-launch.store-B5nhdGdh.js → project-launch.store-CTmbl8UY.js} +1 -1
  111. package/web/assets/{project.store-Bm2JWNbd.js → project.store-CNC6_4r4.js} +1 -1
  112. package/web/assets/runtime-dom.esm-bundler-CyEVRK6B.js +1 -0
  113. package/web/assets/scenario.store-BIFQPXgl.js +7 -0
  114. package/web/assets/{service.store-XS50iGKo.js → service.store-B40U4QzI.js} +1 -1
  115. package/web/assets/settings-scope-CyWMCjRY.js +1 -0
  116. package/web/assets/settings.store-Dw6bAoWE.js +1 -0
  117. package/web/assets/{src-Cgix2VOi.js → src-CrNi4Pjg.js} +3 -3
  118. package/web/assets/src-DIBHat7I.js +3 -0
  119. package/web/assets/src-DtCW_XCh.js +1 -0
  120. package/web/assets/{src-BDZKZr9n.css → src-g1DRo8xZ.css} +1 -1
  121. package/web/assets/{trpc-CfuFkKgp.js → trpc-BAZGRZBD.js} +3 -3
  122. package/web/assets/{useCloudOrg-J8DAnocE.js → useCloudOrg-EnXFBwt1.js} +1 -1
  123. package/web/assets/useEnvVarAutocomplete-BxJh2Lw0.js +1 -0
  124. package/web/assets/useProjectCatalog-BrvUGv9A.js +1 -0
  125. package/web/assets/{useProjectCloudOrg-BnlKts7x.js → useProjectCloudOrg-Czxlg00k.js} +1 -1
  126. package/web/assets/{useServiceUrlHost-hKbKtPMu.js → useServiceUrlHost-Szz9y3rh.js} +1 -1
  127. package/web/assets/vue-i18n-o7yraQpR.js +3 -0
  128. package/web/index.html +21 -21
  129. package/agent-manager-THV3LJOF.js +0 -11
  130. package/chat-XYCHMTFE.js +0 -23
  131. package/chunk-ONVAH65J.js.map +0 -1
  132. package/chunk-Q4RPY3VW.js.map +0 -1
  133. package/chunk-QF56Z6LI.js.map +0 -1
  134. package/settings-manager-QVQRJO6V.js +0 -10
  135. package/src-IODFUZ7A.js.map +0 -1
  136. package/web/assets/AgentTerminalPage-BTH3yCS2.js +0 -1
  137. package/web/assets/AiChatModals-EfRuvId7.css +0 -1
  138. package/web/assets/AiChatModals-It9jnB26.js +0 -4
  139. package/web/assets/AppLayout-BTg2CADo.js +0 -1
  140. package/web/assets/AuditPage-2PKKGLZs.js +0 -1
  141. package/web/assets/CloudAccountPage-CHL61gLe.js +0 -1
  142. package/web/assets/CloudMessagingPage-cgKnABPH.js +0 -2
  143. package/web/assets/DashboardPage-CRntTMKY.js +0 -1
  144. package/web/assets/DirectoryPickerDialog-DCyhT3-W.js +0 -1
  145. package/web/assets/EnvironmentForm-C96PFWRE.js +0 -1
  146. package/web/assets/EnvironmentFormPage-ChorAd63.js +0 -1
  147. package/web/assets/EnvironmentListPage-Dw9ziaI8.js +0 -1
  148. package/web/assets/KanbanBoardPage-BZHbEj-q.js +0 -4
  149. package/web/assets/KanbanListPage-B0jlfKqs.js +0 -1
  150. package/web/assets/LaunchProjectPage-BVs1JwbV.js +0 -1
  151. package/web/assets/LaunchServicesPage-DoLDPqQq.css +0 -1
  152. package/web/assets/LaunchServicesPage-PBYJly9N.js +0 -1
  153. package/web/assets/LogViewer-BDObx29F.js +0 -3
  154. package/web/assets/OrgKeyPrompt-BbcwiwGw.js +0 -1
  155. package/web/assets/ProcessControls-BlpnVylq.css +0 -1
  156. package/web/assets/ProcessControls-DY0HTKde.js +0 -1
  157. package/web/assets/ProcessDetailPage-BZ9kIQqn.js +0 -1
  158. package/web/assets/ProcessMetrics-Crlfkelj.js +0 -1
  159. package/web/assets/ProjectCreateDialog-C1wVDPcK.js +0 -1
  160. package/web/assets/ProjectPublishDialog-APUS1WEN.js +0 -1
  161. package/web/assets/ScenarioEditorPage-DFVsMVu5.js +0 -14
  162. package/web/assets/SendToChatDialog-QHIkxNio.js +0 -1
  163. package/web/assets/ServiceControlsBar-_UQXb6g0.js +0 -1
  164. package/web/assets/ServiceDetailPage-DGIXgHwR.js +0 -2
  165. package/web/assets/ServiceFormPage-RliAfS3_.js +0 -1
  166. package/web/assets/SettingsPage-BSSHxxjM.js +0 -2086
  167. package/web/assets/SettingsPage-W8YszhUL.css +0 -1
  168. package/web/assets/UnlockPage-17EgfpJs.js +0 -1
  169. package/web/assets/VaultMigrationPage-DeMxpXXJ.js +0 -1
  170. package/web/assets/WikiView-BqaqxMxW.js +0 -1
  171. package/web/assets/cloud-chat.store-CadB14dZ.js +0 -1
  172. package/web/assets/editor.store-DmM6ZlTm.js +0 -1
  173. package/web/assets/floating-ui.dom-DWhsZyqT.js +0 -1
  174. package/web/assets/i18n-8FjeJaKW.js +0 -1
  175. package/web/assets/index-B9DJf2V0.js +0 -309
  176. package/web/assets/index-teBqdnuv.css +0 -1
  177. package/web/assets/modal-UK8xRSnh.js +0 -5
  178. package/web/assets/scenario.store-88w6P6s2.js +0 -7
  179. package/web/assets/settings-scope-CKPq86HZ.js +0 -1
  180. package/web/assets/settings.store-BCJ0LJQA.js +0 -1
  181. package/web/assets/src--GO-VSJ0.js +0 -5
  182. package/web/assets/src-DBipSD56.js +0 -1
  183. package/web/assets/useEnvVarAutocomplete-C6zOXYqi.js +0 -1
  184. package/web/assets/useProjectCatalog-BB5IR6o8.js +0 -1
  185. package/web/assets/vue-draggable-plus-BHsveB8w.css +0 -1
  186. package/web/assets/vue-draggable-plus-D5ni4jMk.js +0 -1
  187. /package/{agent-manager-THV3LJOF.js.map → agent-manager-46C4NIQ2.js.map} +0 -0
  188. /package/{chat-XYCHMTFE.js.map → chat-CMNDMXIP.js.map} +0 -0
  189. /package/{chunk-NCO4OOYT.js.map → chunk-4LJQIQMN.js.map} +0 -0
  190. /package/{chunk-FE53XX4M.js.map → chunk-NPMSBYXC.js.map} +0 -0
  191. /package/{companion-HBYSY2D5.js.map → companion-XKMNRIJB.js.map} +0 -0
  192. /package/{settings-manager-QVQRJO6V.js.map → environment-resolver-ZDTF3MHI.js.map} +0 -0
  193. /package/{install-gate-JAEK4BDZ.js.map → install-gate-OJJEUU4E.js.map} +0 -0
  194. /package/{vault-resolver-PN3TYCLJ.js.map → mcp-ready-TAPBAOZY.js.map} +0 -0
@@ -0,0 +1,2679 @@
1
+ // src/external.ts
2
+ import { homedir } from "os";
3
+ import { join as join2 } from "path";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // ../ai-capabilities/dist/index.js
7
+ import { z as z23 } from "zod";
8
+
9
+ // ../shared/dist/index.js
10
+ import { z } from "zod";
11
+ import { z as z2 } from "zod";
12
+ import { z as z3 } from "zod";
13
+ import { z as z7 } from "zod";
14
+ import { z as z5 } from "zod";
15
+ import { z as z4 } from "zod";
16
+ import { z as z6 } from "zod";
17
+ import { z as z8 } from "zod";
18
+ import { z as z9 } from "zod";
19
+ import { z as z10 } from "zod";
20
+ import { z as z11 } from "zod";
21
+ import { z as z12 } from "zod";
22
+ import { z as z13 } from "zod";
23
+ import { z as z14 } from "zod";
24
+ import { z as z15 } from "zod";
25
+ import { z as z16 } from "zod";
26
+ import { z as z17 } from "zod";
27
+ import { z as z18 } from "zod";
28
+ import { z as z19 } from "zod";
29
+ import { z as z20 } from "zod";
30
+ import { z as z21 } from "zod";
31
+ import { z as z22 } from "zod";
32
+ import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from "crypto";
33
+ import vm from "vm";
34
+ import * as Y from "yjs";
35
+ var UserSchema = z.object({
36
+ id: z.string().max(128),
37
+ email: z.string().email().max(320),
38
+ name: z.string().min(1).max(500),
39
+ avatar: z.string().url().max(2048).nullable().optional()
40
+ });
41
+ var OrgSchema = z2.object({
42
+ id: z2.string().max(128),
43
+ name: z2.string().min(1).max(100),
44
+ slug: z2.string().min(1).max(100).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/),
45
+ ownerId: z2.string().max(128)
46
+ });
47
+ var OrgMemberSchema = z2.object({
48
+ id: z2.string().max(128),
49
+ orgId: z2.string().max(128),
50
+ userId: z2.string().max(128),
51
+ globalRole: z2.enum(["owner", "admin", "member"]),
52
+ invitedAt: z2.coerce.date(),
53
+ joinedAt: z2.coerce.date().nullable().optional()
54
+ });
55
+ var ProjectSchema = z3.object({
56
+ id: z3.string().max(128),
57
+ orgId: z3.string().max(128).nullable().optional(),
58
+ name: z3.string().min(1).max(200),
59
+ description: z3.string().max(1e4).optional().default(""),
60
+ serviceIds: z3.array(z3.string().max(128)).max(1e4).default([]),
61
+ // Environment support
62
+ // NOTE: activeEnvironmentId is runtime-only (not persisted to JSON)
63
+ // It's initialized to 'local' by default at load time
64
+ activeEnvironmentId: z3.string().max(128).optional(),
65
+ environmentIds: z3.array(z3.string().max(128)).max(100).default([]),
66
+ // Ordre d'affichage des groupes de services dans la sidebar (noms de groupes).
67
+ // Les groupes absents de cette liste sont affichés après, par ordre alphabétique.
68
+ groupOrder: z3.array(z3.string().max(200)).max(100).default([])
69
+ });
70
+ var CreateProjectSchema = ProjectSchema.omit({ id: true }).partial().required({ name: true });
71
+ var UpdateProjectSchema = ProjectSchema.partial().required({ id: true });
72
+ var PositionSchema = z4.object({
73
+ x: z4.number(),
74
+ y: z4.number()
75
+ });
76
+ var ScenarioGroupSchema = z4.object({
77
+ id: z4.string().min(1).max(128),
78
+ name: z4.string().min(1).max(200),
79
+ description: z4.string().max(1e3).optional(),
80
+ color: z4.string().max(32).optional(),
81
+ position: PositionSchema.default({ x: 0, y: 0 }),
82
+ width: z4.number().default(300),
83
+ height: z4.number().default(200)
84
+ });
85
+ var ScenarioPresetRoleSchema = z4.object({
86
+ id: z4.string().min(1).max(64),
87
+ name: z4.string().min(1).max(200),
88
+ systemPrompt: z4.string().max(1e4).default(""),
89
+ prompt: z4.string().max(1e4).default("")
90
+ });
91
+ var ScenarioPresetInitialDataSchema = z4.object({
92
+ leaderId: z4.string().min(1).max(128),
93
+ groups: z4.array(z4.object({
94
+ id: z4.string().min(1).max(128),
95
+ name: z4.string().min(1).max(200),
96
+ description: z4.string().max(1e3).optional(),
97
+ color: z4.string().max(32).optional(),
98
+ position: z4.object({ x: z4.number(), y: z4.number() }).default({ x: 0, y: 0 }),
99
+ width: z4.number().default(300),
100
+ height: z4.number().default(200)
101
+ })).max(100).default([]),
102
+ nodes: z4.array(z4.object({
103
+ id: z4.string().min(1).max(128),
104
+ type: z4.enum(["ai", "ask"]).default("ai"),
105
+ parentIds: z4.array(z4.string().max(128)).default([]),
106
+ groupId: z4.string().max(128).nullable().default(null),
107
+ label: z4.string().max(240).default(""),
108
+ role: z4.string().max(64).default("other"),
109
+ prompt: z4.string().max(5e4).default(""),
110
+ question: z4.string().max(2e3).default(""),
111
+ autoGenerated: z4.boolean().default(true),
112
+ canAddNode: z4.boolean().default(false),
113
+ aiCli: z4.enum(["claude-code", "codex"]).default("claude-code"),
114
+ model: z4.string().max(128).default("sonnet"),
115
+ position: z4.object({ x: z4.number(), y: z4.number() }).default({ x: 0, y: 0 })
116
+ })).min(1).max(500)
117
+ });
118
+ var LocalizedStringSchema = z4.record(z4.string(), z4.string().max(100));
119
+ var ScenarioPresetLabelsSchema = z4.object({
120
+ group: LocalizedStringSchema.optional(),
121
+ node: LocalizedStringSchema.optional(),
122
+ role: LocalizedStringSchema.optional(),
123
+ leader: LocalizedStringSchema.optional()
124
+ });
125
+ var ScenarioPresetSchema = z4.object({
126
+ id: z4.string().min(1).max(128),
127
+ name: z4.string().min(1).max(200),
128
+ icon: z4.string().max(64).optional(),
129
+ defaultTraversalMode: z4.enum(["hierarchical", "topological"]).optional(),
130
+ canChangeTraversalMode: z4.boolean().default(true),
131
+ roles: z4.array(ScenarioPresetRoleSchema).min(1).max(100),
132
+ initialData: ScenarioPresetInitialDataSchema.optional(),
133
+ labels: ScenarioPresetLabelsSchema.optional()
134
+ });
135
+ var ScenarioNodeSchema = z4.object({
136
+ id: z4.string().min(1).max(128),
137
+ type: z4.enum(["ai", "ask"]).default("ai"),
138
+ parentIds: z4.array(z4.string().max(128)).default([]),
139
+ groupId: z4.string().max(128).nullable().default(null),
140
+ label: z4.string().max(240).default(""),
141
+ role: z4.string().max(64).default("other"),
142
+ prompt: z4.string().max(5e4).default(""),
143
+ question: z4.string().max(2e3).default(""),
144
+ autoGenerated: z4.boolean().default(true),
145
+ canAddNode: z4.boolean().default(false),
146
+ aiCli: z4.enum(["claude-code", "codex"]).default("claude-code"),
147
+ model: z4.string().max(128).default("sonnet"),
148
+ position: PositionSchema.default({ x: 0, y: 0 }),
149
+ injectParentOutputs: z4.boolean().default(false),
150
+ injectedAncestorIds: z4.array(z4.string().max(128)).default([])
151
+ });
152
+ var ScenarioSchema = z4.object({
153
+ id: z4.string().min(1).max(128),
154
+ name: z4.string().min(1).max(200),
155
+ icon: z4.string().max(64).optional(),
156
+ description: z4.string().max(2e3).optional(),
157
+ leaderId: z4.string().min(1).max(128),
158
+ groups: z4.array(ScenarioGroupSchema).max(100).default([]),
159
+ nodes: z4.array(ScenarioNodeSchema).min(1).max(500),
160
+ traversalMode: z4.enum(["hierarchical", "topological"]).default("hierarchical"),
161
+ presetId: z4.string().max(128).optional(),
162
+ isPublished: z4.boolean().default(false),
163
+ createdAt: z4.number(),
164
+ updatedAt: z4.number()
165
+ });
166
+ var AI_PROVIDERS = ["anthropic", "openai", "mistral", "groq", "ollama"];
167
+ var AIProviderSchema = z5.object({
168
+ id: z5.string().min(1).max(128),
169
+ name: z5.string().min(1).max(200),
170
+ provider: z5.enum(AI_PROVIDERS),
171
+ encryptedApiKey: z5.string().max(2e3).default(""),
172
+ model: z5.string().max(200).optional(),
173
+ active: z5.boolean().default(false),
174
+ createdAt: z5.coerce.date(),
175
+ updatedAt: z5.coerce.date()
176
+ });
177
+ var JSLogParserSchema = z5.object({
178
+ id: z5.string().min(1).max(128),
179
+ name: z5.string().min(1).max(200),
180
+ description: z5.string().max(2e3).optional(),
181
+ code: z5.string().min(1).max(5e4).refine(
182
+ (s) => !s.includes("require(") && !s.includes("import ") && !s.includes("eval("),
183
+ { message: "Code must not contain require, import, or eval patterns" }
184
+ ),
185
+ enabled: z5.boolean().default(true),
186
+ builtin: z5.boolean().optional(),
187
+ createdAt: z5.coerce.date(),
188
+ updatedAt: z5.coerce.date()
189
+ });
190
+ var NotificationPreferencesSchema = z5.object({
191
+ scenario_waiting_input: z5.boolean().default(true),
192
+ ai_conversation_complete: z5.boolean().default(true),
193
+ ai_conversation_error: z5.boolean().default(true),
194
+ service_error_log: z5.boolean().default(true),
195
+ service_status_changed: z5.boolean().default(true),
196
+ service_health_changed: z5.boolean().default(true),
197
+ /** Service IDs excluded from error-log notifications */
198
+ service_error_log_excluded: z5.array(z5.string().max(100)).max(500).default([]),
199
+ /** Service IDs excluded from status-change notifications */
200
+ service_status_changed_excluded: z5.array(z5.string().max(100)).max(500).default([]),
201
+ /** Service IDs excluded from health-change notifications */
202
+ service_health_changed_excluded: z5.array(z5.string().max(100)).max(500).default([])
203
+ });
204
+ var CompanionSettingsSchema = z5.object({
205
+ /** Activer le compagnon d'office sur les conversations créées ensuite. */
206
+ enabledByDefault: z5.boolean().default(false),
207
+ /**
208
+ * Spécificités ajoutées aux consignes Runeya.
209
+ *
210
+ * Vide — l'absence — signifie « aucune spécificité ». On ne recopie pas le
211
+ * texte par défaut dans le réglage : les consignes Runeya restent dans le
212
+ * code, pour profiter de leurs corrections.
213
+ *
214
+ * Rejeté au-delà de la borne plutôt que tronqué (§1) : un prompt coupé en
215
+ * plein milieu produirait des relectures incohérentes sans rien signaler.
216
+ */
217
+ prompt: z5.string().max(2e4).optional(),
218
+ /** Runner du compagnon des conversations créées ensuite. Absent = celui de la conversation. */
219
+ runner: z5.enum(["claude-code", "codex"]).optional(),
220
+ /** Modèle du compagnon des conversations créées ensuite. Absent = défaut du runner. */
221
+ model: z5.string().max(200).optional(),
222
+ /** Effort du compagnon des conversations créées ensuite. Absent = effort par défaut. */
223
+ effort: z5.enum(["low", "normal", "high"]).optional()
224
+ });
225
+ var SettingsSchema = z5.object({
226
+ mode: z5.enum(["local-simple", "cloud"]),
227
+ cloudUrl: z5.string().url().max(2048).nullable().optional(),
228
+ logParsers: z5.array(JSLogParserSchema).max(100).default([]),
229
+ scenarios: z5.array(ScenarioSchema).max(100).default([]),
230
+ /** Absolute path to the folder where the global wiki/docs markdown files live. Optional. */
231
+ globalDocsPath: z5.string().max(4096).optional(),
232
+ /**
233
+ * Optionnel plutôt que défauté : un réglage absent est un réglage jamais
234
+ * touché, et le rendre obligatoire dans le type forcerait chaque appelant qui
235
+ * construit un objet de réglages à inventer une valeur.
236
+ */
237
+ companion: CompanionSettingsSchema.optional()
238
+ });
239
+ var TraefikConfigSchema = z5.object({
240
+ enabled: z5.boolean().default(false),
241
+ acmeEmail: z5.string().email().max(256).optional(),
242
+ wildcardDomain: z5.string().max(253).optional(),
243
+ httpPort: z5.number().int().min(1).max(65535).default(80),
244
+ httpsPort: z5.number().int().min(1).max(65535).default(443),
245
+ dnsProvider: z5.string().max(64).optional(),
246
+ dnsCredentials: z5.record(z5.string().max(64), z5.string().max(2048)).default({}),
247
+ acmeStaging: z5.boolean().default(false),
248
+ encryptedCert: z5.string().max(65536).optional(),
249
+ encryptedCertKey: z5.string().max(65536).optional()
250
+ });
251
+ var SettingsPrivateSchema = z5.object({
252
+ aiProviders: z5.array(AIProviderSchema).max(50).default([]),
253
+ scenarios: z5.array(ScenarioSchema).max(100).default([]),
254
+ notifications: NotificationPreferencesSchema.default(() => ({
255
+ scenario_waiting_input: true,
256
+ ai_conversation_complete: true,
257
+ ai_conversation_error: true,
258
+ service_error_log: true,
259
+ service_status_changed: true,
260
+ service_health_changed: true,
261
+ service_error_log_excluded: [],
262
+ service_status_changed_excluded: [],
263
+ service_health_changed_excluded: []
264
+ })),
265
+ traefikConfig: TraefikConfigSchema.optional()
266
+ });
267
+ var SchemaVersionSchema = z5.object({
268
+ version: z5.number().int().positive(),
269
+ migratedAt: z5.coerce.date()
270
+ });
271
+ var safePath = z6.string().min(1).max(4096).refine(
272
+ (p) => !p.includes("\0"),
273
+ { message: "path must not contain null bytes" }
274
+ ).refine(
275
+ (p) => !/(^|[/\\])\.\.([/\\]|$)/.test(p),
276
+ { message: "path must not contain path-traversal sequences (..)" }
277
+ );
278
+ var ConsoleLogSourceSchema = z6.object({
279
+ type: z6.literal("console")
280
+ });
281
+ var FileLogSourceSchema = z6.object({
282
+ type: z6.literal("file"),
283
+ path: safePath,
284
+ encoding: z6.string().max(32).default("utf-8"),
285
+ startFromEnd: z6.boolean().default(true),
286
+ createIfNotExists: z6.boolean().default(false)
287
+ });
288
+ var MongoLogSourceSchema = z6.object({
289
+ type: z6.literal("mongo"),
290
+ /** @sensitive — stored encrypted (AES-256-GCM) on disk */
291
+ connectionString: z6.string().min(1).max(2048),
292
+ database: z6.string().min(1).max(256),
293
+ collection: z6.string().min(1).max(256),
294
+ filter: z6.record(z6.string(), z6.unknown()).optional(),
295
+ timestampField: z6.string().max(128).default("timestamp"),
296
+ messageField: z6.string().max(128).default("message"),
297
+ levelField: z6.string().max(128).optional()
298
+ });
299
+ var MySQLLogSourceSchema = z6.object({
300
+ type: z6.literal("mysql"),
301
+ host: z6.string().min(1).max(253),
302
+ port: z6.number().int().min(1).max(65535).default(3306),
303
+ user: z6.string().min(1).max(256),
304
+ /** @sensitive — stored encrypted (AES-256-GCM) on disk */
305
+ password: z6.string().max(2048),
306
+ database: z6.string().min(1).max(256),
307
+ table: z6.string().min(1).max(256),
308
+ query: z6.string().max(8192).optional(),
309
+ timestampColumn: z6.string().max(128).default("created_at"),
310
+ messageColumn: z6.string().max(128).default("message"),
311
+ levelColumn: z6.string().max(128).optional(),
312
+ pollIntervalMs: z6.number().int().min(500).max(6e4).default(2e3)
313
+ });
314
+ var LogSourceConfigSchema = z6.discriminatedUnion("type", [
315
+ ConsoleLogSourceSchema,
316
+ FileLogSourceSchema,
317
+ MongoLogSourceSchema,
318
+ MySQLLogSourceSchema
319
+ ]);
320
+ var HealthCheckSchema = z7.object({
321
+ type: z7.enum(["http", "command"]).default("http"),
322
+ // HTTP-specific (required when type=http)
323
+ path: z7.string().min(1).max(500).startsWith("/").optional(),
324
+ method: z7.enum(["GET", "HEAD", "POST", "PUT"]).optional().default("GET"),
325
+ expectedStatus: z7.number().int().min(100).max(599).optional().default(200),
326
+ // Command-specific (required when type=command)
327
+ command: z7.string().min(1).max(4096).optional(),
328
+ // Shared
329
+ interval: z7.number().int().positive().default(1e3),
330
+ timeout: z7.number().int().positive().default(1e3),
331
+ retries: z7.number().int().nonnegative().default(3),
332
+ initialDelay: z7.number().int().nonnegative().optional().default(0)
333
+ }).refine(
334
+ (data) => data.type === "http" ? !!data.path : !!data.command,
335
+ { message: "HTTP type requires path; command type requires command" }
336
+ );
337
+ var BootstrapCommandSchema = z7.object({
338
+ id: z7.string().min(1).max(64),
339
+ label: z7.string().max(200).default(""),
340
+ entrypoint: z7.string().max(4096).optional(),
341
+ command: z7.string().max(4096).default(""),
342
+ user: z7.string().max(256).optional()
343
+ });
344
+ var DockerConfigSchema = z7.object({
345
+ // Image mode (pull from registry) — mutually exclusive with build mode in UI
346
+ image: z7.string().min(1).max(1024).optional(),
347
+ // Build mode — Dockerfile stored inline in services.json
348
+ dockerfile: z7.string().max(65536).optional(),
349
+ buildContext: z7.string().max(4096).optional(),
350
+ buildArgs: z7.record(z7.string().max(256), z7.string().max(4096)).default({}).refine(
351
+ (obj) => Object.keys(obj).length <= 50,
352
+ { message: "Max 50 build args" }
353
+ ),
354
+ targetStage: z7.string().max(256).optional(),
355
+ // Runtime config
356
+ networkMode: z7.enum(["host", "bridge"]).default("host"),
357
+ ports: z7.array(z7.string().max(64)).max(100).default([]),
358
+ volumes: z7.array(z7.string().max(1024)).max(100).default([]),
359
+ mountRootpath: z7.boolean().default(false),
360
+ entrypoint: z7.string().max(4096).optional(),
361
+ command: z7.string().max(4096).optional(),
362
+ user: z7.string().max(256).optional(),
363
+ useNativeCommand: z7.boolean().default(false),
364
+ nativeCommandMode: z7.enum(["shared", "isolated"]).default("isolated"),
365
+ nativeShell: z7.string().max(256).default("sh"),
366
+ bootstrapCommands: z7.array(BootstrapCommandSchema).max(20).default([])
367
+ });
368
+ var RestartStrategySchema = z7.enum(["none", "on-failure", "always", "unless-stopped"]).default("none");
369
+ var ServiceShortcutSchema = z7.object({
370
+ id: z7.string().min(1).max(64),
371
+ label: z7.string().max(200).default(""),
372
+ command: z7.string().max(4096).default(""),
373
+ cwd: z7.string().max(4096).optional()
374
+ });
375
+ var ServiceCommandSchema = z7.object({
376
+ id: z7.string().min(1).max(64),
377
+ label: z7.string().max(200).default(""),
378
+ command: z7.string().max(4096).default(""),
379
+ cwd: z7.string().max(4096).optional(),
380
+ shell: z7.boolean().optional().default(true)
381
+ });
382
+ var VariableSlotSchema = z7.object({
383
+ value: z7.string().max(8192).default(""),
384
+ pre: z7.string().max(8192).default(""),
385
+ post: z7.string().max(8192).default(""),
386
+ isSecret: z7.boolean().default(false),
387
+ // Deux décisions distinctes, et il ne faut pas les confondre :
388
+ //
389
+ // - `shareWithAi` est une permission de **lecture** : une IA (le chat de
390
+ // Runeya, un agent par le serveur MCP) peut voir la valeur de ce secret.
391
+ // Une variable publique est lisible de toute façon, le drapeau ne la
392
+ // concerne pas.
393
+ // - `injectIntoAi` est une **exécution** : la variable est posée dans
394
+ // l'environnement du process des agents (Claude Code, Codex, …) lancés
395
+ // depuis Runeya pendant que cet environnement est actif.
396
+ //
397
+ // Les deux sont opt-in : sans rien cocher, aucun secret ne part vers une IA.
398
+ // Injecter implique lire (ce que le process reçoit, l'agent le voit), donc
399
+ // la lecture accepte l'un OU l'autre : voir `isSharedWithAi`, qui est le seul
400
+ // endroit où cette règle s'écrit.
401
+ shareWithAi: z7.boolean().default(false),
402
+ injectIntoAi: z7.boolean().default(false),
403
+ // Une variable obligatoire doit avoir une valeur finale non vide avant qu'un
404
+ // service de cet environnement puisse démarrer. Le démarrage est refusé
405
+ // sinon, avec la liste des manquantes — l'assistant est le moyen de remplir.
406
+ required: z7.boolean().default(false),
407
+ // Une variable « seulement surchargeable » n'a jamais de valeur partagée :
408
+ // `value` reste vide dans le document synchronisé, et chaque poste DOIT
409
+ // fournir sa propre surcharge machine-locale (chemin local, jeton personnel).
410
+ // `pre` et `post` restent partagés — ils décrivent la forme, pas le secret.
411
+ overrideOnly: z7.boolean().default(false),
412
+ // À quoi sert cette variable, pour quelqu'un qui doit la remplir. Affichée
413
+ // par l'assistant de complétion et dans l'éditeur.
414
+ //
415
+ // Du **markdown**, pas du texte brut : une description gagne à pouvoir
416
+ // pointer vers la console qui délivre le jeton. Écrite et relue par le même
417
+ // éditeur Tiptap (`packages/editor-ui`), jamais injectée en HTML — Tiptap
418
+ // analyse le markdown dans son propre schéma, si bien qu'une description
419
+ // arrivée d'un autre poste par la synchronisation ne peut rien exécuter.
420
+ description: z7.string().max(2e3).default("")
421
+ }).refine(
422
+ (slot) => !slot.overrideOnly || slot.value === "",
423
+ { message: "An override-only variable cannot carry a shared value", path: ["value"] }
424
+ );
425
+ var MissingVariableReasonSchema = z7.enum(["required", "overrideOnly"]);
426
+ var MissingVariableSchema = z7.object({
427
+ variableId: z7.string().max(128),
428
+ key: z7.string().max(256),
429
+ description: z7.string().max(2e3),
430
+ reason: MissingVariableReasonSchema,
431
+ isSecret: z7.boolean(),
432
+ environmentId: z7.string().max(128),
433
+ environmentName: z7.string().max(200)
434
+ });
435
+ var VariableSchema = z7.object({
436
+ key: z7.string().min(1).max(256),
437
+ value: VariableSlotSchema
438
+ });
439
+ var VariablesRecordSchema = z7.record(
440
+ z7.string().max(128),
441
+ VariableSchema
442
+ ).refine(
443
+ (obj) => Object.keys(obj).length <= 200,
444
+ { message: "Max 200 variables" }
445
+ );
446
+ var VariableSlotsRecordSchema = z7.record(
447
+ z7.string().max(256),
448
+ VariableSlotSchema
449
+ ).refine(
450
+ (obj) => Object.keys(obj).length <= 200,
451
+ { message: "Max 200 variable slots" }
452
+ );
453
+ var ServiceConfigSchema = z7.object({
454
+ id: z7.string().max(128),
455
+ orgId: z7.string().max(128).nullable().optional(),
456
+ agentId: z7.string().max(128).optional(),
457
+ // Optional: if absent, inherited from active environment's agentId
458
+ name: z7.string().min(1).max(200),
459
+ commands: z7.array(ServiceCommandSchema).max(50).optional().default([]),
460
+ cwd: z7.string().max(4096).optional(),
461
+ ports: z7.array(z7.number().int().positive().max(65535)).max(20).optional().default([]),
462
+ // Metadata fields (migration from old Runeya)
463
+ groups: z7.array(z7.string().max(200)).max(20).optional().default([]),
464
+ description: z7.string().max(2e3).optional(),
465
+ /** Path (relative to service.cwd or absolute) where the service's docs/wiki markdown files live. Optional — defaults to service.cwd. */
466
+ docsPath: z7.string().max(4096).optional(),
467
+ url: z7.string().max(2048).optional(),
468
+ localUrls: z7.array(z7.object({
469
+ url: z7.string().max(253).regex(/^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/),
470
+ port: z7.number().int().min(1).max(65535)
471
+ })).max(20).optional().default([]),
472
+ openapiUrl: z7.string().max(2048).optional(),
473
+ git: z7.object({
474
+ remote: z7.string().max(2048).optional(),
475
+ home: z7.string().max(2048).optional()
476
+ }).optional(),
477
+ meta: z7.record(z7.string().max(256), z7.string().max(2e3)).optional().refine((obj) => !obj || Object.keys(obj).length <= 100, { message: "Too many meta keys (max 100)" }),
478
+ // Transient env dict (resolved by server before deployment, used directly by agent runner — NOT persisted)
479
+ env: z7.record(z7.string().max(256), z7.string().max(8192)).default({}).refine(
480
+ (obj) => Object.keys(obj).length <= 200,
481
+ { message: "Too many env vars (max 200)" }
482
+ ),
483
+ envSecrets: z7.array(z7.string().max(256)).max(200).default([]),
484
+ // Transient: resolved JS log parsers (from parserIds, resolved by server before deployment — NOT persisted)
485
+ resolvedLogParsers: z7.array(JSLogParserSchema).default([]).optional(),
486
+ // Transient: resolved working directory (cwd with env vars expanded — NOT persisted)
487
+ resolvedCwd: z7.string().optional(),
488
+ runner: z7.enum(["native", "docker"]).optional().default("native"),
489
+ dockerConfig: DockerConfigSchema.optional(),
490
+ healthCheck: HealthCheckSchema.optional(),
491
+ parserIds: z7.array(z7.string().max(128)).max(50).default([]),
492
+ shortcuts: z7.array(ServiceShortcutSchema).max(50).optional().default([]),
493
+ autoRestart: z7.boolean().optional().default(false),
494
+ clearLogsOnStart: z7.boolean().optional().default(false),
495
+ restartStrategy: RestartStrategySchema.optional(),
496
+ maxRestarts: z7.number().int().nonnegative().optional(),
497
+ restartBackoffMs: z7.number().int().positive().optional().default(1e3),
498
+ // Log sources additionnelles (au-delà du stdout/stderr console par défaut)
499
+ logSources: z7.array(LogSourceConfigSchema).max(20).optional().default([])
500
+ });
501
+ var CreateServiceSchema = ServiceConfigSchema.omit({ id: true }).partial().required({ name: true });
502
+ var UpdateServiceSchema = ServiceConfigSchema.partial().required({ id: true }).extend({
503
+ agentId: z7.string().max(128).nullable().optional(),
504
+ // null = clear (inherit from env), undefined = no change
505
+ cwd: z7.string().max(4096).nullable().optional(),
506
+ git: z7.object({ remote: z7.string().max(2048).optional(), home: z7.string().max(2048).optional() }).nullable().optional(),
507
+ meta: z7.record(z7.string().max(256), z7.string().max(2e3)).nullable().optional().refine((obj) => !obj || Object.keys(obj).length <= 100, { message: "Too many meta keys (max 100)" }),
508
+ healthCheck: HealthCheckSchema.nullable().optional()
509
+ });
510
+ var ProcessStateSchema = z8.enum(["starting", "running", "stopped", "errored"]);
511
+ var HealthStatusSchema = z8.enum(["healthy", "unhealthy", "unknown"]);
512
+ var ProcessStatusSchema = z8.object({
513
+ id: z8.string().max(128),
514
+ serviceId: z8.string().max(128),
515
+ agentId: z8.string().max(128),
516
+ state: ProcessStateSchema,
517
+ pid: z8.number().int().nullable().optional(),
518
+ pids: z8.array(z8.number().int()).optional().default([]),
519
+ exitCode: z8.number().int().nullable().optional(),
520
+ startedAt: z8.coerce.date().nullable().optional(),
521
+ restartCount: z8.number().int().nonnegative().default(0),
522
+ uptime: z8.number().nonnegative().default(0),
523
+ healthStatus: HealthStatusSchema.default("unknown"),
524
+ /**
525
+ * Le répertoire où le processus s'exécute VRAIMENT, absolutisé par l'agent.
526
+ *
527
+ * Rapporté par l'agent plutôt que déduit de la config : entre le `cwd` de la
528
+ * commande, celui du service, l'ancrage sur le projet et les variables que
529
+ * l'agent étend lui-même, la valeur affichée n'aurait rien prouvé. Ce qui
530
+ * intéresse quand un service tourne contre le mauvais dépôt, c'est le chemin
531
+ * final — y compris quand il retombe sur le répertoire de l'agent.
532
+ */
533
+ cwd: z8.string().max(4096).nullable().optional()
534
+ });
535
+ var LogLevelSchema = z9.enum(["debug", "info", "warn", "error", "fatal"]);
536
+ var LogLineSchema = z9.object({
537
+ /** Raw log line as received from the process (immutable context) */
538
+ raw: z9.string(),
539
+ /** Source stream (immutable context) */
540
+ stream: z9.enum(["stdout", "stderr"]),
541
+ /** Severity level */
542
+ level: LogLevelSchema,
543
+ /** Parsed/enriched message to display */
544
+ message: z9.string(),
545
+ /** When true, the line is hidden from the log view */
546
+ hidden: z9.boolean(),
547
+ /** Arbitrary key/value pairs shown in the detail panel */
548
+ metadata: z9.record(z9.string().max(256), z9.unknown()).refine(
549
+ (obj) => Object.keys(obj).length <= 100,
550
+ { message: "Too many metadata keys (max 100)" }
551
+ ),
552
+ /** Full parsed JSON object when the line is JSON */
553
+ json: z9.record(z9.string(), z9.unknown()).optional(),
554
+ /** Custom header for the JSON log card (supports HTML, sanitized before display) */
555
+ jsonHeader: z9.string().max(256).optional()
556
+ });
557
+ var LogCardDirectiveSchema = z9.object({
558
+ id: z9.string().max(128),
559
+ type: z9.string().max(64),
560
+ header: z9.string().max(256),
561
+ collapsable: z9.boolean().optional(),
562
+ collapsed: z9.boolean().optional(),
563
+ state: z9.enum(["open", "append", "close", "self"])
564
+ });
565
+ var ParsedLogSchema = z9.object({
566
+ id: z9.string().max(128),
567
+ processId: z9.string().max(128),
568
+ pid: z9.number().int().nullable().optional(),
569
+ timestamp: z9.coerce.date(),
570
+ level: LogLevelSchema.default("info"),
571
+ message: z9.string().max(16384),
572
+ raw: z9.string().max(16384),
573
+ stream: z9.enum(["stdout", "stderr"]).default("stdout"),
574
+ hidden: z9.boolean().optional().default(false),
575
+ metadata: z9.record(z9.string().max(256), z9.unknown()).optional().refine(
576
+ (obj) => !obj || Object.keys(obj).length <= 100,
577
+ { message: "Too many metadata keys (max 100)" }
578
+ ),
579
+ json: z9.record(z9.string(), z9.unknown()).optional(),
580
+ logCard: LogCardDirectiveSchema.optional(),
581
+ sourceType: z9.enum(["console", "file", "mongo", "mysql"]).optional(),
582
+ sourcePath: z9.string().max(4096).optional()
583
+ });
584
+ var ENV_VAR_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
585
+ var AgentConfigSchema = z10.object({
586
+ id: z10.string().max(128),
587
+ name: z10.string().min(1).max(100),
588
+ url: z10.string().url().max(2048),
589
+ passphrase: z10.string().min(1).max(1024),
590
+ passthroughEnv: z10.array(
591
+ z10.string().regex(ENV_VAR_NAME_REGEX).max(256)
592
+ ).max(100).default([])
593
+ });
594
+ var AgentStatusSchema = z10.object({
595
+ connected: z10.boolean(),
596
+ lastSeen: z10.string().nullable().optional(),
597
+ uptime: z10.number().nonnegative().default(0),
598
+ runnersAvailable: z10.array(z10.enum(["native", "docker"])).default(["native"]),
599
+ lanIp: z10.string().optional(),
600
+ /**
601
+ * Ce que la machine de l'agent vaut pour les builtins de portée `agent`.
602
+ * Sert à PRÉVISUALISER — la valeur injectée reste calculée au lancement, par
603
+ * l'agent lui-même : une IP change, un agent se déconnecte, et un statut
604
+ * n'est qu'un cliché.
605
+ */
606
+ os: z10.string().max(64).optional(),
607
+ home: z10.string().max(4096).optional(),
608
+ cwd: z10.string().max(4096).optional()
609
+ });
610
+ var GlobalRoleSchema = z11.enum(["owner", "admin", "member"]);
611
+ var ProjectRoleSchema = z11.enum(["admin", "editor", "viewer"]);
612
+ var ProjectMemberRoleSchema = z11.object({
613
+ id: z11.string().max(128),
614
+ projectId: z11.string().max(128),
615
+ userId: z11.string().max(128),
616
+ role: ProjectRoleSchema
617
+ });
618
+ var ConfigVersionSchema = z12.object({
619
+ id: z12.string().max(128),
620
+ entityType: z12.enum(["project", "service"]),
621
+ entityId: z12.string().max(128),
622
+ version: z12.string().max(128),
623
+ parentVersion: z12.string().max(128),
624
+ data: z12.record(z12.string(), z12.unknown()).refine(
625
+ (obj) => Object.keys(obj).length <= 1e3,
626
+ "Too many data keys (max 1000)"
627
+ ),
628
+ patch: z12.array(z12.record(z12.string(), z12.unknown())).max(1e3),
629
+ userId: z12.string().max(128),
630
+ timestamp: z12.coerce.date(),
631
+ resolved: z12.boolean().default(false)
632
+ });
633
+ var ConfigChangeSchema = z12.object({
634
+ entityType: z12.enum(["project", "service"]),
635
+ entityId: z12.string().max(128),
636
+ version: z12.string().max(128),
637
+ parentVersion: z12.string().max(128),
638
+ data: z12.record(z12.string(), z12.unknown()).refine(
639
+ (obj) => Object.keys(obj).length <= 1e3,
640
+ "Too many data keys (max 1000)"
641
+ ),
642
+ patch: z12.array(z12.record(z12.string(), z12.unknown())).max(1e3)
643
+ });
644
+ var SyncStatusSchema = z12.object({
645
+ connected: z12.boolean(),
646
+ lastSync: z12.coerce.date().nullable().optional(),
647
+ pendingChanges: z12.number().int().nonnegative().default(0),
648
+ conflicts: z12.number().int().nonnegative().default(0)
649
+ });
650
+ var AuditActionSchema = z13.enum([
651
+ "create",
652
+ "update",
653
+ "delete",
654
+ "invite",
655
+ "remove",
656
+ "deploy",
657
+ "start",
658
+ "stop",
659
+ "roleChange"
660
+ ]);
661
+ var AuditEntityTypeSchema = z13.enum([
662
+ "org",
663
+ "project",
664
+ "service",
665
+ "member",
666
+ "permission"
667
+ ]);
668
+ var AuditLogSchema = z13.object({
669
+ id: z13.string().max(128),
670
+ orgId: z13.string().max(128),
671
+ userId: z13.string().max(128),
672
+ action: AuditActionSchema,
673
+ entityType: AuditEntityTypeSchema,
674
+ entityId: z13.string().max(128),
675
+ metadata: z13.record(z13.string().max(256), z13.unknown()).default({}).refine(
676
+ (obj) => Object.keys(obj).length <= 100,
677
+ { message: "Too many metadata keys (max 100)" }
678
+ ),
679
+ timestamp: z13.coerce.date()
680
+ });
681
+ var ProcessMetricsSchema = z14.object({
682
+ processId: z14.string().max(128),
683
+ cpu: z14.number().nonnegative(),
684
+ memory: z14.number().int().nonnegative(),
685
+ timestamp: z14.string()
686
+ });
687
+ var EnvironmentVariablesSchema = z15.record(
688
+ z15.string().max(256),
689
+ VariableSchema
690
+ ).refine(
691
+ (obj) => Object.keys(obj).length <= 200,
692
+ { message: "Max 200 variables" }
693
+ );
694
+ var EnvironmentSchema = z15.object({
695
+ id: z15.string().max(128),
696
+ projectId: z15.string().max(128),
697
+ name: z15.string().min(1).max(200),
698
+ description: z15.string().max(2e3).optional().default(""),
699
+ parentIds: z15.array(z15.string().max(128)).max(10).default([]),
700
+ variables: EnvironmentVariablesSchema.default({}),
701
+ // Scope: 'global' = project-level env, 'service' = per-service env
702
+ scope: z15.enum(["global", "service"]).default("global"),
703
+ // When scope='service': the serviceId this env belongs to
704
+ reference: z15.string().max(128).optional(),
705
+ // When scope='service': the globalEnvId this env activates on
706
+ activateOn: z15.string().max(128).optional(),
707
+ immutable: z15.boolean().default(false),
708
+ default: z15.boolean().optional().default(false),
709
+ color: z15.string().max(20).optional(),
710
+ bgColor: z15.string().max(20).optional(),
711
+ // When scope='global': the default agent used by services that don't have an explicit agentId
712
+ agentId: z15.string().max(128).optional()
713
+ });
714
+ var ResolvedEnvironmentSchema = z15.object({
715
+ variables: z15.record(z15.string().max(256), VariableSchema).refine((obj) => Object.keys(obj).length <= 500, { message: "Max 500 resolved variables" }),
716
+ env: z15.record(z15.string().max(256), z15.string().max(8192)).refine((obj) => Object.keys(obj).length <= 500, { message: "Max 500 resolved env vars" }),
717
+ envSecrets: z15.array(z15.string().max(256)).max(500),
718
+ sources: z15.record(z15.string().max(256), z15.object({
719
+ environmentId: z15.string().max(128),
720
+ environmentName: z15.string().max(200),
721
+ overridden: z15.boolean(),
722
+ resolvedFrom: z15.object({
723
+ environmentId: z15.string().max(128),
724
+ environmentName: z15.string().max(200)
725
+ }).optional(),
726
+ // Set when a machine-local override was applied to this variable, pointing
727
+ // at the environment that carries the winning override. Distinct from
728
+ // `overridden`, which also covers "shadows an inherited variable".
729
+ overriddenBy: z15.object({
730
+ environmentId: z15.string().max(128),
731
+ environmentName: z15.string().max(200)
732
+ }).optional()
733
+ })).refine((obj) => Object.keys(obj).length <= 500, { message: "Max 500 source entries" })
734
+ });
735
+ var CreateEnvironmentSchema = EnvironmentSchema.omit({ id: true }).extend({
736
+ description: z15.string().max(2e3).optional().default(""),
737
+ parentIds: z15.array(z15.string().max(128)).max(10).optional().default([]),
738
+ variables: EnvironmentVariablesSchema.optional().default({}),
739
+ scope: z15.enum(["global", "service"]).optional().default("global"),
740
+ reference: z15.string().max(128).optional(),
741
+ activateOn: z15.string().max(128).optional(),
742
+ immutable: z15.boolean().optional().default(false)
743
+ });
744
+ var UpdateEnvironmentSchema = EnvironmentSchema.partial().required({ id: true }).extend({
745
+ description: z15.string().max(2e3).optional(),
746
+ parentIds: z15.array(z15.string().max(128)).max(10).optional(),
747
+ variables: EnvironmentVariablesSchema.optional(),
748
+ scope: z15.enum(["global", "service"]).optional(),
749
+ immutable: z15.boolean().optional(),
750
+ default: z15.boolean().optional(),
751
+ agentId: z15.string().max(128).nullable().optional()
752
+ // null = clear (no default agent)
753
+ });
754
+ var VariableOverrideSchema = z16.object({
755
+ value: VariableSlotSchema
756
+ });
757
+ var EnvironmentOverridesSchema = z16.record(
758
+ z16.string().max(128),
759
+ VariableOverrideSchema
760
+ ).refine(
761
+ (obj) => Object.keys(obj).length <= 5e3,
762
+ { message: "Max 5000 overrides" }
763
+ );
764
+ var ApiServiceActionSchema = z17.enum([
765
+ "service:read",
766
+ "service:start",
767
+ "service:stop",
768
+ "service:restart",
769
+ "service:healthcheck",
770
+ "service:status",
771
+ "logs:read"
772
+ ]);
773
+ var ApiEnvActionSchema = z17.enum(["env:read", "env:write"]);
774
+ var ApiEnvPermSchema = z17.object({
775
+ environmentId: z17.string().max(128),
776
+ actions: z17.array(ApiEnvActionSchema).max(10).default(["env:read"]),
777
+ includeSecrets: z17.boolean().default(false)
778
+ });
779
+ var ApiServicePermSchema = z17.object({
780
+ serviceId: z17.string().max(128),
781
+ actions: z17.array(ApiServiceActionSchema).max(10).default(["service:read"]),
782
+ environments: z17.array(ApiEnvPermSchema).max(100).default([])
783
+ });
784
+ var ApiKeyScopeSchema = z17.object({
785
+ services: z17.array(ApiServicePermSchema).max(500).default([])
786
+ });
787
+ var ApiKeySchema = z17.object({
788
+ id: z17.string().max(128),
789
+ name: z17.string().min(1).max(200),
790
+ key: z17.string().regex(/^[0-9a-f]{64}$/i),
791
+ scopes: ApiKeyScopeSchema,
792
+ createdAt: z17.string().max(64),
793
+ isPublic: z17.boolean().default(false)
794
+ });
795
+ var PersistedApiKeySchema = ApiKeySchema.extend({
796
+ key: z17.string().min(1).max(4096)
797
+ });
798
+ var ApiKeysFileSchema = z17.object({
799
+ keys: z17.array(PersistedApiKeySchema).max(100)
800
+ });
801
+ var MaskedApiKeySchema = ApiKeySchema.omit({ key: true }).extend({
802
+ /** Last 4 characters of the secret, for recognising a key in a list. */
803
+ keyPreview: z17.string().max(8)
804
+ });
805
+ var ALLOWED_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
806
+ var ImagePartSchema = z18.object({
807
+ type: z18.literal("image"),
808
+ imageId: z18.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "Must be a valid UUID"),
809
+ url: z18.string().min(1),
810
+ mimeType: z18.enum(ALLOWED_IMAGE_MIME_TYPES),
811
+ altText: z18.string().max(500).optional()
812
+ });
813
+ var ImageUploadResponseSchema = z18.object({
814
+ imageId: z18.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "Must be a valid UUID"),
815
+ url: z18.string().min(1),
816
+ mimeType: z18.enum(ALLOWED_IMAGE_MIME_TYPES),
817
+ originalName: z18.string()
818
+ });
819
+ var ImageMetaSchema = z18.object({
820
+ imageId: z18.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "Must be a valid UUID"),
821
+ filePath: z18.string().min(1),
822
+ mimeType: z18.enum(ALLOWED_IMAGE_MIME_TYPES),
823
+ originalName: z18.string().min(1),
824
+ createdAt: z18.number().positive(),
825
+ size: z18.number().positive().optional()
826
+ });
827
+ var KanbanAttachmentSchema = z19.object({
828
+ /** Server-issued id, also the storage key. */
829
+ id: z19.string().max(128),
830
+ name: z19.string().max(255),
831
+ size: z19.number().int().nonnegative(),
832
+ contentType: z19.string().max(128)
833
+ });
834
+ var KanbanCardEventSchema = z19.object({
835
+ type: z19.enum(["created", "edited", "moved", "image_added", "image_removed", "title_changed", "conversation_started", "scenario_started", "comment", "archived", "unarchived"]),
836
+ timestamp: z19.string(),
837
+ /** Extra context: old/new column name, old/new title, comment text, etc. */
838
+ /**
839
+ * Le texte de l'entrée : le corps d'un commentaire, ou le contexte du geste.
840
+ *
841
+ * Borné comme le corps d'une carte (100 000), et non plus à 5 000 : un
842
+ * commentaire est de la prose comme une description, et rien de technique ne
843
+ * justifiait un écart de vingt fois. La borne étroite s'est vue à l'import
844
+ * d'un Kanbanize, où des commentaires de 12 000 caractères passaient par
845
+ * l'API publique — bornée, elle, à 20 000 — pour écrire en base une donnée que
846
+ * ce schéma déclarait invalide, et qu'aucune reprise depuis l'interface
847
+ * n'aurait ensuite pu enregistrer.
848
+ */
849
+ detail: z19.string().max(1e5).optional(),
850
+ /**
851
+ * Stable identity of the event, used to tell an edit from an insertion when a
852
+ * whole history is written back. Absent on events recorded before the field
853
+ * existed; those fall back to (type, timestamp).
854
+ */
855
+ id: z19.string().max(128).optional(),
856
+ /**
857
+ * La colonne où l'événement fait entrer la carte (`created`, `moved`,
858
+ * `unarchived`). Le `detail` n'en porte que le titre, qu'un renommage rend
859
+ * introuvable : l'identifiant, lui, tient. Absent des événements antérieurs
860
+ * que la migration n'a pas pu rapprocher.
861
+ */
862
+ columnId: z19.string().max(128).optional(),
863
+ /** Who authored this event — 'ai' or 'user'. Only set for comment events. */
864
+ author: z19.enum(["ai", "user"]).optional(),
865
+ /**
866
+ * Qui est derrière l'événement, estampillé par le serveur — jamais pris du
867
+ * client, sans quoi n'importe qui pourrait signer au nom d'un autre.
868
+ *
869
+ * Posé sur les commentaires (c'est ce qui rend « mon propre commentaire »
870
+ * répondable ; `author` ne sépare que l'IA de l'humain) et sur les
871
+ * déplacements. Absent des événements écrits avant qu'on l'estampille : un
872
+ * lecteur doit donc afficher « on ne sait pas », pas un nom vide.
873
+ *
874
+ * Pour une clé d'API, c'est le nom de la clé et non son porteur : la clé
875
+ * emprunte les droits de son créateur, pas son identité — personne ne doit
876
+ * croire qu'un humain a déplacé ce qu'un robot de CI a déplacé.
877
+ */
878
+ authorUser: z19.object({ id: z19.string().max(128), name: z19.string().max(200) }).optional(),
879
+ /** Set when the author amended their comment, so the UI can say so. */
880
+ editedAt: z19.string().optional(),
881
+ /** Files attached to a comment — same storage as a card's own attachments. */
882
+ attachments: z19.array(KanbanAttachmentSchema).max(20).optional(),
883
+ /** Conversation that produced this event (comments only). Used to navigate back to the conversation. */
884
+ conversationId: z19.string().optional()
885
+ });
886
+ var KanbanBoardEventTypeSchema = z19.enum([
887
+ "board_renamed",
888
+ "column_added",
889
+ "column_renamed",
890
+ "column_deleted",
891
+ "card_deleted"
892
+ ]);
893
+ var KanbanBoardHistoryEntrySchema = z19.object({
894
+ id: z19.string().max(128),
895
+ source: z19.enum(["card", "board"]),
896
+ type: z19.union([KanbanCardEventSchema.shape.type, KanbanBoardEventTypeSchema]),
897
+ timestamp: z19.string(),
898
+ detail: z19.string().max(1e5).optional(),
899
+ author: KanbanCardEventSchema.shape.author,
900
+ authorUser: KanbanCardEventSchema.shape.authorUser,
901
+ columnId: z19.string().max(128).optional(),
902
+ /** La carte concernée : celle qui porte l'événement, ou celle supprimée. */
903
+ card: z19.object({ id: z19.string().max(128), title: z19.string().max(500) }).optional(),
904
+ /** Pour un renommage : l'ancien nom, puis le nouveau. Sinon, le nom concerné dans `to`. */
905
+ from: z19.string().max(500).optional(),
906
+ to: z19.string().max(500).optional()
907
+ });
908
+ var KanbanCardSchema = z19.object({
909
+ id: z19.string(),
910
+ title: z19.string().min(1).max(500),
911
+ markdown: z19.string().max(1e5).default(""),
912
+ /**
913
+ * Legacy inline images, kept so boards written before object storage still
914
+ * render. New uploads go to `attachments` instead.
915
+ */
916
+ images: z19.array(z19.string()).default([]),
917
+ /** Files attached to the card — stored out of band, referenced here. */
918
+ attachments: z19.array(KanbanAttachmentSchema).max(50).default([]),
919
+ order: z19.number().int(),
920
+ /**
921
+ * La priorité de la carte, de 1 à 10 — 10 étant le plus urgent.
922
+ *
923
+ * Facultative, et absente veut dire « pas de priorité », pas « la plus
924
+ * basse » : une carte qu'on n'a pas encore arbitrée ne doit pas se ranger
925
+ * d'office au fond de la pile.
926
+ */
927
+ priority: z19.number().int().min(1).max(10).optional(),
928
+ /**
929
+ * Les groupes de la carte — des étiquettes libres, posées par l'équipe.
930
+ *
931
+ * Rien ne les déclare ailleurs : un groupe existe parce qu'une carte le
932
+ * porte, et disparaît avec la dernière qui le portait. C'est ce qui permet
933
+ * d'en créer un en le tapant, sans passer par un écran d'administration.
934
+ */
935
+ groups: z19.array(z19.string().min(1).max(60)).max(30).optional(),
936
+ /**
937
+ * L'échéance de la carte : le jour où elle est attendue, `AAAA-MM-JJ`.
938
+ *
939
+ * Un jour, pas un instant, et c'est une décision : une équipe se dit « pour
940
+ * vendredi », jamais « pour vendredi 23:59:59 UTC ». Stocker un instant
941
+ * ferait afficher une échéance au 4 mars comme le 3 mars à qui travaille à
942
+ * l'ouest de Greenwich — la carte serait en retard d'un jour pour la moitié
943
+ * de l'équipe, et personne ne saurait pourquoi.
944
+ *
945
+ * Facultative, et absente veut dire « pas d'échéance », pas « en retard » :
946
+ * une carte qu'on n'a pas datée ne doit pas s'annoncer comme urgente.
947
+ */
948
+ deadline: z19.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
949
+ /**
950
+ * La couleur de la carte, en hexadécimal (`#RRGGBB`) — un liseré en haut de
951
+ * la carte, et rien d'autre : ce qu'elle veut dire appartient à l'équipe.
952
+ *
953
+ * Bornée à une notation stricte plutôt qu'à une chaîne libre : cette valeur
954
+ * finit dans un attribut `style`, et une couleur ne doit pouvoir contenir
955
+ * qu'une couleur. Absente sur les cartes sans couleur.
956
+ */
957
+ color: z19.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
958
+ /**
959
+ * Le journal de la carte — commentaires compris.
960
+ *
961
+ * Optionnel, et c'est tout l'intérêt : c'est la partie qui grossit sans fin
962
+ * (une IA bavarde y écrit des pages), alors les lectures de tableau ne le
963
+ * transportent plus. `undefined` veut dire « pas chargé », et une écriture
964
+ * qui l'omet laisse au serveur celui qu'il a en base — à ne pas confondre
965
+ * avec `[]`, qui demanderait bel et bien de le vider.
966
+ */
967
+ history: z19.array(KanbanCardEventSchema).optional(),
968
+ /**
969
+ * Ce que le journal absent laisse quand même dire : combien de commentaires,
970
+ * et quand fut le dernier. Posés par le serveur en lecture, ignorés en
971
+ * écriture — la carte ne les stocke pas, ils se déduisent de `history`.
972
+ */
973
+ commentCount: z19.number().int().nonnegative().optional(),
974
+ lastCommentAt: z19.string().optional(),
975
+ /**
976
+ * L'auteur du dernier commentaire, pour son avatar sur la tuile. Même règle :
977
+ * posé en lecture, ignoré en écriture. `id`/`name` absents quand le
978
+ * commentaire précède l'estampille d'auteur — on sait alors seulement si
979
+ * c'était une IA.
980
+ */
981
+ lastCommentBy: z19.object({ id: z19.string().max(128).optional(), name: z19.string().max(200).optional(), author: z19.enum(["ai", "user"]).optional() }).optional(),
982
+ /**
983
+ * Les entrées de journal qui ouvrent une conversation IA. Elles restent dans
984
+ * la lecture allégée — il y en a une poignée par carte, et l'hôte s'en sert
985
+ * pour dire, sans ouvrir la carte, qu'une IA y travaille.
986
+ */
987
+ conversationEvents: z19.array(KanbanCardEventSchema).optional(),
988
+ createdAt: z19.string(),
989
+ updatedAt: z19.string(),
990
+ /**
991
+ * Quand la carte a été archivée — absente tant qu'elle est au tableau.
992
+ *
993
+ * Une carte archivée reste dans sa colonne en base : c'est la lecture qui la
994
+ * retire du tableau, jamais une écriture qui la déplace. Rien à recoller donc
995
+ * au désarchivage, et aucun cimetière où une carte pourrait se perdre.
996
+ */
997
+ archivedAt: z19.string().optional(),
998
+ /**
999
+ * La colonne d'où la carte a été archivée, telle qu'elle s'appelait alors.
1000
+ *
1001
+ * L'identifiant sert à l'y remettre, le titre à le dire dans l'archive même
1002
+ * quand la colonne a disparu depuis — auquel cas le désarchivage repose la
1003
+ * carte dans la première colonne, sans se taire sur son origine.
1004
+ */
1005
+ archivedFrom: z19.object({ columnId: z19.string().max(128), columnTitle: z19.string().max(200) }).optional(),
1006
+ /**
1007
+ * Le tableau d'origine d'une carte vue depuis un tableau où elle est posée
1008
+ * en référence. Jamais stocké : le serveur le pose à la lecture. Absent chez elle.
1009
+ */
1010
+ origin: z19.object({ boardId: z19.string().max(64), boardName: z19.string().max(200) }).optional(),
1011
+ /**
1012
+ * Quand la carte a été supprimée. Rien n'est effacé : la carte reste en base,
1013
+ * et toutes les lectures l'écartent.
1014
+ *
1015
+ * Une suppression est le seul geste que rien ne rattrape — sauf à ne pas
1016
+ * l'exécuter vraiment. La colonne et le tableau portent le même champ, pour
1017
+ * que supprimer l'un n'emporte jamais les cartes de l'autre.
1018
+ */
1019
+ deletedAt: z19.string().optional(),
1020
+ /**
1021
+ * People assigned to the card. Names are denormalized for display; the server
1022
+ * keeps only members of the owning organization, and notifies newcomers.
1023
+ */
1024
+ assignees: z19.array(z19.object({ id: z19.string().max(128), name: z19.string().max(200) })).max(20).default([]),
1025
+ /**
1026
+ * Who first created the card. Stamped by the server the first time a card id
1027
+ * appears in a board write, never taken from the client — a member could
1028
+ * otherwise claim any authorship. Absent on cards created before this field
1029
+ * existed, so readers must treat it as unknown rather than assume.
1030
+ */
1031
+ createdBy: z19.object({ id: z19.string().max(128), name: z19.string().max(200) }).optional()
1032
+ });
1033
+ var KanbanColumnSchema = z19.object({
1034
+ id: z19.string(),
1035
+ title: z19.string().min(1).max(200),
1036
+ order: z19.number().int(),
1037
+ cards: z19.array(KanbanCardSchema).default([]),
1038
+ /**
1039
+ * Le regroupement auquel la colonne appartient, s'il y en a un.
1040
+ *
1041
+ * Un pointeur vers le parent, et rien dans l'autre sens : le groupe ne tient
1042
+ * pas la liste de ses membres. Deux listes qui prétendent dire qui contient
1043
+ * quoi finissent par se contredire, et un déplacement devient alors deux
1044
+ * écritures au lieu d'une.
1045
+ */
1046
+ groupId: z19.string().optional(),
1047
+ /**
1048
+ * Colonne de cartes finalisées : y entrer compte comme « finalisée » dans les
1049
+ * statistiques de la liste, au même titre qu'un archivage.
1050
+ */
1051
+ done: z19.boolean().optional(),
1052
+ /** Supprimée, mais conservée — voir `deletedAt` sur la carte. */
1053
+ deletedAt: z19.string().optional()
1054
+ });
1055
+ var KanbanColumnGroupSchema = z19.object({
1056
+ id: z19.string(),
1057
+ title: z19.string().min(1).max(200),
1058
+ /** La couleur du bandeau, en hexadécimal — l'interface décide du défaut. */
1059
+ color: z19.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
1060
+ /** Le groupe parent, pour les regroupements imbriqués. */
1061
+ parentId: z19.string().optional(),
1062
+ /** Supprimé, mais conservé — comme une colonne. */
1063
+ deletedAt: z19.string().optional()
1064
+ });
1065
+ var KanbanBoardSchema = z19.object({
1066
+ id: z19.string(),
1067
+ name: z19.string().min(1).max(200),
1068
+ description: z19.string().max(1e4).default(""),
1069
+ /**
1070
+ * Le mode d'emploi du tableau, en texte libre : ce que veulent dire ses
1071
+ * colonnes, quand une carte a le droit d'avancer, les conventions de
1072
+ * l'équipe. Écrit par les humains, lu surtout par l'IA — il est injecté dans
1073
+ * le prompt des agents pour qu'ils travaillent selon les règles du tableau
1074
+ * plutôt que selon les leurs.
1075
+ */
1076
+ instructions: z19.string().max(2e4).default(""),
1077
+ columns: z19.array(KanbanColumnSchema).default([]),
1078
+ /** Les regroupements de colonnes — vide sur un tableau qui n'en a jamais eu. */
1079
+ columnGroups: z19.array(KanbanColumnGroupSchema).default([]),
1080
+ /**
1081
+ * Les projets auxquels le tableau est rattaché, par leur identifiant.
1082
+ *
1083
+ * Un rattachement, pas une possession : il ne sert qu'à filtrer la liste des
1084
+ * tableaux, et un tableau sans projet (`[]`, le cas par défaut) reste visible
1085
+ * partout. Plusieurs projets sont permis parce qu'une équipe partage souvent
1086
+ * un même tableau entre deux projets — obliger à choisir l'un des deux
1087
+ * ferait disparaître le tableau de la liste de l'autre.
1088
+ *
1089
+ * Les identifiants sont ceux des projets de l'organisation ; le serveur
1090
+ * refuse ceux qu'il ne connaît pas plutôt que de garder un lien mort.
1091
+ */
1092
+ projectIds: z19.array(z19.string().min(1).max(128)).max(20).default([]),
1093
+ archived: z19.boolean().default(false),
1094
+ /** Supprimé, mais conservé avec ses cartes — voir `deletedAt` sur la carte. */
1095
+ deletedAt: z19.string().optional(),
1096
+ createdAt: z19.string(),
1097
+ updatedAt: z19.string()
1098
+ });
1099
+ var CreateKanbanBoardInput = z19.object({
1100
+ name: z19.string().min(1).max(200),
1101
+ description: z19.string().max(1e4).optional(),
1102
+ instructions: z19.string().max(2e4).optional(),
1103
+ projectIds: z19.array(z19.string().min(1).max(128)).max(20).optional()
1104
+ });
1105
+ var UpdateKanbanBoardInput = z19.object({
1106
+ id: z19.string(),
1107
+ name: z19.string().min(1).max(200).optional(),
1108
+ description: z19.string().max(1e4).optional(),
1109
+ instructions: z19.string().max(2e4).optional(),
1110
+ columns: z19.array(KanbanColumnSchema).optional(),
1111
+ archived: z19.boolean().optional(),
1112
+ projectIds: z19.array(z19.string().min(1).max(128)).max(20).optional()
1113
+ });
1114
+ var KanbanCardLinkSchema = z19.object({
1115
+ id: z19.string().max(128),
1116
+ /** Le tableau et la carte qui *dépendent* de l'autre. */
1117
+ childBoardId: z19.string().max(64),
1118
+ childCardId: z19.string().max(128),
1119
+ /** Le tableau et la carte *dont* on dépend. */
1120
+ parentBoardId: z19.string().max(64),
1121
+ parentCardId: z19.string().max(128),
1122
+ createdAt: z19.string(),
1123
+ createdBy: z19.object({ id: z19.string().max(128), name: z19.string().max(200) }).optional()
1124
+ });
1125
+ var KanbanCardRefSchema = z19.object({
1126
+ cardId: z19.string().max(128),
1127
+ boardId: z19.string().max(64),
1128
+ boardName: z19.string().max(200),
1129
+ columnId: z19.string().max(128),
1130
+ columnTitle: z19.string().max(200),
1131
+ title: z19.string().max(500),
1132
+ /** Les mêmes informations qu'une ligne de tableau montre d'une carte d'ici. */
1133
+ assignees: z19.array(z19.object({ id: z19.string().max(128), name: z19.string().max(200) })).default([]),
1134
+ commentCount: z19.number().int().nonnegative().default(0)
1135
+ });
1136
+ var KanbanCardLocationSchema = z19.object({
1137
+ boardId: z19.string().max(64),
1138
+ boardName: z19.string().max(200),
1139
+ columnId: z19.string().max(128),
1140
+ columnTitle: z19.string().max(200),
1141
+ home: z19.boolean()
1142
+ });
1143
+ var CloudApiScopeSchema = z20.enum([
1144
+ "kanban:read",
1145
+ "kanban:write",
1146
+ "kanban:comment",
1147
+ "kanban:delete"
1148
+ ]);
1149
+ var CloudApiKeySecretSchema = z20.string().regex(/^rk_[0-9a-f]{64}$/);
1150
+ var CloudApiKeySchema = z20.object({
1151
+ id: z20.string().max(128),
1152
+ organizationId: z20.string().max(128),
1153
+ name: z20.string().min(1).max(200),
1154
+ scopes: z20.array(CloudApiScopeSchema).min(1).max(10),
1155
+ /**
1156
+ * Les tableaux que la clé peut atteindre, ou `null` pour « tous ceux de
1157
+ * l'organisation ».
1158
+ *
1159
+ * `null` et `[]` ne sont pas la même chose : la liste vide n'ouvre rien, et
1160
+ * c'est délibéré — un tableau supprimé ne doit pas élargir une clé qui ne le
1161
+ * nommait plus que lui.
1162
+ */
1163
+ boards: z20.array(z20.string().max(64)).max(200).nullable(),
1164
+ createdAt: z20.string().max(64),
1165
+ /** Dernier usage constaté, pour reconnaître une clé oubliée. */
1166
+ lastUsedAt: z20.string().max(64).nullable(),
1167
+ /** Qui l'a émise — une clé sans visage ne se révoque jamais sereinement. */
1168
+ createdBy: z20.object({ id: z20.string().max(128), name: z20.string().max(200) }),
1169
+ /** Les 4 derniers caractères du secret, pour reconnaître une clé dans une liste. */
1170
+ keyPreview: z20.string().max(8)
1171
+ });
1172
+ var CreatedCloudApiKeySchema = z20.object({
1173
+ key: CloudApiKeySchema,
1174
+ secret: CloudApiKeySecretSchema
1175
+ });
1176
+ var CreateCloudApiKeyInput = z20.object({
1177
+ organizationId: z20.string().min(1).max(128),
1178
+ name: z20.string().min(1).max(200),
1179
+ scopes: z20.array(CloudApiScopeSchema).min(1).max(10),
1180
+ boards: z20.array(z20.string().max(64)).max(200).nullable().default(null)
1181
+ });
1182
+ var UpdateCloudApiKeyInput = z20.object({
1183
+ id: z20.string().min(1).max(128),
1184
+ name: z20.string().min(1).max(200).optional(),
1185
+ scopes: z20.array(CloudApiScopeSchema).min(1).max(10).optional(),
1186
+ boards: z20.array(z20.string().max(64)).max(200).nullable().optional()
1187
+ });
1188
+ var IntegrationProviderSchema = z21.enum(["intercom", "hubspot"]);
1189
+ var IntegrationConnectionSchema = z21.object({
1190
+ id: z21.string().max(128),
1191
+ organizationId: z21.string().max(128),
1192
+ provider: IntegrationProviderSchema,
1193
+ name: z21.string().min(1).max(200),
1194
+ /** Le tableau et la colonne de création — seulement avec la création automatique. */
1195
+ boardId: z21.string().max(64).nullable(),
1196
+ columnId: z21.string().max(128).nullable(),
1197
+ enabled: z21.boolean(),
1198
+ /**
1199
+ * L'URL complète du webhook à coller chez le fournisseur, construite par le
1200
+ * cloud (domaine public des webhooks). Elle contient un jeton imprévisible.
1201
+ */
1202
+ webhookUrl: z21.string().max(2048),
1203
+ /** Les quatre derniers caractères du secret de signature, pour le reconnaître. */
1204
+ secretPreview: z21.string().max(8),
1205
+ region: z21.string().max(16),
1206
+ /** Créer une carte pour toute conversation qui n'est encore liée nulle part. */
1207
+ autoCreate: z21.boolean(),
1208
+ createdBy: z21.object({ id: z21.string().max(128), name: z21.string().max(200) }),
1209
+ createdAt: z21.string().max(64),
1210
+ lastEventAt: z21.string().max(64).nullable()
1211
+ });
1212
+ var IntegrationConversationStateSchema = z21.enum(["open", "resolved"]);
1213
+ var IntegrationMessageSchema = z21.object({
1214
+ id: z21.string().max(256),
1215
+ author: z21.object({ name: z21.string().max(500), role: z21.enum(["customer", "agent"]) }),
1216
+ body: z21.string().max(1e5),
1217
+ internal: z21.boolean(),
1218
+ sentAt: z21.string().max(64),
1219
+ /** Fichiers joints et images, copiés dans Runeya : ils se lisent comme ceux d'une carte. */
1220
+ attachments: z21.array(KanbanAttachmentSchema).max(50),
1221
+ /** Noms des fichiers qui n'ont pas pu être copiés. */
1222
+ missingAttachments: z21.array(z21.string().max(500)).max(50)
1223
+ });
1224
+ var IntegrationConversationSchema = z21.object({
1225
+ id: z21.string().max(256),
1226
+ connectionId: z21.string().max(128),
1227
+ provider: IntegrationProviderSchema,
1228
+ externalId: z21.string().max(128),
1229
+ title: z21.string().max(500),
1230
+ url: z21.string().max(2e3).nullable(),
1231
+ state: IntegrationConversationStateSchema,
1232
+ updatedAt: z21.string().max(64)
1233
+ });
1234
+ var CardConversationSchema = IntegrationConversationSchema.extend({
1235
+ origin: z21.enum(["auto", "manual"]),
1236
+ /** Les fichiers de la conversation sont en train d'être copiés : ils vont apparaître. */
1237
+ filesPending: z21.boolean(),
1238
+ linkedAt: z21.string().max(64),
1239
+ opening: z21.object({
1240
+ author: IntegrationMessageSchema.shape.author,
1241
+ body: z21.string().max(1e5),
1242
+ attachments: z21.array(KanbanAttachmentSchema).max(50),
1243
+ missingAttachments: z21.array(z21.string().max(500)).max(50)
1244
+ }).nullable(),
1245
+ messages: z21.array(IntegrationMessageSchema),
1246
+ otherCards: z21.array(z21.object({ boardId: z21.string().max(64), cardId: z21.string().max(64), title: z21.string().max(500) }))
1247
+ });
1248
+ var ConversationSearchHitSchema = IntegrationConversationSchema.extend({
1249
+ preview: z21.string().max(500),
1250
+ linkedCards: z21.array(z21.object({ boardId: z21.string().max(64), cardId: z21.string().max(64), title: z21.string().max(500) }))
1251
+ });
1252
+ var ChangelogSurfaceSchema = z22.enum(["app", "cloud"]);
1253
+ var ChangelogChangeKindSchema = z22.enum(["feature", "improvement", "fix", "breaking"]);
1254
+ var ChangelogTextSchema = z22.object({
1255
+ fr: z22.string().min(1).max(500),
1256
+ en: z22.string().min(1).max(500)
1257
+ });
1258
+ var ChangelogChangeSchema = z22.object({
1259
+ kind: ChangelogChangeKindSchema,
1260
+ text: ChangelogTextSchema
1261
+ });
1262
+ var ChangelogDraftSchema = ChangelogChangeSchema.extend({
1263
+ date: z22.string().regex(/^\d{4}-\d{2}-\d{2}$/),
1264
+ title: ChangelogTextSchema.optional()
1265
+ });
1266
+ var ChangelogReleaseSchema = z22.object({
1267
+ surface: ChangelogSurfaceSchema,
1268
+ /** `1.4.2` pour l'app (version npm publiée), `2026-09-03` — ou `2026-09-03.2` — pour le cloud. */
1269
+ version: z22.string().min(1).max(32).regex(/^[0-9][0-9.-]*$/),
1270
+ /** Date de publication, `YYYY-MM-DD`. */
1271
+ date: z22.string().regex(/^\d{4}-\d{2}-\d{2}$/),
1272
+ /** Facultatif : une version peut s'annoncer par son seul numéro. */
1273
+ title: ChangelogTextSchema.optional(),
1274
+ /** Une version sans changement n'a rien à annoncer : elle n'a pas sa place ici. */
1275
+ changes: z22.array(ChangelogChangeSchema).min(1).max(50)
1276
+ });
1277
+ var ChangelogIndexItemSchema = ChangelogReleaseSchema.omit({ changes: true }).extend({
1278
+ changeCount: z22.number().int().nonnegative()
1279
+ });
1280
+ var MASKED_VALUE = "***";
1281
+ function maskLogSourceSecrets(sources) {
1282
+ return sources.map((src) => {
1283
+ if (src.type === "mongo") return { ...src, connectionString: MASKED_VALUE };
1284
+ if (src.type === "mysql" && src.password) return { ...src, password: MASKED_VALUE };
1285
+ return src;
1286
+ });
1287
+ }
1288
+ var BUILTIN_VARIABLES = [
1289
+ // ── Le service ───────────────────────────────────────────────────────────
1290
+ { name: "service.NAME", scope: "server", descriptionKey: "serviceName" },
1291
+ { name: "service.ID", scope: "server", descriptionKey: "serviceId" },
1292
+ { name: "service.PORT", scope: "server", descriptionKey: "servicePort" },
1293
+ { name: "service.URL", scope: "server", descriptionKey: "serviceUrl" },
1294
+ { name: "service.BASE_URL", scope: "server", descriptionKey: "serviceBaseUrl" },
1295
+ { name: "service.CWD", scope: "agent", descriptionKey: "serviceCwd" },
1296
+ // ── Le projet et l'environnement actif ───────────────────────────────────
1297
+ { name: "project.NAME", scope: "server", descriptionKey: "projectName" },
1298
+ { name: "runeya.ENV_NAME", scope: "server", descriptionKey: "envName" },
1299
+ // ── Le serveur Runeya ────────────────────────────────────────────────────
1300
+ // `service.HOST` a toujours désigné l'adresse d'écoute du SERVEUR, pas une
1301
+ // propriété du service : le nom trompait, il devient un alias.
1302
+ { name: "runeya.HOST", scope: "server", descriptionKey: "runeyaHost", deprecatedAlias: "service.HOST" },
1303
+ // ── La machine qui exécute ───────────────────────────────────────────────
1304
+ { name: "agent.HOST_LAN", scope: "agent", descriptionKey: "agentHostLan", deprecatedAlias: "service.HOST_LAN" },
1305
+ // Le NOM de l'agent est une étiquette de la configuration du serveur, pas
1306
+ // une propriété de la machine : c'est le serveur qui le connaît et le résout.
1307
+ { name: "agent.NAME", scope: "server", descriptionKey: "agentName" },
1308
+ { name: "agent.OS", scope: "agent", descriptionKey: "agentOs" },
1309
+ { name: "agent.HOME", scope: "agent", descriptionKey: "agentHome" }
1310
+ ];
1311
+ var GRACE_PERIOD = 1e4;
1312
+ var AGENT_STOP_FETCH_TIMEOUT = GRACE_PERIOD + 15e3;
1313
+ var SUPPORTED_EDITORS = [
1314
+ { key: "vscode", name: "Visual Studio Code", command: "code" },
1315
+ { key: "cursor", name: "Cursor", command: "cursor" },
1316
+ { key: "windsurf", name: "Windsurf", command: "windsurf" },
1317
+ { key: "sublime", name: "Sublime Text", command: "subl" },
1318
+ { key: "atom", name: "Atom", command: "atom" },
1319
+ { key: "antigravity", name: "Antigravity", command: "antigravity" },
1320
+ { key: "intellij", name: "IntelliJ IDEA", command: "idea" },
1321
+ { key: "android-studio", name: "Android Studio", command: "studio" },
1322
+ { key: "webstorm", name: "WebStorm", command: "webstorm" },
1323
+ { key: "zed", name: "Zed", command: "zed" },
1324
+ { key: "neovim", name: "Neovim", command: "nvim" },
1325
+ { key: "vim", name: "Vim", command: "vim" },
1326
+ { key: "emacs", name: "Emacs", command: "emacs" },
1327
+ { key: "fleet", name: "Fleet", command: "fleet" }
1328
+ ];
1329
+ var EDITOR_BY_KEY = new Map(
1330
+ SUPPORTED_EDITORS.map((e) => [e.key, e])
1331
+ );
1332
+
1333
+ // ../ai-capabilities/dist/index.js
1334
+ import { z as z24 } from "zod";
1335
+ import { z as z32 } from "zod";
1336
+ function toList(raw) {
1337
+ if (!raw) return [];
1338
+ const text2 = raw.trim();
1339
+ if (text2.startsWith("[")) {
1340
+ try {
1341
+ const parsed = JSON.parse(text2);
1342
+ if (Array.isArray(parsed)) return parsed.map((v) => String(v).trim()).filter(Boolean);
1343
+ } catch {
1344
+ }
1345
+ }
1346
+ return text2.split(",").map((v) => v.trim().replace(/^["'[]+|["'\]]+$/g, "")).filter(Boolean);
1347
+ }
1348
+ function clearable(raw, parse, key) {
1349
+ if (raw === void 0) return {};
1350
+ const text2 = String(raw).trim();
1351
+ if (!text2 || text2 === "null") return { [key]: null };
1352
+ return { [key]: parse(text2) };
1353
+ }
1354
+ function gitTarget(p) {
1355
+ return {
1356
+ ...p.serviceId ? { serviceId: p.serviceId } : {},
1357
+ ...p.projectId ? { projectId: p.projectId } : {},
1358
+ ...p.agentId ? { agentId: p.agentId } : {},
1359
+ ...p.orgId ? { orgId: p.orgId } : {}
1360
+ };
1361
+ }
1362
+ var toBool = (raw) => raw.trim().toLowerCase() === "true";
1363
+ var newId = () => globalThis.crypto.randomUUID();
1364
+ function listItem(raw) {
1365
+ try {
1366
+ return JSON.parse(raw);
1367
+ } catch {
1368
+ return raw;
1369
+ }
1370
+ }
1371
+ function parseTyped(name, schema, raw) {
1372
+ if (raw === void 0) return void 0;
1373
+ let result = raw.trim() === "null" ? schema.safeParse(null) : schema.safeParse(raw);
1374
+ if (!result.success && raw.trim() === "null") result = schema.safeParse(raw);
1375
+ if (!result.success) {
1376
+ const text2 = raw.trim();
1377
+ const json = listItem(text2);
1378
+ if (json !== text2) result = schema.safeParse(json);
1379
+ if (!result.success && !/^[[{]/.test(text2)) {
1380
+ const list = schema.safeParse(toList(text2).map(listItem));
1381
+ if (list.success) result = list;
1382
+ }
1383
+ }
1384
+ if (!result.success) throw new Error(`${name}: ${z23.prettifyError(result.error)}`);
1385
+ return result.data;
1386
+ }
1387
+ function withIds(items) {
1388
+ return items.map((item) => ({ ...item, id: item.id || newId() }));
1389
+ }
1390
+ var optionalId = (schema) => schema.extend({ id: z23.string().min(1).max(64).optional() });
1391
+ var DockerConfigInput = DockerConfigSchema.extend({
1392
+ bootstrapCommands: z23.array(optionalId(BootstrapCommandSchema)).max(20).default([])
1393
+ }).refine((c) => !!c.image !== !!c.dockerfile, {
1394
+ message: 'dockerConfig needs exactly one of "image" (an image to pull) or "dockerfile" (a Dockerfile to build)'
1395
+ });
1396
+ var SERVICE_FIELDS = [
1397
+ { name: "description", description: "What the service is, in a sentence" },
1398
+ { name: "agentId", description: "The agent that runs it, from list_agents. Omit to inherit from the environment.", clearable: true },
1399
+ { name: "cwd", description: `Working directory, RELATIVE to the project root (e.g. "apps/web", or omit for the root). The project is synced between machines and lives in a different folder on each: Runeya resolves a relative cwd against each machine's own project folder, while an absolute path (/home/you/\u2026) breaks on every other machine. Use an absolute path only for something outside the project, written with ~ or an environment variable ($NAME) rather than a literal home directory.`, clearable: true },
1400
+ { name: "command", description: 'Shortcut for a single command line (e.g. "npm run dev"). Replaces every existing command. Use commands for several.' },
1401
+ { name: "commands", description: 'The processes of the service. They all start AT THE SAME TIME, in parallel, and the service runs while they run \u2014 not one after another. Use several for processes that live side by side (a web server and a worker). Steps that must follow each other (build, then install, then launch) go in ONE command chained with &&. A one-off action the user triggers by hand (build, deploy, read logs) is a shortcut, not a command. Each command runs in the service cwd, or its own "cwd", relative to the project root. No absolute paths of this machine: the service is synced to other machines. Replaces every existing command. "id" is generated when omitted; "shell" runs the line through a shell (default true).', schema: z23.array(optionalId(ServiceCommandSchema)).max(50) },
1402
+ { name: "ports", description: "Ports the service listens on. Declare them: they become {{service.PORT}}, {{service.URL}} and {{services.<slug>.URL}} for the other services, which environment variables should reference instead of repeating the numbers.", schema: ServiceConfigSchema.shape.ports.unwrap().unwrap() },
1403
+ { name: "groups", description: "Groups the service belongs to. Replaces the current ones.", schema: ServiceConfigSchema.shape.groups.unwrap().unwrap() },
1404
+ { name: "url", description: "URL where the service is reached" },
1405
+ { name: "localUrls", description: 'Local domain names routed to a port of the service (e.g. {"url":"api.localhost.dev","port":3000})', schema: ServiceConfigSchema.shape.localUrls.unwrap().unwrap() },
1406
+ { name: "openapiUrl", description: "URL of its OpenAPI document" },
1407
+ { name: "docsPath", description: "Where its markdown docs live, relative to cwd or absolute. Defaults to cwd." },
1408
+ { name: "git", description: 'Its git repository: "remote" URL and "home" page', schema: ServiceConfigSchema.shape.git.unwrap(), clearable: true },
1409
+ { name: "meta", description: "Free key/value metadata (strings)", schema: ServiceConfigSchema.shape.meta.unwrap(), clearable: true },
1410
+ { name: "runner", description: '"native" runs the commands on the machine; "docker" runs the service in a container and then needs dockerConfig', schema: ServiceConfigSchema.shape.runner.unwrap().unwrap() },
1411
+ { name: "dockerConfig", description: 'With runner "docker": the container. Exactly one of "image" (pull) or "dockerfile" (inline Dockerfile, built with "buildContext", "buildArgs", "targetStage"). Ports and volumes use Docker syntax ("8080:80", "./data:/data"); host paths in volumes relative to the service directory, never an absolute path of this machine. "useNativeCommand" runs the service commands inside the container instead of its CMD ("nativeCommandMode": one shared container or one per command). "bootstrapCommands" run once when the container starts. Replaces the whole Docker configuration: read it first with get_service to keep what you do not change.', schema: DockerConfigInput },
1412
+ { name: "healthCheck", description: 'How to tell the service is ready: an HTTP "path" to call, or a "command" that must succeed', schema: HealthCheckSchema, clearable: true },
1413
+ { name: "autoRestart", description: "Restart the service when it crashes", schema: z23.boolean() },
1414
+ { name: "restartStrategy", description: "When to restart it", schema: RestartStrategySchema.unwrap() },
1415
+ { name: "maxRestarts", description: "Give up after this many restarts", schema: ServiceConfigSchema.shape.maxRestarts.unwrap() },
1416
+ { name: "restartBackoffMs", description: "Delay before a restart, in milliseconds", schema: ServiceConfigSchema.shape.restartBackoffMs.unwrap().unwrap() },
1417
+ { name: "clearLogsOnStart", description: "Clear the logs each time the service starts", schema: z23.boolean() },
1418
+ { name: "shortcuts", description: 'One-click actions shown on the service, run on demand and independently of the running commands (build, deploy, migrate, read logs). Same path rules as commands: relative to the project root, never an absolute path of this machine. Replaces the current ones. "id" is generated when omitted.', schema: z23.array(optionalId(ServiceShortcutSchema)).max(50) },
1419
+ { name: "parserIds", description: "Log parsers applied to its output, by id", schema: ServiceConfigSchema.shape.parserIds.unwrap() },
1420
+ { name: "logSources", description: 'Extra log sources beyond the console output (files, databases\u2026). Replaces the current ones. Credentials (mongo connectionString, mysql password) read as "***": send "***" back to keep the stored value, or the real value to change it.', schema: ServiceConfigSchema.shape.logSources.unwrap().unwrap() }
1421
+ ];
1422
+ function serviceParams(clearable2) {
1423
+ return SERVICE_FIELDS.map((f) => {
1424
+ const nullable = clearable2 && f.clearable;
1425
+ const base = f.schema ?? (nullable ? z23.string() : void 0);
1426
+ return {
1427
+ name: f.name,
1428
+ description: nullable ? `${f.description} null clears it.` : f.description,
1429
+ required: false,
1430
+ ...base ? { schema: nullable ? base.nullable() : base } : {}
1431
+ };
1432
+ });
1433
+ }
1434
+ function serviceFields(p, params) {
1435
+ const out = {};
1436
+ for (const param of params) {
1437
+ if (p[param.name] === void 0 || param.name === "command") continue;
1438
+ out[param.name] = param.schema ? parseTyped(param.name, param.schema, p[param.name]) : p[param.name];
1439
+ }
1440
+ if (p.command !== void 0 && p.commands === void 0) out["commands"] = [{ label: "", command: p.command, shell: true }];
1441
+ if (Array.isArray(out["commands"])) out["commands"] = withIds(out["commands"]);
1442
+ if (Array.isArray(out["shortcuts"])) out["shortcuts"] = withIds(out["shortcuts"]);
1443
+ const docker = out["dockerConfig"];
1444
+ if (docker?.bootstrapCommands) out["dockerConfig"] = { ...docker, bootstrapCommands: withIds(docker.bootstrapCommands) };
1445
+ return out;
1446
+ }
1447
+ function maskServices(result) {
1448
+ if (Array.isArray(result)) return result.map(maskServices);
1449
+ if (result && typeof result === "object" && Array.isArray(result.logSources)) {
1450
+ const service = result;
1451
+ return { ...service, logSources: maskLogSourceSecrets(service.logSources) };
1452
+ }
1453
+ return result;
1454
+ }
1455
+ var refs = (scope) => BUILTIN_VARIABLES.filter((b) => b.scope === scope).map((b) => `{{${b.name}}}`).join(", ");
1456
+ var BUILTINS_HELP = `Built-in references Runeya fills itself \u2014 filled when the service is deployed, from the service that receives the variable: ${refs("server")}, the numbered {{service.PORT_2}} / {{service.URL_2}}\u2026 for its other ports, and {{services.<slug>.PORT}}, .URL, .BASE_URL, .NAME, .ID for a sibling service of the project (slug = its name in lowercase-dashes). Filled on the machine when the service starts: ${refs("agent")}.`;
1457
+ var CREATE_SERVICE_PARAMS = serviceParams(false);
1458
+ var UPDATE_SERVICE_PARAMS = serviceParams(true);
1459
+ var RUNEYA_CAPABILITIES = [
1460
+ {
1461
+ name: "list_services",
1462
+ transformOutput: maskServices,
1463
+ description: "List services, with the serviceId and agentId to use in other calls. Pass the projectId your context names to stay on the project the user is looking at, or another one from list_projects to look elsewhere. Called with no project, this can answer for every project of every organization on the machine \u2014 far more than the interface shows.",
1464
+ route: "service.list",
1465
+ method: "GET",
1466
+ params: [
1467
+ { name: "projectId", description: "Restrict to this project, from list_projects. Omit to list every service on the machine.", required: false },
1468
+ { name: "orgId", description: "With projectId: its organization, needed only when the same project id exists in several. Alone: list the services of this whole organization.", required: false }
1469
+ ],
1470
+ // `orgId` seul est une portée à part entière, que `service.list` sait filtrer :
1471
+ // l'omettre rendait toute la machine.
1472
+ buildInput: (p) => {
1473
+ if (p.projectId) return { projectId: p.projectId, ...p.orgId ? { orgId: p.orgId } : {} };
1474
+ return p.orgId ? { orgId: p.orgId } : void 0;
1475
+ }
1476
+ },
1477
+ {
1478
+ name: "get_service_status",
1479
+ description: "Get the current status (running, stopped, etc.) of a service. A service that has never been started on this agent has no process yet, and this answers 404 Process not found \u2014 that means STOPPED, not forbidden and not missing. Start it, or read its configuration with get_service.",
1480
+ route: "process.get",
1481
+ method: "GET",
1482
+ params: [
1483
+ { name: "serviceId", description: "The service ID", required: true },
1484
+ { name: "agentId", description: "The agent ID hosting this service", required: true }
1485
+ ],
1486
+ buildInput: (p) => ({ agentId: p.agentId, id: p.serviceId })
1487
+ },
1488
+ {
1489
+ name: "get_logs",
1490
+ description: "Get the recent buffered logs for a service.",
1491
+ route: "process.logs",
1492
+ method: "GET",
1493
+ params: [
1494
+ { name: "serviceId", description: "The service ID", required: true },
1495
+ { name: "agentId", description: "The agent ID hosting this service", required: true }
1496
+ ],
1497
+ buildInput: (p) => ({ agentId: p.agentId, processId: p.serviceId })
1498
+ },
1499
+ {
1500
+ name: "start_service",
1501
+ description: "Start a service on an agent.",
1502
+ route: "process.start",
1503
+ method: "POST",
1504
+ params: [
1505
+ { name: "serviceId", description: "The service ID to start", required: true },
1506
+ { name: "agentId", description: "The agent ID hosting this service", required: true }
1507
+ ],
1508
+ buildInput: (p) => ({ agentId: p.agentId, processId: p.serviceId }),
1509
+ destructive: true
1510
+ },
1511
+ {
1512
+ name: "stop_service",
1513
+ description: "Stop a running service on an agent.",
1514
+ route: "process.stop",
1515
+ method: "POST",
1516
+ params: [
1517
+ { name: "serviceId", description: "The service ID to stop", required: true },
1518
+ { name: "agentId", description: "The agent ID hosting this service", required: true }
1519
+ ],
1520
+ buildInput: (p) => ({ agentId: p.agentId, processId: p.serviceId }),
1521
+ destructive: true
1522
+ },
1523
+ {
1524
+ name: "restart_service",
1525
+ description: "Restart a service on an agent (stop \u2192 redeploy \u2192 start).",
1526
+ route: "process.restart",
1527
+ method: "POST",
1528
+ params: [
1529
+ { name: "serviceId", description: "The service ID to restart", required: true },
1530
+ { name: "agentId", description: "The agent ID hosting this service", required: true }
1531
+ ],
1532
+ buildInput: (p) => ({ agentId: p.agentId, processId: p.serviceId }),
1533
+ destructive: true
1534
+ },
1535
+ // ─── Utility ─────────────────────────────────────────────────────────────
1536
+ {
1537
+ name: "wait",
1538
+ description: "Wait for a given number of milliseconds before continuing.",
1539
+ route: "utility.wait",
1540
+ method: "POST",
1541
+ params: [
1542
+ { name: "ms", description: "Number of MILLISECONDS to wait. Convert from seconds: 1s=1000, 10s=10000, 1min=60000. Max 300000.", required: true }
1543
+ ],
1544
+ buildInput: (p) => ({ ms: Number(p.ms) }),
1545
+ usageHint: '"wait 5s" \u2192 wait(ms=5000), "pause 10 seconds" \u2192 wait(ms=10000), "attends 3 secondes" \u2192 wait(ms=3000). Always convert to milliseconds.'
1546
+ },
1547
+ // ─── Kanban ───────────────────────────────────────────────────────────────
1548
+ {
1549
+ name: "kanban_list_boards",
1550
+ description: "List the kanban boards of the organization. Returns the organizationId actually queried, the boardId to use in the other kanban tools, plus column and card counts. Cards themselves are not included \u2014 use kanban_get_board for those. An empty list means that organization really has no board: a failure to reach the cloud is reported as an error instead, never as an empty list.",
1551
+ route: "kanban.listBoards",
1552
+ method: "GET",
1553
+ params: [
1554
+ { name: "includeArchived", description: 'Set to "true" to include archived boards. Default: false.', required: false },
1555
+ { name: "organizationId", description: "Which organization to read. Optional: needed only when the account belongs to several and the error message asks for it.", required: false }
1556
+ ],
1557
+ buildInput: (p) => ({
1558
+ includeArchived: p.includeArchived === "true",
1559
+ ...p.organizationId ? { organizationId: p.organizationId } : {}
1560
+ })
1561
+ },
1562
+ {
1563
+ name: "kanban_get_board",
1564
+ description: "Read one kanban board: its columns, and the cards in each (id, title, markdown body, assignees, comment count, and attachment metadata \u2014 use kanban_download_attachment to fetch the bytes of a file). Use the exact column names it returns when moving or adding a card.",
1565
+ route: "kanban.getBoard",
1566
+ method: "GET",
1567
+ params: [
1568
+ { name: "boardId", description: "The kanban board ID, from kanban_list_boards", required: true }
1569
+ ],
1570
+ buildInput: (p) => ({ boardId: p.boardId })
1571
+ },
1572
+ {
1573
+ name: "kanban_get_card_comments",
1574
+ description: "Read the comment thread of one kanban card, newest last: who wrote each comment (ai or user, with the person's name), when, its text, and its attachments. The comments in your task context are a snapshot taken when the task started \u2014 call this to see anything written since, whenever someone tells you a comment was added or kanban_get_board reports a higher commentCount than you have.",
1575
+ route: "kanban.getCardComments",
1576
+ method: "GET",
1577
+ params: [
1578
+ { name: "boardId", description: "The kanban board ID", required: true },
1579
+ { name: "cardId", description: "The card ID whose comments to read", required: true },
1580
+ { name: "limit", description: "How many of the most recent comments to return (1-200). Default: 50.", required: false }
1581
+ ],
1582
+ buildInput: (p) => ({
1583
+ boardId: p.boardId,
1584
+ cardId: p.cardId,
1585
+ ...p.limit ? { limit: Number(p.limit) } : {}
1586
+ }),
1587
+ usageHint: '"le support a ajout\xE9 un commentaire" / "regarde les nouveaux commentaires" \u2192 kanban_get_card_comments(boardId, cardId). Never answer from the snapshot in your context when fresh comments are mentioned.'
1588
+ },
1589
+ {
1590
+ name: "kanban_search_cards",
1591
+ description: 'Search kanban cards by free text, matched against their title, body, comments and the support conversations they follow (title, messages, internal notes, and the names of message authors) \u2014 accent- and case-insensitive. Archived cards are included. Searches every board of the organization unless boardId narrows it down. createdBy narrows to the cards someone created ("les cartes cr\xE9\xE9es par Isabelle"), alone or with a query.',
1592
+ route: "kanban.searchCards",
1593
+ method: "GET",
1594
+ params: [
1595
+ { name: "query", description: "Text to look for in card titles, bodies, comments and followed conversations. Optional when createdBy is given.", required: false },
1596
+ { name: "createdBy", description: "Only cards created by this author: a user id, or a name (accent- and case-insensitive, partial match).", required: false },
1597
+ { name: "boardId", description: "Restrict the search to this board. Omit to search all boards.", required: false },
1598
+ { name: "limit", description: "Maximum number of cards to return (1-100). Default: 25.", required: false },
1599
+ { name: "organizationId", description: "Which organization to search. Optional: needed only when the account belongs to several and the error message asks for it.", required: false }
1600
+ ],
1601
+ buildInput: (p) => ({
1602
+ ...p.query ? { query: p.query } : {},
1603
+ ...p.createdBy ? { createdBy: p.createdBy } : {},
1604
+ ...p.boardId ? { boardId: p.boardId } : {},
1605
+ ...p.limit ? { limit: Number(p.limit) } : {},
1606
+ ...p.organizationId ? { organizationId: p.organizationId } : {}
1607
+ })
1608
+ },
1609
+ {
1610
+ name: "kanban_download_attachment",
1611
+ description: "Download a kanban card attachment (image, PDF, any file) onto this machine and return its local path, so it can be opened with file-reading tools. Attachment ids come from the card context, from kanban_get_board, or from kanban_get_card_comments \u2014 a file attached to a comment downloads exactly like one attached to the card.",
1612
+ route: "kanban.downloadAttachment",
1613
+ method: "POST",
1614
+ params: [
1615
+ { name: "attachmentId", description: "The attachment ID, as listed on the card", required: true },
1616
+ { name: "boardId", description: "The board the attachment comes from. Pass it whenever you know it: on a machine linked to several cloud accounts, it is what says which one holds the file.", required: false },
1617
+ { name: "organizationId", description: "Which organization the attachment belongs to. Optional: needed only when neither the board id is known nor the machine has a single account.", required: false }
1618
+ ],
1619
+ buildInput: (p) => ({
1620
+ attachmentId: p.attachmentId,
1621
+ ...p.boardId ? { boardId: p.boardId } : {},
1622
+ ...p.organizationId ? { orgId: p.organizationId } : {}
1623
+ })
1624
+ },
1625
+ {
1626
+ name: "kanban_move_card",
1627
+ description: "Move a kanban card to another column. Use the exact column name.",
1628
+ route: "kanban.moveCard",
1629
+ method: "POST",
1630
+ params: [
1631
+ { name: "boardId", description: "The kanban board ID", required: true },
1632
+ { name: "cardId", description: "The card ID to move", required: true },
1633
+ { name: "targetColumn", description: "Exact name of the target column", required: true }
1634
+ ],
1635
+ buildInput: (p) => ({ boardId: p.boardId, cardId: p.cardId, targetColumn: p.targetColumn })
1636
+ },
1637
+ {
1638
+ name: "kanban_add_comment",
1639
+ description: "Add a comment to a kanban card.",
1640
+ route: "kanban.addComment",
1641
+ method: "POST",
1642
+ params: [
1643
+ { name: "boardId", description: "The kanban board ID", required: true },
1644
+ { name: "cardId", description: "The card ID", required: true },
1645
+ { name: "comment", description: "The comment text (markdown supported)", required: true }
1646
+ ],
1647
+ buildInput: (p) => ({ boardId: p.boardId, cardId: p.cardId, comment: p.comment })
1648
+ },
1649
+ {
1650
+ name: "kanban_get_card_conversations",
1651
+ description: "Read the customer support conversations (Intercom\u2026) linked to one kanban card: for each, its title, provider, state (open/resolved), link, the opening message and the thread of messages (author name, customer or team, internal note or not, date, text, attached files \u2014 fetch their bytes with kanban_download_attachment), plus the other cards following the same conversation. These messages are NOT comments of the card \u2014 read them here.",
1652
+ route: "kanban.getCardConversations",
1653
+ method: "GET",
1654
+ params: [
1655
+ { name: "boardId", description: "The kanban board ID", required: true },
1656
+ { name: "cardId", description: "The card ID whose conversations to read", required: true },
1657
+ { name: "limit", description: "How many of the most recent messages to return per conversation (1-500). Default: 100.", required: false }
1658
+ ],
1659
+ buildInput: (p) => ({
1660
+ boardId: p.boardId,
1661
+ cardId: p.cardId,
1662
+ ...p.limit ? { limit: Number(p.limit) } : {}
1663
+ }),
1664
+ usageHint: `"regarde la conversation Intercom de la carte" / "qu'a dit le client" \u2192 kanban_get_card_conversations(boardId, cardId).`
1665
+ },
1666
+ {
1667
+ name: "kanban_search_conversations",
1668
+ description: "Search customer support conversations (Intercom\u2026) at the providers connected to the board's organization, by words of the conversation title, or by conversation number or URL. Read-only at the provider. Each result gives connectionId and externalId (needed to link it), title, state, a preview and the cards already following it.",
1669
+ route: "kanban.searchConversations",
1670
+ method: "GET",
1671
+ params: [
1672
+ { name: "boardId", description: "Any kanban board ID of the organization to search in", required: true },
1673
+ { name: "query", description: "Words of the conversation title, a conversation number, or a conversation URL", required: true }
1674
+ ],
1675
+ buildInput: (p) => ({ boardId: p.boardId, query: p.query })
1676
+ },
1677
+ {
1678
+ name: "kanban_link_conversation",
1679
+ description: "Link a customer support conversation found with kanban_search_conversations to a kanban card. Its whole thread is imported and new messages keep arriving on the card. A conversation can be linked to several cards. Nothing is written at the provider.",
1680
+ route: "kanban.linkConversation",
1681
+ method: "POST",
1682
+ params: [
1683
+ { name: "boardId", description: "The kanban board ID of the card", required: true },
1684
+ { name: "cardId", description: "The card to link the conversation to", required: true },
1685
+ { name: "connectionId", description: "The connectionId of the search result", required: true },
1686
+ { name: "externalId", description: "The externalId of the search result", required: true }
1687
+ ],
1688
+ buildInput: (p) => ({ boardId: p.boardId, cardId: p.cardId, connectionId: p.connectionId, externalId: p.externalId })
1689
+ },
1690
+ {
1691
+ name: "kanban_get_card_links",
1692
+ description: "Read the filiation of a board: which card depends on which. Returns one entry per link (child card \u2194 parent card), plus the title, board, column, assignees and comment count of any linked card living on another board. Use it when a card context says it has a parent or sub-cards and you need their ids to read them.",
1693
+ route: "kanban.getCardLinks",
1694
+ method: "GET",
1695
+ params: [
1696
+ { name: "boardId", description: "The kanban board ID whose links to read", required: true }
1697
+ ],
1698
+ buildInput: (p) => ({ boardId: p.boardId })
1699
+ },
1700
+ {
1701
+ name: "kanban_set_card_parent",
1702
+ description: "Link a kanban card to a parent card, or detach it. The parent is the card the other one depends on \u2014 an epic and its tasks, a bug and its follow-ups. A card has at most one parent, and setting a new one replaces it. Omit parentCardId to detach. kanban_get_board reports the current filiation as parentCardId / childCardIds on each card.",
1703
+ route: "kanban.setCardParent",
1704
+ method: "POST",
1705
+ params: [
1706
+ { name: "boardId", description: "The kanban board ID of the card being linked", required: true },
1707
+ { name: "cardId", description: "The card that depends on the parent (the child)", required: true },
1708
+ { name: "parentCardId", description: "The parent card ID. Omit to detach the card from its parent.", required: false },
1709
+ { name: "parentBoardId", description: "The parent's board, when it lives on another board of the same organization. Defaults to boardId.", required: false }
1710
+ ],
1711
+ buildInput: (p) => ({
1712
+ boardId: p.boardId,
1713
+ cardId: p.cardId,
1714
+ ...p.parentCardId ? { parentCardId: p.parentCardId } : {},
1715
+ ...p.parentBoardId ? { parentBoardId: p.parentBoardId } : {}
1716
+ })
1717
+ },
1718
+ {
1719
+ name: "kanban_place_card",
1720
+ description: "Place an existing kanban card on another board of the same organization, without copying it \u2014 a reference. The card stays on its origin board; the target board gives it a column of its own. Editing, commenting, archiving or deleting the card from either board acts on the same card; moving it on the target board only moves the reference. Use it to show an issue from an issue list on a team kanban, for instance.",
1721
+ route: "kanban.placeCard",
1722
+ method: "POST",
1723
+ params: [
1724
+ { name: "boardId", description: "The board to place the card on (not its own board)", required: true },
1725
+ { name: "cardId", description: "The card to place", required: true },
1726
+ { name: "column", description: "The column on that board: its ID or its title", required: true }
1727
+ ],
1728
+ buildInput: (p) => ({ boardId: p.boardId, cardId: p.cardId, column: p.column })
1729
+ },
1730
+ {
1731
+ name: "kanban_remove_card_placement",
1732
+ description: "Remove a card from a board where it was placed as a reference. The card itself is untouched and stays on its origin board.",
1733
+ route: "kanban.unplaceCard",
1734
+ method: "POST",
1735
+ params: [
1736
+ { name: "boardId", description: "The board the reference is on", required: true },
1737
+ { name: "cardId", description: "The card to remove from that board", required: true }
1738
+ ],
1739
+ buildInput: (p) => ({ boardId: p.boardId, cardId: p.cardId })
1740
+ },
1741
+ {
1742
+ name: "kanban_get_card_placements",
1743
+ description: "List where a kanban card is: its origin board (home: true), then every board it is placed on as a reference, with the column on each. kanban_get_board flags a referenced card with an `origin` field naming its origin board.",
1744
+ route: "kanban.getCardPlacements",
1745
+ method: "GET",
1746
+ params: [
1747
+ { name: "boardId", description: "Any board the card is on", required: true },
1748
+ { name: "cardId", description: "The card", required: true }
1749
+ ],
1750
+ buildInput: (p) => ({ boardId: p.boardId, cardId: p.cardId })
1751
+ },
1752
+ {
1753
+ name: "kanban_add_card",
1754
+ description: "Add a new card to a kanban column.",
1755
+ route: "kanban.addCard",
1756
+ method: "POST",
1757
+ params: [
1758
+ { name: "boardId", description: "The kanban board ID", required: true },
1759
+ { name: "column", description: "Exact name of the target column", required: true },
1760
+ { name: "title", description: "Card title", required: true },
1761
+ { name: "content", description: "Card content in markdown", required: false }
1762
+ ],
1763
+ buildInput: (p) => ({ boardId: p.boardId, column: p.column, title: p.title, content: p.content })
1764
+ },
1765
+ {
1766
+ name: "kanban_get_card",
1767
+ description: "Read one kanban card by id, including archived ones. Cheaper than fetching the whole board, and the only way to read a card that has been archived.",
1768
+ route: "kanban.getCard",
1769
+ method: "GET",
1770
+ params: [
1771
+ { name: "boardId", description: "The kanban board ID", required: true },
1772
+ { name: "cardId", description: "The card ID", required: true }
1773
+ ],
1774
+ buildInput: (p) => ({ boardId: p.boardId, cardId: p.cardId })
1775
+ },
1776
+ {
1777
+ name: "kanban_get_card_context",
1778
+ description: "Load the working context of a kanban card: the card, the board's columns, the team's guidelines for the board and the expected ending (move the card if needed, then comment). Call it first when the user asks you to work on a card, then follow it. It is the context a session started from the card in Runeya receives.",
1779
+ route: "kanban.cardContext",
1780
+ method: "GET",
1781
+ params: [
1782
+ { name: "boardId", description: "The kanban board ID", required: true },
1783
+ { name: "cardId", description: "The card ID", required: true },
1784
+ { name: "projectId", description: "The project the work happens in, named in the context. Optional.", required: false },
1785
+ { name: "orgId", description: "Its organization. Optional.", required: false }
1786
+ ],
1787
+ buildInput: (p) => ({
1788
+ boardId: p.boardId,
1789
+ cardId: p.cardId,
1790
+ ...p.projectId ? { projectId: p.projectId } : {},
1791
+ ...p.orgId ? { orgId: p.orgId } : {}
1792
+ })
1793
+ },
1794
+ {
1795
+ name: "kanban_update_card",
1796
+ description: "Update an existing kanban card. Only the fields you pass are changed; passing null to priority, color or deadline clears it. Prefer this over creating a second card when something needs correcting.",
1797
+ route: "kanban.updateCard",
1798
+ method: "POST",
1799
+ params: [
1800
+ { name: "boardId", description: "The kanban board ID", required: true },
1801
+ { name: "cardId", description: "The card ID", required: true },
1802
+ { name: "title", description: "New title", required: false },
1803
+ { name: "content", description: "New card content, in markdown", required: false },
1804
+ { name: "priority", description: "Priority from 1 to 10, or null to clear it", required: false },
1805
+ { name: "color", description: "Card colour, or null to clear it", required: false },
1806
+ { name: "groups", description: "Labels, as a list of strings. Replaces the current ones.", required: false },
1807
+ { name: "deadline", description: "Due date as YYYY-MM-DD, or null to clear it", required: false },
1808
+ { name: "assignees", description: "User ids to assign, as a list. Replaces the current ones; use kanban_list_members to find the ids.", required: false }
1809
+ ],
1810
+ buildInput: (p) => ({
1811
+ boardId: p.boardId,
1812
+ cardId: p.cardId,
1813
+ ...p.title !== void 0 ? { title: p.title } : {},
1814
+ ...p.content !== void 0 ? { content: p.content } : {},
1815
+ // Tout arrive en chaîne : un modèle n'écrit pas des nombres, des listes
1816
+ // ni des `null` typés. `clearable` distingue « efface » de « ne touche
1817
+ // pas » — sans quoi vider une couleur serait impossible à exprimer, et
1818
+ // changer un titre risquerait de vider le reste.
1819
+ ...clearable(p.priority, (v) => Number(v), "priority"),
1820
+ ...clearable(p.color, (v) => v, "color"),
1821
+ ...clearable(p.deadline, (v) => v, "deadline"),
1822
+ ...p.groups !== void 0 ? { groups: toList(p.groups) } : {},
1823
+ ...p.assignees !== void 0 ? { assignees: toList(p.assignees) } : {}
1824
+ })
1825
+ },
1826
+ {
1827
+ name: "kanban_set_card_archived",
1828
+ description: "Archive a kanban card (it leaves the board but is not deleted) or bring it back. An unarchived card returns to the column it left.",
1829
+ route: "kanban.setCardArchived",
1830
+ method: "POST",
1831
+ params: [
1832
+ { name: "boardId", description: "The kanban board ID", required: true },
1833
+ { name: "cardId", description: "The card ID", required: true },
1834
+ { name: "archived", description: "true to archive, false to bring the card back", required: true }
1835
+ ],
1836
+ buildInput: (p) => ({ boardId: p.boardId, cardId: p.cardId, archived: p.archived === "true" })
1837
+ },
1838
+ {
1839
+ name: "kanban_list_members",
1840
+ description: "List the people of the organization who can be assigned to a card, with their ids. Needed before assigning anyone with kanban_update_card.",
1841
+ route: "kanban.listMembers",
1842
+ method: "GET",
1843
+ params: [
1844
+ { name: "organizationId", description: "Which organization. Optional: needed only when the account belongs to several and the error message asks for it.", required: false }
1845
+ ],
1846
+ buildInput: (p) => p.organizationId ? { organizationId: p.organizationId } : {}
1847
+ },
1848
+ // ─── Lecture seule ───────────────────────────────────────────────────────
1849
+ //
1850
+ // Tout ce qui se consulte sans rien changer. Les écritures s'ajoutent au cas
1851
+ // par cas : aujourd'hui les services et leurs variables, plus bas.
1852
+ //
1853
+ // Volontairement absentes, et à ne pas ajouter sans y regarder à deux fois,
1854
+ // parce qu'elles rendent un secret : `environment.list` / `get` / `resolve` /
1855
+ // `resolveFlat` (un `Environment` porte ses variables, valeurs comprises),
1856
+ // `apiKeys.list` / `reveal`, `setup.getEncryptionKeyHex` et `getOrgKeys`
1857
+ // (clés de chiffrement), `settings.get` et `settings.listAIProviders` (clés
1858
+ // de fournisseurs d'IA — `settings.getPaths` et `getNetwork`, eux, ne
1859
+ // rendent que des chemins et une adresse, d'où leur présence ci-dessous),
1860
+ // `cloudAuth.session`, `agent.getPassthroughEnv`. Un agent n'a pas besoin de
1861
+ // ces valeurs pour travailler, et ce qu'il lit finit dans une transcription.
1862
+ //
1863
+ // La seule exception est `service.getEnv` (`get_service_env` ci-dessous),
1864
+ // et elle n'en est une que parce qu'elle filtre : les variables publiques, et
1865
+ // les seuls secrets que leur auteur a partagés avec les IA (`isSharedWithAi`
1866
+ // — « Lisible par l'IA », ou l'injection, qui vaut la lecture). Un secret non
1867
+ // partagé n'y apparaît pas, pas même par son nom.
1868
+ {
1869
+ name: "list_processes",
1870
+ description: "List every process across all agents with its current state (running, stopped, exit code, uptime). Use this to see the whole machine at once, rather than calling get_service_status service by service. An agent only learns about a service when it is first started, so this lists what is deployed, not what is configured: an empty list means nothing has been started, not that you cannot see them \u2014 list_services shows what exists.",
1871
+ route: "process.list",
1872
+ method: "GET",
1873
+ params: [],
1874
+ buildInput: () => void 0
1875
+ },
1876
+ {
1877
+ name: "get_service",
1878
+ transformOutput: maskServices,
1879
+ description: "Read the full configuration of one service: its commands, ports, groups, runner, log sources and restart policy. list_services gives the ids.",
1880
+ route: "service.get",
1881
+ method: "GET",
1882
+ params: [
1883
+ { name: "serviceId", description: "The service ID", required: true }
1884
+ ],
1885
+ buildInput: (p) => ({ id: p.serviceId })
1886
+ },
1887
+ {
1888
+ name: "get_service_env",
1889
+ description: 'Read the environment variables a service runs with, and the project\'s variable inventory. `env` lists every key the service receives; a value reads "***" when it is a secret whose author did not share it with AI (sharing = ticking "AI read", or injecting it into the agents\' environment, which grants reading too). `envSecrets` says which keys are secret, `hiddenKeys` which ones are masked. A masked key is set on the service: never treat it as missing, never suggest adding it, and never pass "***" as a value \u2014 ask the user for it. `variables` describes every variable of the project \u2014 which environment defines it and its scope, its description, its flags, whether this machine overrides it, and `inService: false` for the ones this service does not receive. Use it to say where a variable must be changed instead of guessing. On an overridden variable, `sharedValue` is what the machine-local override replaces (the team\'s value, masked as "***" when unreadable); no other value appears there. This is the resolver: the values are what the service would receive \u2014 inheritance, {{environment.KEY}} references, this machine\'s overrides and server built-ins applied; agent built-ins stay as written until the service starts. Use it to check a variable you just composed. `unresolvedReferences` lists references nothing will fill (a typo, a variable that does not exist): fix them.',
1890
+ route: "service.getEnv",
1891
+ method: "GET",
1892
+ params: [
1893
+ { name: "serviceId", description: "The service ID", required: true },
1894
+ { name: "environmentId", description: "A global environment of the project to resolve in instead of the active one, without activating it (from list_environments). Omit for the active one.", required: false }
1895
+ ],
1896
+ buildInput: (p) => ({ id: p.serviceId, ...p.environmentId ? { environmentId: p.environmentId } : {} })
1897
+ },
1898
+ // ─── Écrire un service et ses variables ────────────────────────────────────
1899
+ //
1900
+ // Les variables s'écrivent à l'aveugle (`environment.setVariableBlind`) : une
1901
+ // IA remplace la valeur d'un secret sans la lire, et ne peut ni le rendre
1902
+ // public, ni le partager avec les IA. La règle tient côté serveur, dans la
1903
+ // route ; ces outils ne passent jamais par une route qui renvoie des valeurs
1904
+ // (`upsertVariable`, `removeVariable`, `update` rendent l'environnement entier).
1905
+ {
1906
+ name: "create_service",
1907
+ transformOutput: maskServices,
1908
+ description: "Create a service, native or Docker, in a project: it joins the project and gets its environments in the same call. Pass the projectId your context names. Projects are synced between machines, so write paths relative to the project root and put machine-specific values (a device serial, a local token) in environment variables, referenced as $NAME in commands \u2014 see the cwd, commands and shortcuts parameters. Then set its variables with set_environment_variable.",
1909
+ route: "service.create",
1910
+ method: "POST",
1911
+ params: [
1912
+ { name: "name", description: "The service name", required: true },
1913
+ { name: "projectId", description: "The project it joins: the one your context names, or another from list_projects. Without it, the service belongs to no project.", required: false },
1914
+ { name: "orgId", description: "Its organization. Needed only when the same project id exists in several.", required: false },
1915
+ ...CREATE_SERVICE_PARAMS
1916
+ ],
1917
+ buildInput: (p) => {
1918
+ const fields = serviceFields(p, CREATE_SERVICE_PARAMS);
1919
+ if (fields["runner"] === "docker" && !fields["dockerConfig"]) {
1920
+ throw new Error('dockerConfig: required with runner "docker" \u2014 give an "image" or a "dockerfile"');
1921
+ }
1922
+ return {
1923
+ name: p.name,
1924
+ ...fields,
1925
+ ...p.projectId ? { projectId: p.projectId } : {},
1926
+ ...p.orgId ? { orgId: p.orgId } : {}
1927
+ };
1928
+ }
1929
+ },
1930
+ {
1931
+ name: "add_service_to_project",
1932
+ description: "Add an existing service to a project, and create its environments \u2014 one for each of the project's environments, where its own variables go. Not needed after create_service, which already does it; calling it again changes nothing.",
1933
+ route: "project.addService",
1934
+ method: "POST",
1935
+ params: [
1936
+ { name: "projectId", description: "The project ID, from list_projects", required: true },
1937
+ { name: "serviceId", description: "The service ID", required: true },
1938
+ { name: "orgId", description: "Its organization. Needed only when the same project id exists in several.", required: false }
1939
+ ],
1940
+ buildInput: (p) => ({ projectId: p.projectId, serviceId: p.serviceId, ...p.orgId ? { orgId: p.orgId } : {} })
1941
+ },
1942
+ {
1943
+ name: "update_service",
1944
+ transformOutput: maskServices,
1945
+ description: "Change any part of a service configuration \u2014 commands, ports, health check, restart policy, shortcuts, log sources, whether it runs natively or in Docker (runner, dockerConfig)\u2026 Only the fields you pass change; a list or an object you pass replaces the current one whole. Read the service first with get_service.",
1946
+ route: "service.update",
1947
+ method: "POST",
1948
+ params: [
1949
+ { name: "serviceId", description: "The service ID", required: true },
1950
+ { name: "name", description: "New name", required: false },
1951
+ { name: "orgId", description: "Move the service to this organization vault. Leave unset unless asked.", required: false, schema: z23.string().max(128).nullable() },
1952
+ ...UPDATE_SERVICE_PARAMS
1953
+ ],
1954
+ buildInput: (p) => ({
1955
+ id: p.serviceId,
1956
+ ...p.name ? { name: p.name } : {},
1957
+ ...p.orgId !== void 0 ? { orgId: parseTyped("orgId", z23.string().max(128).nullable(), p.orgId) } : {},
1958
+ ...serviceFields(p, UPDATE_SERVICE_PARAMS)
1959
+ })
1960
+ },
1961
+ {
1962
+ name: "list_environments",
1963
+ description: "List the environments of a project, with no value: each one's id, name and scope (global = shared by the whole project, service = one service's own variables), which global environment is active, which service a service environment belongs to and which global environment activates it, its parents (it inherits their variables), `isolated` when a service environment does not inherit from its global one, and the variables it defines with their flags. Empty environments are listed too \u2014 a service you just created has one per global environment, where its own variables go. Use it to find the environmentId before set_environment_variable.",
1964
+ route: "environment.listForAi",
1965
+ method: "GET",
1966
+ params: [
1967
+ { name: "projectId", description: "The project ID: the one your context names, or another from list_projects", required: true },
1968
+ { name: "orgId", description: "Its organization. Needed only when the same project id exists in several.", required: false }
1969
+ ],
1970
+ buildInput: (p) => ({ projectId: p.projectId, ...p.orgId ? { orgId: p.orgId } : {} })
1971
+ },
1972
+ {
1973
+ name: "resolve_environment",
1974
+ description: "Resolve one environment alone \u2014 its variables with those inherited from its parents and this machine's overrides \u2014 as an AI may read it: secrets not shared with AI read \"***\". Built-in references ({{service.*}}, {{services.*}}, {{agent.*}}\u2026) are NOT filled, since they depend on the service that receives the variable; use get_service_env for a service's final values. `unresolvedReferences` lists references nothing will fill: a misspelled {{environment.KEY}}, or a name that is no built-in.",
1975
+ route: "environment.resolveForAi",
1976
+ method: "GET",
1977
+ params: [
1978
+ { name: "environmentId", description: "The environment, from list_environments", required: true }
1979
+ ],
1980
+ buildInput: (p) => ({ environmentId: p.environmentId })
1981
+ },
1982
+ {
1983
+ name: "set_environment_variable",
1984
+ description: `Create or change an environment variable, by name, in one environment \u2014 global or service, same rules for both. Find the environmentId with list_environments. Where it belongs: a value shared by several services (a database host, an API base URL) goes ONCE in the active global environment; services inherit it. A value only one service needs goes in that service's environment (scope service, activated by the active global one), where it also overrides a global variable of the same name. Never hardcode what Runeya already knows: a service's own port is {{service.PORT}}, its own URL {{service.URL}}, another service's address {{services.<slug>.URL}} \u2014 a copied "3210" or "http://localhost:3211" breaks as soon as a port changes or the project runs on another machine. If the port a variable needs is not declared on its service, declare it there (update_service, ports) instead of writing the number. When a written value contains a port or URL of a project service, the response lists \`suggestions\`: rewrite the value with those references. Compose instead of copying: a value can reference another variable of the resolved environment as {{environment.KEY}} (e.g. "postgres://{{environment.DB_USER}}@{{environment.DB_HOST}}:5432"). ${BUILTINS_HELP} After writing, check the result with get_service_env (a service, optionally in another global environment) or resolve_environment (an environment alone). References are substituted in one pass: a reference to a variable whose own value is only a reference may stay unexpanded, so point at the variable that holds the actual value. $NAME is not a reference here: it only expands inside a shell command. pre and post are concatenated around the value, each resolved separately. You can replace a secret's value without reading it, and create a variable as secret. You cannot make a secret public nor make it readable by AI: that stays the user's decision. Never pass "***" as a value. The response never contains the value.`,
1985
+ route: "environment.setVariableBlind",
1986
+ method: "POST",
1987
+ params: [
1988
+ { name: "environmentId", description: "The environment that holds the variable, from list_environments", required: true },
1989
+ { name: "key", description: "The variable name, e.g. DATABASE_URL", required: true },
1990
+ { name: "value", description: "The new value, possibly with {{environment.KEY}} references. Omit to leave it unchanged.", required: false },
1991
+ { name: "pre", description: 'Text put before the value (e.g. "https://"). Omit to leave it unchanged.', required: false },
1992
+ { name: "post", description: 'Text put after the value (e.g. "/api"). Omit to leave it unchanged.', required: false },
1993
+ { name: "isSecret", description: '"true" to store it as a secret. On an existing public variable, makes it secret.', required: false },
1994
+ { name: "description", description: "What the variable is for, in markdown", required: false }
1995
+ ],
1996
+ buildInput: (p) => ({
1997
+ environmentId: p.environmentId,
1998
+ key: p.key,
1999
+ ...p.value !== void 0 ? { value: p.value } : {},
2000
+ ...p.pre !== void 0 ? { pre: p.pre } : {},
2001
+ ...p.post !== void 0 ? { post: p.post } : {},
2002
+ ...p.isSecret !== void 0 ? { isSecret: toBool(p.isSecret) } : {},
2003
+ ...p.description !== void 0 ? { description: p.description } : {}
2004
+ })
2005
+ },
2006
+ {
2007
+ name: "set_environment_inheritance",
2008
+ description: "Switch whether a service environment inherits the variables of the global environment that activates it (the default), or is isolated from it and receives only its own variables. list_environments shows this as `isolated`. Isolating a service is unusual: it loses every shared variable of the project. Do it only when the user asks, and never to work around a variable \u2014 override it in the service environment instead.",
2009
+ route: "environment.setInheritanceBlind",
2010
+ method: "POST",
2011
+ params: [
2012
+ { name: "environmentId", description: "A service environment (scope service), from list_environments", required: true },
2013
+ { name: "inherit", description: '"true" to inherit from the global environment, "false" to isolate', required: true, schema: z23.boolean() }
2014
+ ],
2015
+ buildInput: (p) => ({ environmentId: p.environmentId, inherit: toBool(p.inherit ?? "true") })
2016
+ },
2017
+ {
2018
+ name: "remove_environment_variable",
2019
+ description: "Delete an environment variable, by name, from one environment. Its value is lost.",
2020
+ route: "environment.removeVariableBlind",
2021
+ method: "POST",
2022
+ params: [
2023
+ { name: "environmentId", description: "The environment that holds the variable", required: true },
2024
+ { name: "key", description: "The variable name", required: true }
2025
+ ],
2026
+ buildInput: (p) => ({ environmentId: p.environmentId, key: p.key }),
2027
+ destructive: true
2028
+ },
2029
+ {
2030
+ name: "get_service_metrics",
2031
+ description: "Read the CPU and memory currently used by a running service.",
2032
+ route: "process.metrics",
2033
+ method: "GET",
2034
+ params: [
2035
+ { name: "serviceId", description: "The service ID", required: true },
2036
+ { name: "agentId", description: "The agent ID hosting this service", required: true }
2037
+ ],
2038
+ buildInput: (p) => ({ agentId: p.agentId, processId: p.serviceId })
2039
+ },
2040
+ {
2041
+ name: "list_agents",
2042
+ description: "List the agents connected to this Runeya, with their status and whether they run on this machine. Passphrases are never returned.",
2043
+ route: "agent.list",
2044
+ method: "GET",
2045
+ params: [],
2046
+ buildInput: () => void 0
2047
+ },
2048
+ {
2049
+ name: "list_projects",
2050
+ description: "List the projects known to this Runeya, with their ids and the services they hold.",
2051
+ route: "project.list",
2052
+ method: "GET",
2053
+ params: [],
2054
+ buildInput: () => void 0
2055
+ },
2056
+ {
2057
+ name: "get_project",
2058
+ description: "Read one project: its services, its active environment, its organization.",
2059
+ route: "project.get",
2060
+ method: "GET",
2061
+ params: [
2062
+ { name: "projectId", description: "The project ID, from list_projects", required: true },
2063
+ { name: "orgId", description: "Its organization. Needed only when the same project id exists in several.", required: false }
2064
+ ],
2065
+ buildInput: (p) => ({ id: p.projectId, ...p.orgId ? { orgId: p.orgId } : {} })
2066
+ },
2067
+ {
2068
+ name: "list_project_paths",
2069
+ description: "Where each project lives on this machine. Use it to find the directory to work in before reading or editing files.",
2070
+ route: "project.localPaths",
2071
+ method: "GET",
2072
+ params: [],
2073
+ buildInput: () => void 0
2074
+ },
2075
+ {
2076
+ name: "git_status",
2077
+ description: "Read the git working tree of a service or a project: staged, modified and untracked files, current branch, ahead/behind counts.",
2078
+ route: "git.status",
2079
+ method: "GET",
2080
+ params: [
2081
+ { name: "serviceId", description: "Target the repository of this service", required: false },
2082
+ { name: "projectId", description: "Target the repository of this project, when no service is given", required: false },
2083
+ { name: "agentId", description: "The agent hosting the repository", required: false },
2084
+ { name: "orgId", description: "Its organization, when a project id alone is ambiguous", required: false }
2085
+ ],
2086
+ buildInput: (p) => gitTarget(p)
2087
+ },
2088
+ {
2089
+ name: "git_log",
2090
+ description: "Read the recent commits of a service or project repository: hash, author, date, message.",
2091
+ route: "git.log",
2092
+ method: "GET",
2093
+ params: [
2094
+ { name: "serviceId", description: "Target the repository of this service", required: false },
2095
+ { name: "projectId", description: "Target the repository of this project, when no service is given", required: false },
2096
+ { name: "agentId", description: "The agent hosting the repository", required: false },
2097
+ { name: "orgId", description: "Its organization, when a project id alone is ambiguous", required: false },
2098
+ { name: "limit", description: "How many commits to return (1-200). Default: 50.", required: false }
2099
+ ],
2100
+ buildInput: (p) => ({ ...gitTarget(p), ...p.limit ? { limit: Number(p.limit) } : {} })
2101
+ },
2102
+ {
2103
+ name: "git_branches",
2104
+ description: "List the local and remote branches of a service or project repository.",
2105
+ route: "git.branches",
2106
+ method: "GET",
2107
+ params: [
2108
+ { name: "serviceId", description: "Target the repository of this service", required: false },
2109
+ { name: "projectId", description: "Target the repository of this project, when no service is given", required: false },
2110
+ { name: "agentId", description: "The agent hosting the repository", required: false },
2111
+ { name: "orgId", description: "Its organization, when a project id alone is ambiguous", required: false }
2112
+ ],
2113
+ buildInput: (p) => gitTarget(p)
2114
+ },
2115
+ {
2116
+ name: "git_diff",
2117
+ description: "Read the diff of a service or project repository \u2014 everything, or one file. Use it to see what changed before describing or reviewing it.",
2118
+ route: "git.diff",
2119
+ method: "GET",
2120
+ params: [
2121
+ { name: "serviceId", description: "Target the repository of this service", required: false },
2122
+ { name: "projectId", description: "Target the repository of this project, when no service is given", required: false },
2123
+ { name: "agentId", description: "The agent hosting the repository", required: false },
2124
+ { name: "orgId", description: "Its organization, when a project id alone is ambiguous", required: false },
2125
+ { name: "path", description: "Restrict the diff to this file path", required: false },
2126
+ { name: "staged", description: 'Set to "true" to read the staged diff instead of the working tree', required: false },
2127
+ { name: "untracked", description: 'Set to "true" to include untracked files', required: false }
2128
+ ],
2129
+ buildInput: (p) => ({
2130
+ ...gitTarget(p),
2131
+ ...p.path ? { path: p.path } : {},
2132
+ ...p.staged !== void 0 ? { staged: p.staged === "true" } : {},
2133
+ ...p.untracked !== void 0 ? { untracked: p.untracked === "true" } : {}
2134
+ })
2135
+ },
2136
+ {
2137
+ name: "list_docs",
2138
+ description: "List the markdown documentation files of a scope, as a tree. Pair it with read_doc to answer from the project's own docs rather than from guesswork.",
2139
+ route: "wiki.list",
2140
+ method: "GET",
2141
+ params: [
2142
+ { name: "scope", description: `"global" for this Runeya, or "service:<serviceId>" for one service's repository`, required: true }
2143
+ ],
2144
+ buildInput: (p) => ({ scope: p.scope })
2145
+ },
2146
+ {
2147
+ name: "read_doc",
2148
+ description: "Read one markdown documentation file. Paths come from list_docs; only .md and .markdown can be read.",
2149
+ route: "wiki.read",
2150
+ method: "GET",
2151
+ params: [
2152
+ { name: "scope", description: `"global" for this Runeya, or "service:<serviceId>" for one service's repository`, required: true },
2153
+ { name: "path", description: "Relative path of the file, as returned by list_docs", required: true }
2154
+ ],
2155
+ buildInput: (p) => ({ scope: p.scope, path: p.path })
2156
+ },
2157
+ {
2158
+ name: "get_server_logs",
2159
+ description: "Read Runeya's own recent log entries. Use it to explain why Runeya itself misbehaved \u2014 not to read a service's output, which is get_logs.",
2160
+ route: "diagnostics.recentLogs",
2161
+ method: "GET",
2162
+ params: [
2163
+ { name: "limit", description: "How many entries to return (1-2000). Default: 500.", required: false }
2164
+ ],
2165
+ buildInput: (p) => p.limit ? { limit: Number(p.limit) } : void 0
2166
+ },
2167
+ {
2168
+ name: "get_kanban_queue_state",
2169
+ description: "Read the state of the kanban task queue: what is running, what is waiting. Use it before queuing work, or to explain why a card has not started.",
2170
+ route: "kanbanQueue.getState",
2171
+ method: "GET",
2172
+ params: [],
2173
+ buildInput: () => void 0
2174
+ },
2175
+ {
2176
+ name: "export_project",
2177
+ description: "Read a project and every service it holds in one call, with secrets stripped out. Cheaper and safer than calling get_service for each service \u2014 prefer it whenever you need the whole picture of a project.",
2178
+ route: "project.export",
2179
+ method: "GET",
2180
+ params: [
2181
+ { name: "projectId", description: "The project ID, from list_projects", required: true },
2182
+ { name: "orgId", description: "Its organization. Needed only when the same project id exists in several.", required: false }
2183
+ ],
2184
+ buildInput: (p) => ({ id: p.projectId, ...p.orgId ? { orgId: p.orgId } : {} })
2185
+ },
2186
+ {
2187
+ name: "git_remotes",
2188
+ description: "List the git remotes of a service or project repository, with their URLs.",
2189
+ route: "git.remotes",
2190
+ method: "GET",
2191
+ params: [
2192
+ { name: "serviceId", description: "Target the repository of this service", required: false },
2193
+ { name: "projectId", description: "Target the repository of this project, when no service is given", required: false },
2194
+ { name: "agentId", description: "The agent hosting the repository", required: false },
2195
+ { name: "orgId", description: "Its organization, when a project id alone is ambiguous", required: false }
2196
+ ],
2197
+ buildInput: (p) => gitTarget(p)
2198
+ },
2199
+ {
2200
+ name: "git_stash_list",
2201
+ description: "List the stashes of a service or project repository. Useful before assuming work was lost: it may simply be stashed.",
2202
+ route: "git.stashList",
2203
+ method: "GET",
2204
+ params: [
2205
+ { name: "serviceId", description: "Target the repository of this service", required: false },
2206
+ { name: "projectId", description: "Target the repository of this project, when no service is given", required: false },
2207
+ { name: "agentId", description: "The agent hosting the repository", required: false },
2208
+ { name: "orgId", description: "Its organization, when a project id alone is ambiguous", required: false }
2209
+ ],
2210
+ buildInput: (p) => gitTarget(p)
2211
+ },
2212
+ {
2213
+ name: "get_runeya_version",
2214
+ description: "Read the version of Runeya running here, the latest published one, and whether this install is up to date. Use it before blaming a bug on the code: the machine may simply be behind.",
2215
+ route: "health.versionInfo",
2216
+ method: "GET",
2217
+ params: [],
2218
+ buildInput: () => void 0
2219
+ },
2220
+ {
2221
+ name: "get_runeya_paths",
2222
+ description: "Read where Runeya keeps its things on this machine: its launch directory, its data directory, and the machine-wide directory holding cloud projects.",
2223
+ route: "settings.getPaths",
2224
+ method: "GET",
2225
+ params: [],
2226
+ buildInput: () => void 0
2227
+ },
2228
+ {
2229
+ name: "get_network_info",
2230
+ description: "Read where this Runeya listens (host and port) and whether it is reachable from the local network, with the URLs to use. Use it to give someone a working link instead of guessing one.",
2231
+ route: "settings.getNetwork",
2232
+ method: "GET",
2233
+ params: [],
2234
+ buildInput: () => void 0
2235
+ },
2236
+ {
2237
+ name: "list_runeya_instances",
2238
+ description: "List every Runeya answering on this machine, this one included, with its port and directory. Use it when a port is taken or when two instances seem to disagree.",
2239
+ route: "diagnostics.instances",
2240
+ method: "GET",
2241
+ params: [],
2242
+ buildInput: () => void 0
2243
+ },
2244
+ {
2245
+ name: "get_cloud_status",
2246
+ description: "Say whether this Runeya is linked to a cloud account, and to which cloud. A kanban tool failing for lack of a cloud link is explained here.",
2247
+ route: "cloudAuth.status",
2248
+ method: "GET",
2249
+ params: [],
2250
+ buildInput: () => void 0
2251
+ }
2252
+ ];
2253
+ function buildMcpShape(cap) {
2254
+ const shape = {};
2255
+ for (const param of cap.params) {
2256
+ const base = (param.schema ?? z32.string()).describe(param.description);
2257
+ shape[param.name] = param.required ? base : base.optional();
2258
+ }
2259
+ shape["authToken"] = z32.string().describe("Bearer token for authentication").optional();
2260
+ return shape;
2261
+ }
2262
+ function toRawParams(input, cap) {
2263
+ const typed = new Set(cap?.params.filter((p) => p.schema).map((p) => p.name));
2264
+ return Object.fromEntries(Object.entries(input).map(([k, v]) => [
2265
+ k,
2266
+ v === null && typed.has(k) ? "null" : v == null ? void 0 : typeof v === "object" ? JSON.stringify(v) : String(v)
2267
+ ]));
2268
+ }
2269
+ async function callTrpc(baseUrl, route, method, input, authToken, fetchImpl = fetch) {
2270
+ const headers = {
2271
+ "Content-Type": "application/json",
2272
+ "Authorization": `Bearer ${authToken}`
2273
+ };
2274
+ const url = method === "GET" && input ? `${baseUrl}/api/trpc/${route}?input=${encodeURIComponent(JSON.stringify(input))}` : `${baseUrl}/api/trpc/${route}`;
2275
+ const res = await fetchImpl(url, {
2276
+ method,
2277
+ headers,
2278
+ ...method === "POST" ? { body: JSON.stringify(input ?? {}) } : {},
2279
+ signal: AbortSignal.timeout(15e3)
2280
+ });
2281
+ const json = await res.json();
2282
+ if (json["error"]) throw new Error(JSON.stringify(json["error"]));
2283
+ const result = json["result"];
2284
+ return result?.["data"] ?? result ?? json;
2285
+ }
2286
+ async function runCapability(cap, baseUrl, params, authToken, fetchImpl = fetch) {
2287
+ const result = await callTrpc(baseUrl, cap.route, cap.method, cap.buildInput(params), authToken, fetchImpl);
2288
+ return cap.transformOutput ? cap.transformOutput(result) : result;
2289
+ }
2290
+ function applyProjectAnchor(cap, params, anchor) {
2291
+ const accepts = (name) => cap.params.some((p) => p.name === name);
2292
+ const anchored = { ...params };
2293
+ const mayAnchorOrg = !params["projectId"] || params["projectId"] === anchor.projectId;
2294
+ if (anchor.projectId && accepts("projectId") && !anchored["projectId"]) {
2295
+ anchored["projectId"] = anchor.projectId;
2296
+ }
2297
+ if (mayAnchorOrg && anchor.orgId && !anchored["orgId"] && accepts("orgId")) {
2298
+ anchored["orgId"] = anchor.orgId;
2299
+ }
2300
+ if (mayAnchorOrg && anchor.orgId && !anchored["organizationId"] && accepts("organizationId")) {
2301
+ anchored["organizationId"] = anchor.orgId;
2302
+ }
2303
+ return anchored;
2304
+ }
2305
+
2306
+ // src/external.ts
2307
+ import { z as z26 } from "zod";
2308
+
2309
+ // src/project-anchors.ts
2310
+ function createProjectAnchors(env, cwd, containing) {
2311
+ const anchors = /* @__PURE__ */ new Map();
2312
+ return (key, baseUrl, authToken) => {
2313
+ if (env.projectId) return Promise.resolve(env);
2314
+ let anchor = anchors.get(key);
2315
+ if (!anchor) {
2316
+ anchor = containing(baseUrl, authToken, cwd).then((found) => {
2317
+ const p = found;
2318
+ return p ? { projectId: p.projectId, ...p.orgId ? { orgId: p.orgId } : {} } : {};
2319
+ }).catch((err) => {
2320
+ anchors.delete(key);
2321
+ throw new Error(`Impossible de savoir \xE0 quel projet Runeya appartient le dossier ${cwd} (${err.message}). Nomme le projet : appelle list_projects, puis passe son projectId.`);
2322
+ });
2323
+ anchors.set(key, anchor);
2324
+ }
2325
+ return anchor;
2326
+ };
2327
+ }
2328
+
2329
+ // src/session-token.ts
2330
+ var RETRY_DELAY_MS = 1e3;
2331
+ function jwtExpiresAt(token) {
2332
+ try {
2333
+ const payload = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
2334
+ return typeof payload.exp === "number" ? payload.exp : null;
2335
+ } catch {
2336
+ return null;
2337
+ }
2338
+ }
2339
+ function startSessionToken(initial, baseUrl, fetchImpl = fetch) {
2340
+ let token = initial;
2341
+ let timer = null;
2342
+ let stopped = false;
2343
+ const arm = (delayMs) => {
2344
+ if (stopped) return;
2345
+ timer = setTimeout(() => {
2346
+ void refresh();
2347
+ }, delayMs);
2348
+ timer.unref?.();
2349
+ };
2350
+ const schedule = () => {
2351
+ const exp = jwtExpiresAt(token);
2352
+ if (exp === null) return;
2353
+ const remainingMs = exp * 1e3 - Date.now();
2354
+ if (remainingMs > 0) arm(remainingMs / 2);
2355
+ };
2356
+ const retry = () => {
2357
+ const exp = jwtExpiresAt(token);
2358
+ if (exp !== null && exp * 1e3 - Date.now() > RETRY_DELAY_MS) arm(RETRY_DELAY_MS);
2359
+ };
2360
+ async function refresh() {
2361
+ timer = null;
2362
+ let res;
2363
+ try {
2364
+ res = await fetchImpl(`${baseUrl}/api/auth/refresh`, {
2365
+ method: "POST",
2366
+ headers: { Authorization: `Bearer ${token}` },
2367
+ signal: AbortSignal.timeout(5e3)
2368
+ });
2369
+ } catch {
2370
+ retry();
2371
+ return;
2372
+ }
2373
+ if (res.status === 401) return;
2374
+ if (!res.ok) {
2375
+ retry();
2376
+ return;
2377
+ }
2378
+ const body = await res.json().catch(() => null);
2379
+ if (typeof body?.token !== "string") {
2380
+ retry();
2381
+ return;
2382
+ }
2383
+ token = body.token;
2384
+ schedule();
2385
+ }
2386
+ if (initial) schedule();
2387
+ return {
2388
+ current: () => token,
2389
+ isInitial: (candidate) => candidate === initial,
2390
+ stop: () => {
2391
+ stopped = true;
2392
+ if (timer) clearTimeout(timer);
2393
+ timer = null;
2394
+ }
2395
+ };
2396
+ }
2397
+
2398
+ // src/instance-selector.ts
2399
+ function instanceUrl(i) {
2400
+ return `http://localhost:${i.port}`;
2401
+ }
2402
+ function describe(i) {
2403
+ return `pid ${i.pid} \u2014 port ${i.port} \u2014 ${i.cwd}${i.version ? ` (v${i.version})` : ""}`;
2404
+ }
2405
+ function createInstanceSelector(listInstances) {
2406
+ return {
2407
+ async instance(pid) {
2408
+ const instances = await listInstances();
2409
+ if (pid !== void 0) {
2410
+ const named = instances.find((i) => i.pid === pid);
2411
+ if (named) return named;
2412
+ throw new Error(`Aucune instance Runeya lanc\xE9e avec le pid ${pid}. Instances en cours :
2413
+ ${instances.map((i) => `- ${describe(i)}`).join("\n") || "(aucune)"}`);
2414
+ }
2415
+ if (instances.length === 0) {
2416
+ throw new Error("Runeya n'est pas lanc\xE9 sur cette machine : aucune instance \xE0 contacter.");
2417
+ }
2418
+ if (instances.length === 1) return instances[0];
2419
+ throw new Error(
2420
+ `Plusieurs instances Runeya sont lanc\xE9es. Demande \xE0 l'utilisateur laquelle utiliser, puis passe son pid dans le param\xE8tre runeyaInstance, \xE0 chaque appel de cette conversation :
2421
+ ${instances.map((i) => `- ${describe(i)}`).join("\n")}`
2422
+ );
2423
+ },
2424
+ list: listInstances
2425
+ };
2426
+ }
2427
+
2428
+ // src/pairing-client.ts
2429
+ import { createHash } from "crypto";
2430
+ import { mkdir, readFile, rm, writeFile, chmod } from "fs/promises";
2431
+ import { join } from "path";
2432
+ function clientDisplayName(info) {
2433
+ const title = info.title?.trim();
2434
+ if (title) return title.slice(0, 100);
2435
+ const words = info.name.split(/[-_\s]+/).filter(Boolean);
2436
+ const pretty = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2437
+ return pretty.slice(0, 100) || "Client MCP";
2438
+ }
2439
+ var EXPIRY_MARGIN_S = 30;
2440
+ var PairingClient = class {
2441
+ constructor(dir, clientInfo, cwd, fetchImpl = fetch) {
2442
+ this.dir = dir;
2443
+ this.clientInfo = clientInfo;
2444
+ this.cwd = cwd;
2445
+ this.fetchImpl = fetchImpl;
2446
+ }
2447
+ tokens = /* @__PURE__ */ new Map();
2448
+ pending = /* @__PURE__ */ new Map();
2449
+ /** Le nom technique du client : il distingue les secrets, il ne s'affiche pas. */
2450
+ name() {
2451
+ return this.clientInfo().name.slice(0, 100) || "Client MCP";
2452
+ }
2453
+ /**
2454
+ * Un secret par instance ET par client : accepter Claude Code ne donne pas
2455
+ * accès à Cursor, et chaque outil se révoque à part.
2456
+ */
2457
+ secretFile(key) {
2458
+ const id = createHash("sha256").update(`${key}\0${this.name()}`).digest("hex").slice(0, 32);
2459
+ return join(this.dir, `${id}.json`);
2460
+ }
2461
+ async readSecret(key) {
2462
+ try {
2463
+ const { secret } = JSON.parse(await readFile(this.secretFile(key), "utf8"));
2464
+ return typeof secret === "string" ? secret : null;
2465
+ } catch {
2466
+ return null;
2467
+ }
2468
+ }
2469
+ async writeSecret(key, secret) {
2470
+ await mkdir(this.dir, { recursive: true, mode: 448 });
2471
+ const file = this.secretFile(key);
2472
+ await writeFile(file, JSON.stringify({ secret }), { mode: 384 });
2473
+ await chmod(file, 384);
2474
+ }
2475
+ async call(target, route, method, input) {
2476
+ try {
2477
+ return await callTrpc(target.baseUrl, route, method, input, "", this.fetchImpl);
2478
+ } catch (err) {
2479
+ if (/NOT_FOUND/.test(err.message)) {
2480
+ throw new Error(`L'instance Runeya de ${target.baseUrl} est trop ancienne pour accepter ce client : mettez Runeya \xE0 jour, ou passez une cl\xE9 d'API dans RUNEYA_API_TOKEN.`);
2481
+ }
2482
+ throw err;
2483
+ }
2484
+ }
2485
+ /**
2486
+ * Oublie le jeton en mémoire. Le serveur l'a refusé, typiquement après un
2487
+ * redémarrage (la clé de signature change) : le prochain `token()` le
2488
+ * réobtient avec le secret, sans nouvelle demande d'accès.
2489
+ */
2490
+ forget(target) {
2491
+ this.tokens.delete(target.key);
2492
+ }
2493
+ /** Un jeton pour cette instance, ou une erreur qui dit à l'utilisateur quoi faire. */
2494
+ async token(target) {
2495
+ const cached = this.tokens.get(target.key);
2496
+ const exp = cached ? jwtExpiresAt(cached) : null;
2497
+ if (cached && exp !== null && exp - EXPIRY_MARGIN_S > Date.now() / 1e3) return cached;
2498
+ this.tokens.delete(target.key);
2499
+ const secret = await this.readSecret(target.key);
2500
+ if (secret) {
2501
+ try {
2502
+ const { token } = await this.call(target, "mcpPairing.exchange", "POST", { secret });
2503
+ this.tokens.set(target.key, token);
2504
+ return token;
2505
+ } catch (err) {
2506
+ if (!/UNAUTHORIZED/.test(err.message)) throw err;
2507
+ await rm(this.secretFile(target.key), { force: true });
2508
+ }
2509
+ }
2510
+ return this.pair(target);
2511
+ }
2512
+ async pair(target) {
2513
+ const pending = this.pending.get(target.key);
2514
+ if (pending) {
2515
+ const { status, secret } = await this.call(target, "mcpPairing.status", "GET", { requestId: pending.requestId });
2516
+ if (status === "approved" && secret) {
2517
+ this.pending.delete(target.key);
2518
+ await this.writeSecret(target.key, secret);
2519
+ return this.token(target);
2520
+ }
2521
+ if (status === "pending") {
2522
+ throw new Error(`Acc\xE8s en attente : l'utilisateur doit accepter la demande dans Runeya (code ${pending.code}), puis relancer l'action.`);
2523
+ }
2524
+ this.pending.delete(target.key);
2525
+ if (status === "denied") throw new Error("L'utilisateur a refus\xE9 l'acc\xE8s dans Runeya.");
2526
+ }
2527
+ const { id, code } = await this.call(target, "mcpPairing.request", "POST", { name: clientDisplayName(this.clientInfo()), cwd: this.cwd });
2528
+ this.pending.set(target.key, { requestId: id, code });
2529
+ throw new Error(`Runeya doit autoriser ce client. Une demande d'acc\xE8s vient de s'afficher dans Runeya avec le code ${code} : demande \xE0 l'utilisateur de v\xE9rifier le code et d'accepter, puis relance l'action.`);
2530
+ }
2531
+ };
2532
+
2533
+ // src/server.ts
2534
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2535
+ import { z as z25 } from "zod";
2536
+ function needsAnchor(cap, params) {
2537
+ const accepts = (name) => cap.params.some((p) => p.name === name);
2538
+ if (params["orgId"] || params["organizationId"]) return false;
2539
+ if (accepts("projectId")) return !params["projectId"];
2540
+ return ["orgId", "organizationId"].some((name) => accepts(name) && !params[name]);
2541
+ }
2542
+ var RETRY = /* @__PURE__ */ Symbol("retry");
2543
+ function text(value) {
2544
+ return { content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }] };
2545
+ }
2546
+ function error(err) {
2547
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
2548
+ }
2549
+ function createRuneyaMcpServer(opts) {
2550
+ const server = new McpServer({ name: "runeya", version: "1.0.0" });
2551
+ async function callCapability(cap, input) {
2552
+ const rawParams = toRawParams(input, cap);
2553
+ const raw = rawParams["authToken"];
2554
+ const explicit = raw && !raw.startsWith("$") && !opts.sessionToken.isInitial(raw) ? raw : null;
2555
+ const call = async () => {
2556
+ const target = await opts.target(input);
2557
+ const authToken = explicit ?? (target.authToken || opts.sessionToken.current() || opts.internalToken || void 0);
2558
+ if (!authToken) throw new Error("No auth token available");
2559
+ const anchor = target.projectAnchor && needsAnchor(cap, rawParams) ? await target.projectAnchor() : opts.projectAnchor;
2560
+ const params = applyProjectAnchor(cap, rawParams, anchor);
2561
+ try {
2562
+ return await runCapability(cap, target.baseUrl, params, authToken);
2563
+ } catch (err) {
2564
+ if (!explicit && target.forgetToken && /UNAUTHORIZED/.test(err.message)) {
2565
+ target.forgetToken();
2566
+ return RETRY;
2567
+ }
2568
+ throw err;
2569
+ }
2570
+ };
2571
+ let result = await call();
2572
+ if (result === RETRY) result = await call();
2573
+ if (result === RETRY) throw new Error("Runeya refuse le jeton de ce client, m\xEAme renouvel\xE9.");
2574
+ return result;
2575
+ }
2576
+ for (const cap of RUNEYA_CAPABILITIES) {
2577
+ server.tool(cap.name, cap.description, { ...buildMcpShape(cap), ...opts.extraShape }, async (input) => {
2578
+ try {
2579
+ return text(await callCapability(cap, input));
2580
+ } catch (err) {
2581
+ return error(err);
2582
+ }
2583
+ });
2584
+ }
2585
+ registerWorkOnCardPrompt(server, (input) => callCapability(CARD_CONTEXT, input), opts.extraShape !== void 0);
2586
+ return server;
2587
+ }
2588
+ var CARD_CONTEXT = RUNEYA_CAPABILITIES.find((c) => c.name === "kanban_get_card_context");
2589
+ function registerWorkOnCardPrompt(server, cardContext, withInstance) {
2590
+ const args = {
2591
+ boardId: z25.string().max(128).describe("The kanban board ID"),
2592
+ cardId: z25.string().max(128).describe("The card ID"),
2593
+ ...withInstance ? { runeyaInstance: z25.string().max(16).optional().describe("pid of the Runeya instance, when several are running") } : {}
2594
+ };
2595
+ server.prompt(
2596
+ "work_on_card",
2597
+ "Work on a Runeya kanban card: loads the card, the board guidelines and the expected ending.",
2598
+ args,
2599
+ async (input) => {
2600
+ const { boardId, cardId, runeyaInstance } = input;
2601
+ const pid = runeyaInstance ? Number(runeyaInstance) : void 0;
2602
+ const { context } = await cardContext({ boardId, cardId, ...pid ? { runeyaInstance: pid } : {} });
2603
+ return {
2604
+ description: `Carte ${cardId}`,
2605
+ messages: [{
2606
+ role: "user",
2607
+ content: {
2608
+ type: "text",
2609
+ text: `Je veux travailler sur cette carte du kanban Runeya. Voici son contexte, tel que Runeya le donne \xE0 une session lanc\xE9e depuis la carte : suis ces consignes, en particulier la fin attendue (d\xE9placer la carte si besoin, puis la commenter).
2610
+
2611
+ ${context}`
2612
+ }
2613
+ }]
2614
+ };
2615
+ }
2616
+ );
2617
+ }
2618
+ function registerInstanceTools(server, selector) {
2619
+ server.tool(
2620
+ "list_local_runeya_instances",
2621
+ "List the Runeya instances running on this machine, with their pid. When several are running, ask the user which one to use, then pass its pid as runeyaInstance on every call of this conversation.",
2622
+ {},
2623
+ async () => {
2624
+ try {
2625
+ return text(await selector.list());
2626
+ } catch (err) {
2627
+ return error(err);
2628
+ }
2629
+ }
2630
+ );
2631
+ }
2632
+
2633
+ // src/external.ts
2634
+ async function runExternalMcp(opts) {
2635
+ const fixedUrl = process.env["RUNEYA_BASE_URL"];
2636
+ const selector = createInstanceSelector(async () => (await opts.listInstances()).map(({ pid, port, cwd, dataDir, version }) => ({ pid, port, cwd, dataDir, version })));
2637
+ const sessionToken = startSessionToken(process.env["RUNEYA_API_TOKEN"] ?? "", fixedUrl ?? "");
2638
+ const pairing = new PairingClient(
2639
+ // Même règle que `machineRoot()` du serveur : `RUNEYA_HOME` déplace la racine machine.
2640
+ join2(opts.machineRoot ?? join2(process.env["RUNEYA_HOME"] || homedir(), ".runeya"), "mcp"),
2641
+ () => server.server.getClientVersion() ?? { name: "" },
2642
+ process.cwd()
2643
+ );
2644
+ const envAnchor = {
2645
+ projectId: process.env["RUNEYA_PROJECT_ID"] || void 0,
2646
+ orgId: process.env["RUNEYA_ORG_ID"] || void 0
2647
+ };
2648
+ const anchorFor = createProjectAnchors(envAnchor, process.cwd(), (baseUrl, authToken, path) => callTrpc(baseUrl, "project.containing", "GET", { path }, authToken));
2649
+ const server = createRuneyaMcpServer({
2650
+ target: async (input) => {
2651
+ const pid = typeof input["runeyaInstance"] === "number" ? input["runeyaInstance"] : void 0;
2652
+ const { baseUrl, key } = fixedUrl ? { baseUrl: fixedUrl, key: fixedUrl } : await selector.instance(pid).then((i) => ({ baseUrl: instanceUrl(i), key: i.dataDir }));
2653
+ if (sessionToken.current()) {
2654
+ const authToken2 = sessionToken.current();
2655
+ return { baseUrl, authToken: authToken2, projectAnchor: () => anchorFor(key, baseUrl, authToken2) };
2656
+ }
2657
+ const authToken = await pairing.token({ baseUrl, key });
2658
+ return {
2659
+ baseUrl,
2660
+ authToken,
2661
+ projectAnchor: () => anchorFor(key, baseUrl, authToken),
2662
+ forgetToken: () => pairing.forget({ baseUrl, key })
2663
+ };
2664
+ },
2665
+ sessionToken,
2666
+ projectAnchor: envAnchor,
2667
+ extraShape: fixedUrl ? void 0 : {
2668
+ runeyaInstance: z26.number().int().positive().optional().describe(
2669
+ "The pid of the Runeya instance to call, from list_local_runeya_instances. Needed only when several instances are running: then pass it on every call of this conversation."
2670
+ )
2671
+ }
2672
+ });
2673
+ if (!fixedUrl) registerInstanceTools(server, selector);
2674
+ await server.connect(new StdioServerTransport());
2675
+ }
2676
+ export {
2677
+ runExternalMcp
2678
+ };
2679
+ //# sourceMappingURL=external.js.map