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.
- package/.env.example +10 -0
- package/LICENSE.md +21 -0
- package/README.md +131 -0
- package/bin/teamcopilot.js +281 -0
- package/dist/auth/index.js +189 -0
- package/dist/change-user-role.js +77 -0
- package/dist/chat/index.js +849 -0
- package/dist/constants.js +2 -0
- package/dist/create-user.js +98 -0
- package/dist/cronjob/index.js +16 -0
- package/dist/cronjob/resource-reconciliation.js +33 -0
- package/dist/delete-user.js +66 -0
- package/dist/frontend/assets/abap-CRCWOmpq.js +1 -0
- package/dist/frontend/assets/apex-DnsZk_dE.js +1 -0
- package/dist/frontend/assets/azcli-1IWB1ccx.js +1 -0
- package/dist/frontend/assets/bat-DPkNLes8.js +1 -0
- package/dist/frontend/assets/bicep-Corcdgou.js +2 -0
- package/dist/frontend/assets/cameligo-CGrWLZr3.js +1 -0
- package/dist/frontend/assets/clojure-D9WOWImG.js +1 -0
- package/dist/frontend/assets/codicon-DCmgc-ay.ttf +0 -0
- package/dist/frontend/assets/coffee-B7EJu28W.js +1 -0
- package/dist/frontend/assets/cpp-SEyurbux.js +1 -0
- package/dist/frontend/assets/csharp-BoL64M5l.js +1 -0
- package/dist/frontend/assets/csp-C46ZqvIl.js +1 -0
- package/dist/frontend/assets/css-DQU6DXDx.js +3 -0
- package/dist/frontend/assets/cssMode-BDT3WbVs.js +4 -0
- package/dist/frontend/assets/cypher-D84EuPTj.js +1 -0
- package/dist/frontend/assets/dart-D8lhlL1r.js +1 -0
- package/dist/frontend/assets/dockerfile-DLk6rpji.js +1 -0
- package/dist/frontend/assets/ecl-BO6FnfXk.js +1 -0
- package/dist/frontend/assets/editor.worker-B4pQIWZD.js +12 -0
- package/dist/frontend/assets/elixir-BRjLKONM.js +1 -0
- package/dist/frontend/assets/flow9-Cac8vKd7.js +1 -0
- package/dist/frontend/assets/freemarker2-C7-hEgID.js +3 -0
- package/dist/frontend/assets/fsharp-fd1GTHhf.js +1 -0
- package/dist/frontend/assets/go-O9LJTZXk.js +1 -0
- package/dist/frontend/assets/graphql-LQdxqEYJ.js +1 -0
- package/dist/frontend/assets/handlebars-4cwTkPir.js +1 -0
- package/dist/frontend/assets/hcl-DxDQ3s82.js +1 -0
- package/dist/frontend/assets/html-YNfE1Q0A.js +1 -0
- package/dist/frontend/assets/htmlMode-opTQ1HoB.js +4 -0
- package/dist/frontend/assets/index-DWyaVa1h.js +782 -0
- package/dist/frontend/assets/index-lXrsgeTF.css +1 -0
- package/dist/frontend/assets/ini-BvajGCUy.js +1 -0
- package/dist/frontend/assets/java-SYsfObOQ.js +1 -0
- package/dist/frontend/assets/javascript-BEwGzk7T.js +1 -0
- package/dist/frontend/assets/jsonMode-CGhIS5Al.js +10 -0
- package/dist/frontend/assets/julia-DQXNmw_w.js +1 -0
- package/dist/frontend/assets/kotlin-qQ0MG-9I.js +1 -0
- package/dist/frontend/assets/less-GGFNNJHn.js +2 -0
- package/dist/frontend/assets/lexon-Canl7DCW.js +1 -0
- package/dist/frontend/assets/liquid-QekTGCGJ.js +1 -0
- package/dist/frontend/assets/lua-D28Ae8-K.js +1 -0
- package/dist/frontend/assets/m3-DPitgjJI.js +1 -0
- package/dist/frontend/assets/markdown-B811l8j2.js +1 -0
- package/dist/frontend/assets/mdx-BAVDaB7v.js +1 -0
- package/dist/frontend/assets/mips-CdjsipkG.js +1 -0
- package/dist/frontend/assets/msdax-CYqgjx_P.js +1 -0
- package/dist/frontend/assets/mysql-BHd6q0vd.js +1 -0
- package/dist/frontend/assets/objective-c-B1aVtJYH.js +1 -0
- package/dist/frontend/assets/pascal-BhNW15KB.js +1 -0
- package/dist/frontend/assets/pascaligo-5jv8CcQD.js +1 -0
- package/dist/frontend/assets/perl-DlYyT36c.js +1 -0
- package/dist/frontend/assets/pgsql-Dy0bjov7.js +1 -0
- package/dist/frontend/assets/php-120yhfDK.js +1 -0
- package/dist/frontend/assets/pla-CjnFlu4u.js +1 -0
- package/dist/frontend/assets/postiats-CQpG440k.js +1 -0
- package/dist/frontend/assets/powerquery-DdJtto1Z.js +1 -0
- package/dist/frontend/assets/powershell-Bu_VLpJB.js +1 -0
- package/dist/frontend/assets/protobuf-IBS6jZEB.js +2 -0
- package/dist/frontend/assets/pug-kFxLfcjb.js +1 -0
- package/dist/frontend/assets/python-BQlHw7XO.js +1 -0
- package/dist/frontend/assets/qsharp-q7JyzKFN.js +1 -0
- package/dist/frontend/assets/r-BIFz-_sK.js +1 -0
- package/dist/frontend/assets/razor-Be3Wwc2E.js +1 -0
- package/dist/frontend/assets/redis-CHOsPHWR.js +1 -0
- package/dist/frontend/assets/redshift-CBifECDb.js +1 -0
- package/dist/frontend/assets/restructuredtext-CghPJEOS.js +1 -0
- package/dist/frontend/assets/ruby-CYWGW-b1.js +1 -0
- package/dist/frontend/assets/rust-DMDD0SHb.js +1 -0
- package/dist/frontend/assets/sb-BYAiYHFx.js +1 -0
- package/dist/frontend/assets/scala-Bqvq8jcR.js +1 -0
- package/dist/frontend/assets/scheme-Dhb-2j9p.js +1 -0
- package/dist/frontend/assets/scss-CTwUZ5N7.js +3 -0
- package/dist/frontend/assets/shell-CsDZo4DB.js +1 -0
- package/dist/frontend/assets/solidity-CME5AdoB.js +1 -0
- package/dist/frontend/assets/sophia-RYC1BQQz.js +1 -0
- package/dist/frontend/assets/sparql-KEyrF7De.js +1 -0
- package/dist/frontend/assets/sql-BdTr02Mf.js +1 -0
- package/dist/frontend/assets/st-C7iG7M4S.js +1 -0
- package/dist/frontend/assets/swift-D7IUmUK8.js +1 -0
- package/dist/frontend/assets/systemverilog-DgMryOEJ.js +1 -0
- package/dist/frontend/assets/tcl-PloMZuKG.js +1 -0
- package/dist/frontend/assets/tsMode-CIBFoN3z.js +11 -0
- package/dist/frontend/assets/twig-BfRIq3la.js +1 -0
- package/dist/frontend/assets/typescript-BuV9wEIE.js +1 -0
- package/dist/frontend/assets/typespec-CzxlYoT_.js +1 -0
- package/dist/frontend/assets/vb-BwAE3J76.js +1 -0
- package/dist/frontend/assets/wgsl-B_1kOXbF.js +298 -0
- package/dist/frontend/assets/xml-DcDKYaM4.js +1 -0
- package/dist/frontend/assets/yaml-CuBNmOuI.js +1 -0
- package/dist/frontend/index.html +14 -0
- package/dist/frontend/logo.svg +50 -0
- package/dist/index.js +169 -0
- package/dist/logging.js +30 -0
- package/dist/opencode-auth/index.js +122 -0
- package/dist/opencode-server.js +91 -0
- package/dist/prisma/client.js +38 -0
- package/dist/reset-password.js +73 -0
- package/dist/rotate-jwt-secret.js +20 -0
- package/dist/scripts/prisma-workspace.js +34 -0
- package/dist/skills/index.js +311 -0
- package/dist/types/permissions.js +2 -0
- package/dist/types/shared/permissions.js +17 -0
- package/dist/types/shared/skill.js +17 -0
- package/dist/types/shared/workflow-files.js +17 -0
- package/dist/types/shared/workflow.js +17 -0
- package/dist/types/skill.js +2 -0
- package/dist/types/workflow-files.js +2 -0
- package/dist/types/workflow.js +2 -0
- package/dist/users/index.js +22 -0
- package/dist/utils/approval-snapshot-common.js +596 -0
- package/dist/utils/assert.js +20 -0
- package/dist/utils/chat-session.js +44 -0
- package/dist/utils/cli-bootstrap.js +26 -0
- package/dist/utils/index.js +95 -0
- package/dist/utils/jwt-secret.js +63 -0
- package/dist/utils/opencode-auth.js +126 -0
- package/dist/utils/opencode-client.js +109 -0
- package/dist/utils/password-policy.js +12 -0
- package/dist/utils/permission-common.js +280 -0
- package/dist/utils/redact.js +108 -0
- package/dist/utils/resource-access.js +37 -0
- package/dist/utils/resource-file-routes.js +115 -0
- package/dist/utils/resource-files.js +572 -0
- package/dist/utils/runtime-paths.js +61 -0
- package/dist/utils/session-abort.js +52 -0
- package/dist/utils/skill-approval-snapshot.js +39 -0
- package/dist/utils/skill-files.js +17 -0
- package/dist/utils/skill-permissions.js +15 -0
- package/dist/utils/skill.js +217 -0
- package/dist/utils/user-role.js +14 -0
- package/dist/utils/workflow-approval-snapshot.js +38 -0
- package/dist/utils/workflow-files.js +17 -0
- package/dist/utils/workflow-interruption.js +50 -0
- package/dist/utils/workflow-permissions.js +27 -0
- package/dist/utils/workflow-runner.js +414 -0
- package/dist/utils/workflow.js +158 -0
- package/dist/utils/workspace-sync.js +204 -0
- package/dist/workflows/index.js +751 -0
- package/dist/workspace_files/.opencode/opencode.json +17 -0
- package/dist/workspace_files/.opencode/package.json +14 -0
- package/dist/workspace_files/.opencode/plugins/createSkill.ts +339 -0
- package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +345 -0
- package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +173 -0
- package/dist/workspace_files/.opencode/plugins/findSkill.ts +211 -0
- package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +135 -0
- package/dist/workspace_files/.opencode/plugins/honeytoken-protection.ts +64 -0
- package/dist/workspace_files/.opencode/plugins/listAvailableSkills.ts +93 -0
- package/dist/workspace_files/.opencode/plugins/listAvailableWorkflows.ts +93 -0
- package/dist/workspace_files/.opencode/plugins/python-protection.ts +184 -0
- package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +168 -0
- package/dist/workspace_files/.opencode/tsconfig.json +16 -0
- package/dist/workspace_files/AGENTS.md +483 -0
- package/dist/workspace_files/package-lock.json +167 -0
- package/dist/workspace_files/package.json +5 -0
- package/package.json +86 -0
- package/prisma/migrations/20260203040755_init/migration.sql +20 -0
- package/prisma/migrations/20260204034845_replace_google_auth_with_email_password/migration.sql +25 -0
- package/prisma/migrations/20260207022226_add_user_role/migration.sql +25 -0
- package/prisma/migrations/20260210161254_add_workflow_runs/migration.sql +16 -0
- package/prisma/migrations/20260211050606_adds_workflow_table/migration.sql +40 -0
- package/prisma/migrations/20260211050750_adds_fkey_constraint/migration.sql +21 -0
- package/prisma/migrations/20260211051912_removes_workflow_table/migration.sql +34 -0
- package/prisma/migrations/20260211052238_changes_workflow_id_to_slug/migration.sql +27 -0
- package/prisma/migrations/20260212051912_add_output_to_workflow_runs/migration.sql +2 -0
- package/prisma/migrations/20260213073006_add_chat_sessions/migration.sql +13 -0
- package/prisma/migrations/20260216053202_add_chat_sessions_opencode_session_id_idx/migration.sql +2 -0
- package/prisma/migrations/20260216053237_drop_redundant_chat_sessions_opencode_idx/migration.sql +2 -0
- package/prisma/migrations/20260219060705_makes/migration.sql +24 -0
- package/prisma/migrations/20260222040542_add_workflow_execution_permissions/migration.sql +18 -0
- package/prisma/migrations/20260222040815_remove_workflow_execution_permissions/migration.sql +10 -0
- package/prisma/migrations/20260222041348_add_workflow_execution_permissions_final/migration.sql +17 -0
- package/prisma/migrations/20260222041741_rename_to_tool_execution_permissions/migration.sql +30 -0
- package/prisma/migrations/20260222041826_simplify_tool_execution_permissions/migration.sql +29 -0
- package/prisma/migrations/20260222041950_add_fields_for_standalone_permissions/migration.sql +32 -0
- package/prisma/migrations/20260222042954_simplify_tool_permissions_table/migration.sql +27 -0
- package/prisma/migrations/20260223073902_add_workflow_run_permissions_tables/migration.sql +23 -0
- package/prisma/migrations/20260225025151_add_workflow_metadata/migration.sql +16 -0
- package/prisma/migrations/20260225031035_merge_workflow_permissions_into_metadata/migration.sql +44 -0
- package/prisma/migrations/20260225031752_removes_default_for_run_permission_mode/migration.sql +20 -0
- package/prisma/migrations/20260225033603_remove_workflow_metadata_user_fkeys/migration.sql +18 -0
- package/prisma/migrations/20260225043032_restore_workflow_metadata_user_fkeys/migration.sql +20 -0
- package/prisma/migrations/20260225091423_add_workflow_approved_snapshots/migration.sql +28 -0
- package/prisma/migrations/20260226032121_add_is_approved_to_workflow_metadata/migration.sql +21 -0
- package/prisma/migrations/20260226032444_undoes_last_db_change/migration.sql +26 -0
- package/prisma/migrations/20260227120000_remove_snapshot_hash_from_approved_snapshots/migration.sql +16 -0
- package/prisma/migrations/20260228071125_adds_workspace_path_to_snapshot_table/migration.sql +22 -0
- package/prisma/migrations/20260228071217_modifies_index_and_removes_default_value/migration.sql +22 -0
- package/prisma/migrations/20260228071710_undoes_previous/migration.sql +27 -0
- package/prisma/migrations/20260228105022_add_must_change_password_first_login/migration.sql +20 -0
- package/prisma/migrations/20260301115439_add_workflow_run_log_refs/migration.sql +8 -0
- package/prisma/migrations/20260301122557_add_workflow_aborted_sessions/migration.sql +5 -0
- package/prisma/migrations/20260302045545_move_workflow_run_log_refs_into_workflow_runs/migration.sql +17 -0
- package/prisma/migrations/20260303040318_add_skill_tables/migration.sql +61 -0
- package/prisma/migrations/20260303051533_unify_resource_permissions/migration.sql +97 -0
- package/prisma/migrations/20260303064255_unify_resource_metadata_and_snapshots/migration.sql +179 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- 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;
|