teamcopilot 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/.env.example +10 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +131 -0
  4. package/bin/teamcopilot.js +281 -0
  5. package/dist/auth/index.js +189 -0
  6. package/dist/change-user-role.js +77 -0
  7. package/dist/chat/index.js +849 -0
  8. package/dist/constants.js +2 -0
  9. package/dist/create-user.js +98 -0
  10. package/dist/cronjob/index.js +16 -0
  11. package/dist/cronjob/resource-reconciliation.js +33 -0
  12. package/dist/delete-user.js +66 -0
  13. package/dist/frontend/assets/abap-CRCWOmpq.js +1 -0
  14. package/dist/frontend/assets/apex-DnsZk_dE.js +1 -0
  15. package/dist/frontend/assets/azcli-1IWB1ccx.js +1 -0
  16. package/dist/frontend/assets/bat-DPkNLes8.js +1 -0
  17. package/dist/frontend/assets/bicep-Corcdgou.js +2 -0
  18. package/dist/frontend/assets/cameligo-CGrWLZr3.js +1 -0
  19. package/dist/frontend/assets/clojure-D9WOWImG.js +1 -0
  20. package/dist/frontend/assets/codicon-DCmgc-ay.ttf +0 -0
  21. package/dist/frontend/assets/coffee-B7EJu28W.js +1 -0
  22. package/dist/frontend/assets/cpp-SEyurbux.js +1 -0
  23. package/dist/frontend/assets/csharp-BoL64M5l.js +1 -0
  24. package/dist/frontend/assets/csp-C46ZqvIl.js +1 -0
  25. package/dist/frontend/assets/css-DQU6DXDx.js +3 -0
  26. package/dist/frontend/assets/cssMode-BDT3WbVs.js +4 -0
  27. package/dist/frontend/assets/cypher-D84EuPTj.js +1 -0
  28. package/dist/frontend/assets/dart-D8lhlL1r.js +1 -0
  29. package/dist/frontend/assets/dockerfile-DLk6rpji.js +1 -0
  30. package/dist/frontend/assets/ecl-BO6FnfXk.js +1 -0
  31. package/dist/frontend/assets/editor.worker-B4pQIWZD.js +12 -0
  32. package/dist/frontend/assets/elixir-BRjLKONM.js +1 -0
  33. package/dist/frontend/assets/flow9-Cac8vKd7.js +1 -0
  34. package/dist/frontend/assets/freemarker2-C7-hEgID.js +3 -0
  35. package/dist/frontend/assets/fsharp-fd1GTHhf.js +1 -0
  36. package/dist/frontend/assets/go-O9LJTZXk.js +1 -0
  37. package/dist/frontend/assets/graphql-LQdxqEYJ.js +1 -0
  38. package/dist/frontend/assets/handlebars-4cwTkPir.js +1 -0
  39. package/dist/frontend/assets/hcl-DxDQ3s82.js +1 -0
  40. package/dist/frontend/assets/html-YNfE1Q0A.js +1 -0
  41. package/dist/frontend/assets/htmlMode-opTQ1HoB.js +4 -0
  42. package/dist/frontend/assets/index-DWyaVa1h.js +782 -0
  43. package/dist/frontend/assets/index-lXrsgeTF.css +1 -0
  44. package/dist/frontend/assets/ini-BvajGCUy.js +1 -0
  45. package/dist/frontend/assets/java-SYsfObOQ.js +1 -0
  46. package/dist/frontend/assets/javascript-BEwGzk7T.js +1 -0
  47. package/dist/frontend/assets/jsonMode-CGhIS5Al.js +10 -0
  48. package/dist/frontend/assets/julia-DQXNmw_w.js +1 -0
  49. package/dist/frontend/assets/kotlin-qQ0MG-9I.js +1 -0
  50. package/dist/frontend/assets/less-GGFNNJHn.js +2 -0
  51. package/dist/frontend/assets/lexon-Canl7DCW.js +1 -0
  52. package/dist/frontend/assets/liquid-QekTGCGJ.js +1 -0
  53. package/dist/frontend/assets/lua-D28Ae8-K.js +1 -0
  54. package/dist/frontend/assets/m3-DPitgjJI.js +1 -0
  55. package/dist/frontend/assets/markdown-B811l8j2.js +1 -0
  56. package/dist/frontend/assets/mdx-BAVDaB7v.js +1 -0
  57. package/dist/frontend/assets/mips-CdjsipkG.js +1 -0
  58. package/dist/frontend/assets/msdax-CYqgjx_P.js +1 -0
  59. package/dist/frontend/assets/mysql-BHd6q0vd.js +1 -0
  60. package/dist/frontend/assets/objective-c-B1aVtJYH.js +1 -0
  61. package/dist/frontend/assets/pascal-BhNW15KB.js +1 -0
  62. package/dist/frontend/assets/pascaligo-5jv8CcQD.js +1 -0
  63. package/dist/frontend/assets/perl-DlYyT36c.js +1 -0
  64. package/dist/frontend/assets/pgsql-Dy0bjov7.js +1 -0
  65. package/dist/frontend/assets/php-120yhfDK.js +1 -0
  66. package/dist/frontend/assets/pla-CjnFlu4u.js +1 -0
  67. package/dist/frontend/assets/postiats-CQpG440k.js +1 -0
  68. package/dist/frontend/assets/powerquery-DdJtto1Z.js +1 -0
  69. package/dist/frontend/assets/powershell-Bu_VLpJB.js +1 -0
  70. package/dist/frontend/assets/protobuf-IBS6jZEB.js +2 -0
  71. package/dist/frontend/assets/pug-kFxLfcjb.js +1 -0
  72. package/dist/frontend/assets/python-BQlHw7XO.js +1 -0
  73. package/dist/frontend/assets/qsharp-q7JyzKFN.js +1 -0
  74. package/dist/frontend/assets/r-BIFz-_sK.js +1 -0
  75. package/dist/frontend/assets/razor-Be3Wwc2E.js +1 -0
  76. package/dist/frontend/assets/redis-CHOsPHWR.js +1 -0
  77. package/dist/frontend/assets/redshift-CBifECDb.js +1 -0
  78. package/dist/frontend/assets/restructuredtext-CghPJEOS.js +1 -0
  79. package/dist/frontend/assets/ruby-CYWGW-b1.js +1 -0
  80. package/dist/frontend/assets/rust-DMDD0SHb.js +1 -0
  81. package/dist/frontend/assets/sb-BYAiYHFx.js +1 -0
  82. package/dist/frontend/assets/scala-Bqvq8jcR.js +1 -0
  83. package/dist/frontend/assets/scheme-Dhb-2j9p.js +1 -0
  84. package/dist/frontend/assets/scss-CTwUZ5N7.js +3 -0
  85. package/dist/frontend/assets/shell-CsDZo4DB.js +1 -0
  86. package/dist/frontend/assets/solidity-CME5AdoB.js +1 -0
  87. package/dist/frontend/assets/sophia-RYC1BQQz.js +1 -0
  88. package/dist/frontend/assets/sparql-KEyrF7De.js +1 -0
  89. package/dist/frontend/assets/sql-BdTr02Mf.js +1 -0
  90. package/dist/frontend/assets/st-C7iG7M4S.js +1 -0
  91. package/dist/frontend/assets/swift-D7IUmUK8.js +1 -0
  92. package/dist/frontend/assets/systemverilog-DgMryOEJ.js +1 -0
  93. package/dist/frontend/assets/tcl-PloMZuKG.js +1 -0
  94. package/dist/frontend/assets/tsMode-CIBFoN3z.js +11 -0
  95. package/dist/frontend/assets/twig-BfRIq3la.js +1 -0
  96. package/dist/frontend/assets/typescript-BuV9wEIE.js +1 -0
  97. package/dist/frontend/assets/typespec-CzxlYoT_.js +1 -0
  98. package/dist/frontend/assets/vb-BwAE3J76.js +1 -0
  99. package/dist/frontend/assets/wgsl-B_1kOXbF.js +298 -0
  100. package/dist/frontend/assets/xml-DcDKYaM4.js +1 -0
  101. package/dist/frontend/assets/yaml-CuBNmOuI.js +1 -0
  102. package/dist/frontend/index.html +14 -0
  103. package/dist/frontend/logo.svg +50 -0
  104. package/dist/index.js +169 -0
  105. package/dist/logging.js +30 -0
  106. package/dist/opencode-auth/index.js +122 -0
  107. package/dist/opencode-server.js +91 -0
  108. package/dist/prisma/client.js +38 -0
  109. package/dist/reset-password.js +73 -0
  110. package/dist/rotate-jwt-secret.js +20 -0
  111. package/dist/scripts/prisma-workspace.js +34 -0
  112. package/dist/skills/index.js +311 -0
  113. package/dist/types/permissions.js +2 -0
  114. package/dist/types/shared/permissions.js +17 -0
  115. package/dist/types/shared/skill.js +17 -0
  116. package/dist/types/shared/workflow-files.js +17 -0
  117. package/dist/types/shared/workflow.js +17 -0
  118. package/dist/types/skill.js +2 -0
  119. package/dist/types/workflow-files.js +2 -0
  120. package/dist/types/workflow.js +2 -0
  121. package/dist/users/index.js +22 -0
  122. package/dist/utils/approval-snapshot-common.js +596 -0
  123. package/dist/utils/assert.js +20 -0
  124. package/dist/utils/chat-session.js +44 -0
  125. package/dist/utils/cli-bootstrap.js +26 -0
  126. package/dist/utils/index.js +95 -0
  127. package/dist/utils/jwt-secret.js +63 -0
  128. package/dist/utils/opencode-auth.js +126 -0
  129. package/dist/utils/opencode-client.js +109 -0
  130. package/dist/utils/password-policy.js +12 -0
  131. package/dist/utils/permission-common.js +280 -0
  132. package/dist/utils/redact.js +108 -0
  133. package/dist/utils/resource-access.js +37 -0
  134. package/dist/utils/resource-file-routes.js +115 -0
  135. package/dist/utils/resource-files.js +572 -0
  136. package/dist/utils/runtime-paths.js +61 -0
  137. package/dist/utils/session-abort.js +52 -0
  138. package/dist/utils/skill-approval-snapshot.js +39 -0
  139. package/dist/utils/skill-files.js +17 -0
  140. package/dist/utils/skill-permissions.js +15 -0
  141. package/dist/utils/skill.js +217 -0
  142. package/dist/utils/user-role.js +14 -0
  143. package/dist/utils/workflow-approval-snapshot.js +38 -0
  144. package/dist/utils/workflow-files.js +17 -0
  145. package/dist/utils/workflow-interruption.js +50 -0
  146. package/dist/utils/workflow-permissions.js +27 -0
  147. package/dist/utils/workflow-runner.js +414 -0
  148. package/dist/utils/workflow.js +158 -0
  149. package/dist/utils/workspace-sync.js +204 -0
  150. package/dist/workflows/index.js +751 -0
  151. package/dist/workspace_files/.opencode/opencode.json +17 -0
  152. package/dist/workspace_files/.opencode/package.json +14 -0
  153. package/dist/workspace_files/.opencode/plugins/createSkill.ts +339 -0
  154. package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +345 -0
  155. package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +173 -0
  156. package/dist/workspace_files/.opencode/plugins/findSkill.ts +211 -0
  157. package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +135 -0
  158. package/dist/workspace_files/.opencode/plugins/honeytoken-protection.ts +64 -0
  159. package/dist/workspace_files/.opencode/plugins/listAvailableSkills.ts +93 -0
  160. package/dist/workspace_files/.opencode/plugins/listAvailableWorkflows.ts +93 -0
  161. package/dist/workspace_files/.opencode/plugins/python-protection.ts +184 -0
  162. package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +168 -0
  163. package/dist/workspace_files/.opencode/tsconfig.json +16 -0
  164. package/dist/workspace_files/AGENTS.md +483 -0
  165. package/dist/workspace_files/package-lock.json +167 -0
  166. package/dist/workspace_files/package.json +5 -0
  167. package/package.json +86 -0
  168. package/prisma/migrations/20260203040755_init/migration.sql +20 -0
  169. package/prisma/migrations/20260204034845_replace_google_auth_with_email_password/migration.sql +25 -0
  170. package/prisma/migrations/20260207022226_add_user_role/migration.sql +25 -0
  171. package/prisma/migrations/20260210161254_add_workflow_runs/migration.sql +16 -0
  172. package/prisma/migrations/20260211050606_adds_workflow_table/migration.sql +40 -0
  173. package/prisma/migrations/20260211050750_adds_fkey_constraint/migration.sql +21 -0
  174. package/prisma/migrations/20260211051912_removes_workflow_table/migration.sql +34 -0
  175. package/prisma/migrations/20260211052238_changes_workflow_id_to_slug/migration.sql +27 -0
  176. package/prisma/migrations/20260212051912_add_output_to_workflow_runs/migration.sql +2 -0
  177. package/prisma/migrations/20260213073006_add_chat_sessions/migration.sql +13 -0
  178. package/prisma/migrations/20260216053202_add_chat_sessions_opencode_session_id_idx/migration.sql +2 -0
  179. package/prisma/migrations/20260216053237_drop_redundant_chat_sessions_opencode_idx/migration.sql +2 -0
  180. package/prisma/migrations/20260219060705_makes/migration.sql +24 -0
  181. package/prisma/migrations/20260222040542_add_workflow_execution_permissions/migration.sql +18 -0
  182. package/prisma/migrations/20260222040815_remove_workflow_execution_permissions/migration.sql +10 -0
  183. package/prisma/migrations/20260222041348_add_workflow_execution_permissions_final/migration.sql +17 -0
  184. package/prisma/migrations/20260222041741_rename_to_tool_execution_permissions/migration.sql +30 -0
  185. package/prisma/migrations/20260222041826_simplify_tool_execution_permissions/migration.sql +29 -0
  186. package/prisma/migrations/20260222041950_add_fields_for_standalone_permissions/migration.sql +32 -0
  187. package/prisma/migrations/20260222042954_simplify_tool_permissions_table/migration.sql +27 -0
  188. package/prisma/migrations/20260223073902_add_workflow_run_permissions_tables/migration.sql +23 -0
  189. package/prisma/migrations/20260225025151_add_workflow_metadata/migration.sql +16 -0
  190. package/prisma/migrations/20260225031035_merge_workflow_permissions_into_metadata/migration.sql +44 -0
  191. package/prisma/migrations/20260225031752_removes_default_for_run_permission_mode/migration.sql +20 -0
  192. package/prisma/migrations/20260225033603_remove_workflow_metadata_user_fkeys/migration.sql +18 -0
  193. package/prisma/migrations/20260225043032_restore_workflow_metadata_user_fkeys/migration.sql +20 -0
  194. package/prisma/migrations/20260225091423_add_workflow_approved_snapshots/migration.sql +28 -0
  195. package/prisma/migrations/20260226032121_add_is_approved_to_workflow_metadata/migration.sql +21 -0
  196. package/prisma/migrations/20260226032444_undoes_last_db_change/migration.sql +26 -0
  197. package/prisma/migrations/20260227120000_remove_snapshot_hash_from_approved_snapshots/migration.sql +16 -0
  198. package/prisma/migrations/20260228071125_adds_workspace_path_to_snapshot_table/migration.sql +22 -0
  199. package/prisma/migrations/20260228071217_modifies_index_and_removes_default_value/migration.sql +22 -0
  200. package/prisma/migrations/20260228071710_undoes_previous/migration.sql +27 -0
  201. package/prisma/migrations/20260228105022_add_must_change_password_first_login/migration.sql +20 -0
  202. package/prisma/migrations/20260301115439_add_workflow_run_log_refs/migration.sql +8 -0
  203. package/prisma/migrations/20260301122557_add_workflow_aborted_sessions/migration.sql +5 -0
  204. package/prisma/migrations/20260302045545_move_workflow_run_log_refs_into_workflow_runs/migration.sql +17 -0
  205. package/prisma/migrations/20260303040318_add_skill_tables/migration.sql +61 -0
  206. package/prisma/migrations/20260303051533_unify_resource_permissions/migration.sql +97 -0
  207. package/prisma/migrations/20260303064255_unify_resource_metadata_and_snapshots/migration.sql +179 -0
  208. package/prisma/migrations/migration_lock.toml +3 -0
  209. package/prisma/schema.prisma +147 -0
@@ -0,0 +1,849 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = __importDefault(require("express"));
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const url_1 = require("url");
10
+ const client_1 = __importDefault(require("../prisma/client"));
11
+ const index_1 = require("../utils/index");
12
+ const opencode_client_1 = require("../utils/opencode-client");
13
+ const chat_session_1 = require("../utils/chat-session");
14
+ const assert_1 = require("../utils/assert");
15
+ const redact_1 = require("../utils/redact");
16
+ const session_abort_1 = require("../utils/session-abort");
17
+ const router = express_1.default.Router({ mergeParams: true });
18
+ const USER_INSTRUCTIONS_FILENAME = "USER_INSTRUCTIONS.md";
19
+ const ACTUAL_USER_MESSAGE_MARKER = "####### Actual user message below #######";
20
+ function getErrorMessage(error) {
21
+ if (error && typeof error === 'object' && 'detail' in error) {
22
+ return String(error.detail);
23
+ }
24
+ if (error instanceof Error) {
25
+ return error.message;
26
+ }
27
+ return 'Unknown error';
28
+ }
29
+ function normalizeWorkspaceRelativePath(rawPath) {
30
+ const normalized = path_1.default.posix.normalize(rawPath.split("\\").join("/").trim());
31
+ if (!normalized || normalized === "." || normalized.startsWith("../") || normalized === ".." || normalized.startsWith("/")) {
32
+ throw {
33
+ status: 400,
34
+ message: "file path must be a workspace-relative path"
35
+ };
36
+ }
37
+ return normalized;
38
+ }
39
+ function parseMessageParts(rawParts) {
40
+ if (!Array.isArray(rawParts)) {
41
+ throw {
42
+ status: 400,
43
+ message: "parts must be an array"
44
+ };
45
+ }
46
+ const parts = [];
47
+ for (const rawPart of rawParts) {
48
+ if (!rawPart || typeof rawPart !== "object") {
49
+ throw {
50
+ status: 400,
51
+ message: "Each part must be an object"
52
+ };
53
+ }
54
+ const part = rawPart;
55
+ if (part.type === "text") {
56
+ if (typeof part.text !== "string") {
57
+ throw {
58
+ status: 400,
59
+ message: "Text part requires a string text field"
60
+ };
61
+ }
62
+ parts.push({ type: "text", text: part.text });
63
+ continue;
64
+ }
65
+ if (part.type === "file") {
66
+ if (typeof part.path !== "string") {
67
+ throw {
68
+ status: 400,
69
+ message: "File part requires a string path field"
70
+ };
71
+ }
72
+ parts.push({ type: "file", path: normalizeWorkspaceRelativePath(part.path) });
73
+ continue;
74
+ }
75
+ throw {
76
+ status: 400,
77
+ message: `Unsupported part type: ${String(part.type)}`
78
+ };
79
+ }
80
+ return parts;
81
+ }
82
+ function buildOpencodePromptParts(parts) {
83
+ const workspaceDir = (0, opencode_client_1.getWorkspaceDir)();
84
+ return parts.map((part) => {
85
+ if (part.type === "text") {
86
+ return {
87
+ type: "text",
88
+ text: part.text
89
+ };
90
+ }
91
+ const normalizedPath = normalizeWorkspaceRelativePath(part.path);
92
+ const absolutePath = path_1.default.resolve(workspaceDir, normalizedPath);
93
+ const relativeCheck = path_1.default.relative(workspaceDir, absolutePath);
94
+ if (relativeCheck.startsWith("..") || path_1.default.isAbsolute(relativeCheck)) {
95
+ throw {
96
+ status: 400,
97
+ message: "file path must stay inside workspace"
98
+ };
99
+ }
100
+ const filename = normalizedPath.endsWith("/")
101
+ ? normalizedPath.slice(0, -1).split("/").pop() || normalizedPath
102
+ : normalizedPath.split("/").pop() || normalizedPath;
103
+ return {
104
+ type: "file",
105
+ mime: "text/plain",
106
+ filename,
107
+ url: (0, url_1.pathToFileURL)(absolutePath).href
108
+ };
109
+ });
110
+ }
111
+ async function readWorkspaceUserInstructions() {
112
+ const workspaceDir = (0, opencode_client_1.getWorkspaceDir)();
113
+ const userInstructionsPath = path_1.default.join(workspaceDir, USER_INSTRUCTIONS_FILENAME);
114
+ try {
115
+ const content = await promises_1.default.readFile(userInstructionsPath, "utf-8");
116
+ if (content.trim().length === 0) {
117
+ return null;
118
+ }
119
+ return content;
120
+ }
121
+ catch (err) {
122
+ const nodeError = err;
123
+ if (nodeError.code === "ENOENT") {
124
+ return null;
125
+ }
126
+ throw new Error(`Failed to read ${USER_INSTRUCTIONS_FILENAME}: ${nodeError.message}`);
127
+ }
128
+ }
129
+ function stripTextBeforeActualUserMarker(text) {
130
+ const markerIndex = text.indexOf(ACTUAL_USER_MESSAGE_MARKER);
131
+ if (markerIndex === -1) {
132
+ return text;
133
+ }
134
+ return text.slice(markerIndex + ACTUAL_USER_MESSAGE_MARKER.length).replace(/^\s+/, "");
135
+ }
136
+ function sanitizeFirstUserMessageForClient(messages) {
137
+ const firstUserMessageId = messages.find((message) => {
138
+ const info = message.info;
139
+ return info.role === "user";
140
+ })?.info.id;
141
+ if (!firstUserMessageId) {
142
+ return messages;
143
+ }
144
+ const firstUserMessage = messages.find((message) => message.info.id === firstUserMessageId);
145
+ if (!firstUserMessage) {
146
+ return messages;
147
+ }
148
+ const hasMarker = firstUserMessage.parts.some((part) => {
149
+ if (part.type !== "text") {
150
+ return false;
151
+ }
152
+ const textPart = part;
153
+ return typeof textPart.text === "string" && textPart.text.includes(ACTUAL_USER_MESSAGE_MARKER);
154
+ });
155
+ if (!hasMarker) {
156
+ return messages;
157
+ }
158
+ let markerFound = false;
159
+ return messages.map((message) => {
160
+ if (message.info.id !== firstUserMessageId) {
161
+ return message;
162
+ }
163
+ const parts = message.parts.map((part) => {
164
+ if (part.type !== "text") {
165
+ return part;
166
+ }
167
+ const textPart = part;
168
+ if (typeof textPart.text !== "string") {
169
+ return part;
170
+ }
171
+ if (markerFound) {
172
+ return part;
173
+ }
174
+ const markerIndex = textPart.text.indexOf(ACTUAL_USER_MESSAGE_MARKER);
175
+ if (markerIndex === -1) {
176
+ return {
177
+ ...textPart,
178
+ text: ""
179
+ };
180
+ }
181
+ markerFound = true;
182
+ return {
183
+ ...textPart,
184
+ text: stripTextBeforeActualUserMarker(textPart.text)
185
+ };
186
+ });
187
+ return {
188
+ info: message.info,
189
+ parts
190
+ };
191
+ });
192
+ }
193
+ function sanitizeEventForClient(event) {
194
+ if (event.type !== "message.part.updated") {
195
+ return event;
196
+ }
197
+ const properties = event.properties;
198
+ if (!properties || typeof properties !== "object") {
199
+ return event;
200
+ }
201
+ const part = properties.part;
202
+ if (!part || typeof part !== "object") {
203
+ return event;
204
+ }
205
+ const candidate = part;
206
+ if (candidate.type !== "text" || typeof candidate.text !== "string") {
207
+ return event;
208
+ }
209
+ return {
210
+ ...event,
211
+ properties: {
212
+ ...properties,
213
+ part: {
214
+ ...part,
215
+ text: stripTextBeforeActualUserMarker(candidate.text)
216
+ }
217
+ }
218
+ };
219
+ }
220
+ function shouldAutoGenerateTitle(title) {
221
+ if (!title)
222
+ return true;
223
+ const normalized = title.trim().toLowerCase();
224
+ if (!normalized)
225
+ return true;
226
+ const genericPatterns = [
227
+ /^new chat$/,
228
+ /^new session(?:\s*-\s*.*)?$/,
229
+ /^session(?:\s*-\s*.*)?$/,
230
+ /^chat(?:\s*-\s*.*)?$/
231
+ ];
232
+ return genericPatterns.some((pattern) => pattern.test(normalized));
233
+ }
234
+ function getEventSessionId(event) {
235
+ const directSessionId = event.sessionID;
236
+ if (typeof directSessionId === "string" && directSessionId.length > 0) {
237
+ return directSessionId;
238
+ }
239
+ const properties = event.properties;
240
+ if (!properties || typeof properties !== "object") {
241
+ return null;
242
+ }
243
+ const propertyRecord = properties;
244
+ const candidates = [
245
+ propertyRecord.sessionID,
246
+ propertyRecord.info?.sessionID,
247
+ propertyRecord.part?.sessionID,
248
+ propertyRecord.permission?.sessionID,
249
+ propertyRecord.message?.sessionID,
250
+ propertyRecord.error?.sessionID
251
+ ];
252
+ for (const candidate of candidates) {
253
+ if (typeof candidate === "string" && candidate.length > 0) {
254
+ return candidate;
255
+ }
256
+ }
257
+ return null;
258
+ }
259
+ function generateTitleFromUserMessage(content) {
260
+ const maxChars = 60;
261
+ const maxWords = 9;
262
+ let text = content
263
+ .replace(/\s+/g, " ")
264
+ .replace(/[`*_#>\[\]()]/g, "")
265
+ .trim();
266
+ text = text.split(/[.!?\n]/)[0]?.trim() || text;
267
+ const leadingPhrases = [
268
+ "can you ",
269
+ "could you ",
270
+ "please ",
271
+ "help me ",
272
+ "i want to ",
273
+ "i need to ",
274
+ "let's ",
275
+ "lets "
276
+ ];
277
+ let lowered = text.toLowerCase();
278
+ for (const phrase of leadingPhrases) {
279
+ if (lowered.startsWith(phrase)) {
280
+ text = text.slice(phrase.length).trim();
281
+ lowered = text.toLowerCase();
282
+ break;
283
+ }
284
+ }
285
+ const words = text.split(/\s+/).filter(Boolean).slice(0, maxWords);
286
+ let candidate = words.join(" ").trim();
287
+ if (!candidate) {
288
+ return "New Chat";
289
+ }
290
+ if (candidate.length > maxChars) {
291
+ candidate = candidate.slice(0, maxChars).trim();
292
+ if (!/[.!?]$/.test(candidate)) {
293
+ candidate = `${candidate}...`;
294
+ }
295
+ }
296
+ return candidate;
297
+ }
298
+ // GET /api/chat/sessions - List user's sessions
299
+ router.get('/sessions', (0, index_1.apiHandler)(async (req, res) => {
300
+ const sessions = await client_1.default.chat_sessions.findMany({
301
+ where: { user_id: req.userId },
302
+ orderBy: { updated_at: 'desc' }
303
+ });
304
+ if (sessions.length === 0) {
305
+ res.json({ sessions });
306
+ return;
307
+ }
308
+ const client = await (0, opencode_client_1.getOpencodeClient)();
309
+ const opencodeSessionsResult = await client.session.list();
310
+ if (opencodeSessionsResult.error) {
311
+ throw new Error(getErrorMessage(opencodeSessionsResult.error) || 'Failed to list sessions from opencode');
312
+ }
313
+ const opencodeSessionIds = new Set((opencodeSessionsResult.data || []).map((session) => session.id));
314
+ const staleSessionIds = sessions
315
+ .filter((session) => !opencodeSessionIds.has(session.opencode_session_id))
316
+ .map((session) => session.id);
317
+ if (staleSessionIds.length > 0) {
318
+ await client_1.default.chat_sessions.deleteMany({
319
+ where: {
320
+ id: { in: staleSessionIds },
321
+ user_id: req.userId
322
+ }
323
+ });
324
+ }
325
+ const validSessions = sessions.filter((session) => opencodeSessionIds.has(session.opencode_session_id));
326
+ res.json({ sessions: validSessions });
327
+ }, true));
328
+ // GET /api/chat/file-suggestions - Search workspace files/folders for @mentions
329
+ router.get('/file-suggestions', (0, index_1.apiHandler)(async (req, res) => {
330
+ const query = req.query.query;
331
+ if (typeof query !== "string") {
332
+ throw {
333
+ status: 400,
334
+ message: "query is required and must be a string"
335
+ };
336
+ }
337
+ const trimmedQuery = query.trim();
338
+ if (!trimmedQuery) {
339
+ res.json({ files: [] });
340
+ return;
341
+ }
342
+ const rawLimit = req.query.limit;
343
+ let limit = 10;
344
+ if (rawLimit !== undefined) {
345
+ const parsedLimit = Number.parseInt(String(rawLimit), 10);
346
+ if (!Number.isFinite(parsedLimit) || parsedLimit < 1 || parsedLimit > 50) {
347
+ throw {
348
+ status: 400,
349
+ message: "limit must be an integer between 1 and 50"
350
+ };
351
+ }
352
+ limit = parsedLimit;
353
+ }
354
+ const client = await (0, opencode_client_1.getOpencodeClient)();
355
+ const result = await client.find.files({
356
+ query: {
357
+ query: trimmedQuery
358
+ }
359
+ });
360
+ if (result.error) {
361
+ throw new Error(getErrorMessage(result.error) || "Failed to fetch file suggestions from opencode");
362
+ }
363
+ res.json({ files: (result.data || []).slice(0, limit) });
364
+ }, true));
365
+ // POST /api/chat/sessions - Create new session
366
+ router.post('/sessions', (0, index_1.apiHandler)(async (req, res) => {
367
+ const client = await (0, opencode_client_1.getOpencodeClient)();
368
+ // Create session in opencode
369
+ const result = await client.session.create();
370
+ if (result.error) {
371
+ throw new Error(getErrorMessage(result.error) || 'Failed to create opencode session');
372
+ }
373
+ const opencodeSession = result.data;
374
+ // Save session in our database
375
+ const session = await client_1.default.chat_sessions.create({
376
+ data: {
377
+ user_id: req.userId,
378
+ opencode_session_id: opencodeSession.id,
379
+ title: opencodeSession.title || 'New Chat',
380
+ created_at: Date.now(),
381
+ updated_at: Date.now()
382
+ }
383
+ });
384
+ res.json({
385
+ session: {
386
+ id: session.id,
387
+ opencode_session_id: session.opencode_session_id,
388
+ title: session.title,
389
+ created_at: session.created_at,
390
+ updated_at: session.updated_at
391
+ }
392
+ });
393
+ }, true));
394
+ // GET /api/chat/sessions/:id - Get session details
395
+ router.get('/sessions/:id', (0, index_1.apiHandler)(async (req, res) => {
396
+ const id = req.params.id;
397
+ const session = await client_1.default.chat_sessions.findFirst({
398
+ where: {
399
+ id,
400
+ user_id: req.userId
401
+ }
402
+ });
403
+ if (!session) {
404
+ throw {
405
+ status: 404,
406
+ message: 'Session not found'
407
+ };
408
+ }
409
+ // Get session details from opencode
410
+ const client = await (0, opencode_client_1.getOpencodeClient)();
411
+ const result = await client.session.get({
412
+ path: { id: session.opencode_session_id }
413
+ });
414
+ if (result.error) {
415
+ throw new Error(getErrorMessage(result.error) || 'Failed to get session from opencode');
416
+ }
417
+ res.json({
418
+ session: {
419
+ id: session.id,
420
+ opencode_session_id: session.opencode_session_id,
421
+ title: session.title || result.data?.title,
422
+ created_at: session.created_at,
423
+ updated_at: session.updated_at,
424
+ opencode_data: result.data
425
+ }
426
+ });
427
+ }, true));
428
+ /*
429
+ // DELETE /api/chat/sessions/:id - Delete session
430
+ router.delete('/sessions/:id', apiHandler(async (req, res) => {
431
+ const id = req.params.id as string;
432
+
433
+ const session = await prisma.chat_sessions.findFirst({
434
+ where: {
435
+ id,
436
+ user_id: req.userId!
437
+ }
438
+ });
439
+
440
+ if (!session) {
441
+ throw {
442
+ status: 404,
443
+ message: 'Session not found'
444
+ };
445
+ }
446
+
447
+ // Delete session from opencode
448
+ const client = await getOpencodeClient();
449
+ await client.session.delete({
450
+ path: { id: session.opencode_session_id }
451
+ });
452
+
453
+ // Delete from our database
454
+ await prisma.chat_sessions.delete({
455
+ where: { id }
456
+ });
457
+
458
+ res.json({ success: true });
459
+ }, true));
460
+ */
461
+ // GET /api/chat/sessions/:id/messages - Get messages
462
+ router.get('/sessions/:id/messages', (0, index_1.apiHandler)(async (req, res) => {
463
+ const id = req.params.id;
464
+ const session = await client_1.default.chat_sessions.findFirst({
465
+ where: {
466
+ id,
467
+ user_id: req.userId
468
+ }
469
+ });
470
+ if (!session) {
471
+ throw {
472
+ status: 404,
473
+ message: 'Session not found'
474
+ };
475
+ }
476
+ const client = await (0, opencode_client_1.getOpencodeClient)();
477
+ const result = await client.session.messages({
478
+ path: { id: session.opencode_session_id }
479
+ });
480
+ if (result.error) {
481
+ throw new Error(getErrorMessage(result.error) || 'Failed to get messages from opencode');
482
+ }
483
+ const statusResult = await client.session.status();
484
+ (0, assert_1.assertCondition)(!statusResult.error, getErrorMessage(statusResult.error));
485
+ const sessionStatusType = (0, chat_session_1.getSessionStatusTypeForSession)(statusResult.data, session.opencode_session_id);
486
+ const normalizedMessages = (0, chat_session_1.normalizeStaleRunningTools)(result.data, sessionStatusType);
487
+ const sanitizedMessages = sanitizeFirstUserMessageForClient(normalizedMessages);
488
+ res.json({
489
+ messages: sanitizedMessages,
490
+ session_status: sessionStatusType
491
+ });
492
+ }, true));
493
+ // POST /api/chat/sessions/:id/messages - Send message
494
+ router.post('/sessions/:id/messages', (0, index_1.apiHandler)(async (req, res) => {
495
+ const id = req.params.id;
496
+ const rawParts = req.body?.parts;
497
+ if (rawParts === undefined) {
498
+ throw {
499
+ status: 400,
500
+ message: "parts is required"
501
+ };
502
+ }
503
+ const inputParts = parseMessageParts(rawParts);
504
+ if (inputParts.length === 0) {
505
+ throw {
506
+ status: 400,
507
+ message: "parts must contain at least one item"
508
+ };
509
+ }
510
+ if (!inputParts.some((part) => part.type === "text" && part.text.trim().length > 0)) {
511
+ throw {
512
+ status: 400,
513
+ message: "Message must include a non-empty text part"
514
+ };
515
+ }
516
+ const session = await client_1.default.chat_sessions.findFirst({
517
+ where: {
518
+ id,
519
+ user_id: req.userId
520
+ }
521
+ });
522
+ if (!session) {
523
+ throw {
524
+ status: 404,
525
+ message: 'Session not found'
526
+ };
527
+ }
528
+ const pendingQuestion = await (0, opencode_client_1.getPendingQuestionForSession)(session.opencode_session_id);
529
+ if (pendingQuestion) {
530
+ throw {
531
+ status: 409,
532
+ message: 'A tool is waiting for input. Reply through the tool-answer endpoint.'
533
+ };
534
+ }
535
+ const pendingPermission = await (0, opencode_client_1.listPendingPermissionsForSession)(session.opencode_session_id);
536
+ if (pendingPermission.length > 0) {
537
+ throw {
538
+ status: 409,
539
+ message: 'At least one permission request is waiting for input. Reply through the permission-response endpoint.'
540
+ };
541
+ }
542
+ const client = await (0, opencode_client_1.getOpencodeClient)();
543
+ const promptParts = buildOpencodePromptParts(inputParts);
544
+ let finalPromptParts = promptParts;
545
+ const existingMessagesResult = await client.session.messages({
546
+ path: { id: session.opencode_session_id },
547
+ query: { limit: 1 }
548
+ });
549
+ if (existingMessagesResult.error) {
550
+ throw new Error(getErrorMessage(existingMessagesResult.error) || "Failed to check session message history");
551
+ }
552
+ if ((existingMessagesResult.data || []).length === 0) {
553
+ const userInstructions = await readWorkspaceUserInstructions();
554
+ if (userInstructions) {
555
+ const wrappedUserInstructions = `# Custom user instructions (in case of conflicts, the instructions here take precedence over the contents of the AGENTS.md file)\n\n${userInstructions}\n\n${ACTUAL_USER_MESSAGE_MARKER}\n\n`;
556
+ finalPromptParts = [
557
+ {
558
+ type: "text",
559
+ text: wrappedUserInstructions
560
+ },
561
+ ...promptParts
562
+ ];
563
+ }
564
+ }
565
+ // Use promptAsync to send message and return immediately
566
+ const result = await client.session.promptAsync({
567
+ path: { id: session.opencode_session_id },
568
+ body: { parts: finalPromptParts }
569
+ });
570
+ if (result.error) {
571
+ throw new Error(getErrorMessage(result.error) || 'Failed to send message to opencode');
572
+ }
573
+ const data = {
574
+ updated_at: Date.now()
575
+ };
576
+ if (shouldAutoGenerateTitle(session.title)) {
577
+ const firstTextPart = inputParts.find((part) => part.type === "text" && part.text.trim().length > 0);
578
+ data.title = generateTitleFromUserMessage(firstTextPart?.text || "");
579
+ }
580
+ const updatedSession = await client_1.default.chat_sessions.update({
581
+ where: { id },
582
+ data
583
+ });
584
+ res.json({
585
+ success: true,
586
+ session: {
587
+ id: updatedSession.id,
588
+ title: updatedSession.title,
589
+ updated_at: updatedSession.updated_at
590
+ }
591
+ });
592
+ }, true));
593
+ // GET /api/chat/sessions/:id/pending-permission - Get pending permission for session
594
+ router.get('/sessions/:id/pending-permission', (0, index_1.apiHandler)(async (req, res) => {
595
+ const id = req.params.id;
596
+ const session = await client_1.default.chat_sessions.findFirst({
597
+ where: {
598
+ id,
599
+ user_id: req.userId
600
+ }
601
+ });
602
+ if (!session) {
603
+ throw {
604
+ status: 404,
605
+ message: 'Session not found'
606
+ };
607
+ }
608
+ const permissions = [];
609
+ // Include opencode native pending permissions
610
+ const opencodePendingPermissions = (await (0, opencode_client_1.listPendingPermissions)())
611
+ .filter((permission) => permission.sessionID === session.opencode_session_id)
612
+ .map((permission) => {
613
+ (0, assert_1.assertCondition)(typeof permission.tool?.messageID === 'string' && typeof permission.tool?.callID === 'string', `Pending opencode permission '${permission.id}' is missing tool.messageID/callID`);
614
+ return {
615
+ ...permission,
616
+ tool: {
617
+ messageID: permission.tool.messageID,
618
+ callID: permission.tool.callID
619
+ }
620
+ };
621
+ });
622
+ permissions.push(...opencodePendingPermissions);
623
+ // Include custom tool execution pending permissions
624
+ const customPendingPermissions = await client_1.default.tool_execution_permissions.findMany({
625
+ where: {
626
+ opencode_session_id: session.opencode_session_id,
627
+ status: 'pending'
628
+ },
629
+ orderBy: {
630
+ created_at: 'asc'
631
+ }
632
+ });
633
+ for (const customPendingPermission of customPendingPermissions) {
634
+ permissions.push({
635
+ id: customPendingPermission.id,
636
+ sessionID: customPendingPermission.opencode_session_id,
637
+ permission: 'tool',
638
+ patterns: [],
639
+ metadata: {},
640
+ always: [],
641
+ tool: {
642
+ messageID: customPendingPermission.message_id,
643
+ callID: customPendingPermission.call_id
644
+ }
645
+ });
646
+ }
647
+ res.json({ permissions });
648
+ }, true));
649
+ // POST /api/chat/sessions/:id/tool-answer - Reply to a pending tool question
650
+ router.post('/sessions/:id/tool-answer', (0, index_1.apiHandler)(async (req, res) => {
651
+ const id = req.params.id;
652
+ const { content } = req.body;
653
+ if (!content || typeof content !== 'string') {
654
+ throw {
655
+ status: 400,
656
+ message: 'content is required and must be a string'
657
+ };
658
+ }
659
+ const session = await client_1.default.chat_sessions.findFirst({
660
+ where: {
661
+ id,
662
+ user_id: req.userId
663
+ }
664
+ });
665
+ if (!session) {
666
+ throw {
667
+ status: 404,
668
+ message: 'Session not found'
669
+ };
670
+ }
671
+ const pendingQuestion = await (0, opencode_client_1.getPendingQuestionForSession)(session.opencode_session_id);
672
+ if (!pendingQuestion) {
673
+ throw {
674
+ status: 409,
675
+ message: 'No pending tool question for this session'
676
+ };
677
+ }
678
+ // Current UI replies with a single string; map it to the first question.
679
+ const answers = pendingQuestion.questions.map((_, index) => index === 0 ? [content] : []);
680
+ await (0, opencode_client_1.replyToPendingQuestion)(pendingQuestion.id, answers);
681
+ await client_1.default.chat_sessions.update({
682
+ where: { id },
683
+ data: { updated_at: Date.now() }
684
+ });
685
+ res.json({ success: true });
686
+ }, true));
687
+ // POST /api/chat/sessions/:id/permission-response - Reply to a pending permission request
688
+ router.post('/sessions/:id/permission-response', (0, index_1.apiHandler)(async (req, res) => {
689
+ const id = req.params.id;
690
+ const { response, permission_id } = req.body;
691
+ if (response !== 'once' && response !== 'always' && response !== 'reject') {
692
+ throw {
693
+ status: 400,
694
+ message: 'response is required and must be one of: once, always, reject'
695
+ };
696
+ }
697
+ if (typeof permission_id !== 'string') {
698
+ throw {
699
+ status: 400,
700
+ message: 'permission_id is required and must be a string'
701
+ };
702
+ }
703
+ const session = await client_1.default.chat_sessions.findFirst({
704
+ where: {
705
+ id,
706
+ user_id: req.userId
707
+ }
708
+ });
709
+ if (!session) {
710
+ throw {
711
+ status: 404,
712
+ message: 'Session not found'
713
+ };
714
+ }
715
+ // First check our custom tool execution permissions by explicit permission_id.
716
+ const customPendingPermission = await client_1.default.tool_execution_permissions.findFirst({
717
+ where: {
718
+ id: permission_id,
719
+ opencode_session_id: session.opencode_session_id,
720
+ status: 'pending'
721
+ }
722
+ });
723
+ if (customPendingPermission) {
724
+ // Update our custom permission status
725
+ await client_1.default.tool_execution_permissions.update({
726
+ where: { id: customPendingPermission.id },
727
+ data: {
728
+ status: response === 'reject' ? 'rejected' : 'approved',
729
+ responded_at: BigInt(Date.now())
730
+ }
731
+ });
732
+ await client_1.default.chat_sessions.update({
733
+ where: { id },
734
+ data: { updated_at: Date.now() }
735
+ });
736
+ res.json({ success: true });
737
+ return;
738
+ }
739
+ // Otherwise treat it as an opencode-native permission ID and reply directly.
740
+ await (0, opencode_client_1.replyToPendingPermission)(session.opencode_session_id, permission_id, response);
741
+ await client_1.default.chat_sessions.update({
742
+ where: { id },
743
+ data: { updated_at: Date.now() }
744
+ });
745
+ res.json({ success: true });
746
+ }, true));
747
+ // POST /api/chat/sessions/:id/abort - Abort AI response
748
+ router.post('/sessions/:id/abort', (0, index_1.apiHandler)(async (req, res) => {
749
+ const id = req.params.id;
750
+ const session = await client_1.default.chat_sessions.findFirst({
751
+ where: {
752
+ id,
753
+ user_id: req.userId
754
+ }
755
+ });
756
+ if (!session) {
757
+ throw {
758
+ status: 404,
759
+ message: 'Session not found'
760
+ };
761
+ }
762
+ await (0, session_abort_1.abortOpencodeSession)(session.opencode_session_id);
763
+ res.json({
764
+ success: true
765
+ });
766
+ }, true));
767
+ // GET /api/chat/sessions/:id/events - SSE stream for real-time updates
768
+ router.get('/sessions/:id/events', (0, index_1.apiHandler)(async (req, res) => {
769
+ const id = req.params.id;
770
+ const session = await client_1.default.chat_sessions.findFirst({
771
+ where: {
772
+ id,
773
+ user_id: req.userId
774
+ }
775
+ });
776
+ if (!session) {
777
+ throw {
778
+ status: 404,
779
+ message: 'Session not found'
780
+ };
781
+ }
782
+ // Set up SSE headers
783
+ res.setHeader('Content-Type', 'text/event-stream');
784
+ res.setHeader('Cache-Control', 'no-cache');
785
+ res.setHeader('Connection', 'keep-alive');
786
+ res.setHeader('X-Accel-Buffering', 'no');
787
+ res.flushHeaders();
788
+ const port = (0, opencode_client_1.getOpencodePort)();
789
+ const workspaceDir = (0, opencode_client_1.getWorkspaceDir)();
790
+ // Create an abort controller for cleanup
791
+ const abortController = new AbortController();
792
+ // Handle client disconnect
793
+ req.on('close', () => {
794
+ abortController.abort();
795
+ });
796
+ try {
797
+ // Subscribe to opencode events using fetch with SSE
798
+ const response = await fetch(`http://localhost:${port}/event?directory=${encodeURIComponent(workspaceDir)}`, {
799
+ headers: {
800
+ 'Accept': 'text/event-stream',
801
+ },
802
+ signal: abortController.signal
803
+ });
804
+ if (!response.ok || !response.body) {
805
+ res.write(`data: ${JSON.stringify({ type: 'error', message: 'Failed to connect to opencode events' })}\n\n`);
806
+ res.end();
807
+ return;
808
+ }
809
+ const reader = response.body.getReader();
810
+ const decoder = new TextDecoder();
811
+ let buffer = '';
812
+ while (true) {
813
+ const { done, value } = await reader.read();
814
+ if (done) {
815
+ break;
816
+ }
817
+ buffer += decoder.decode(value, { stream: true });
818
+ // Process complete SSE messages
819
+ const lines = buffer.split('\n');
820
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
821
+ for (const line of lines) {
822
+ if (line.startsWith('data:')) {
823
+ const data = line.slice(5).trim();
824
+ if (data) {
825
+ try {
826
+ const event = JSON.parse(data);
827
+ const eventSessionId = getEventSessionId(event);
828
+ // Filter events to only include ones for this session
829
+ if (eventSessionId === session.opencode_session_id) {
830
+ const sanitizedEvent = sanitizeEventForClient(event);
831
+ res.write(`data: ${JSON.stringify((0, redact_1.sanitizeForClient)(sanitizedEvent))}\n\n`);
832
+ }
833
+ }
834
+ catch {
835
+ // Skip malformed JSON
836
+ }
837
+ }
838
+ }
839
+ }
840
+ }
841
+ }
842
+ catch (err) {
843
+ if (err.name !== 'AbortError') {
844
+ res.write(`data: ${JSON.stringify({ type: 'error', message: 'SSE stream error' })}\n\n`);
845
+ }
846
+ }
847
+ res.end();
848
+ }, true));
849
+ exports.default = router;