teamcopilot 0.1.2 → 0.1.4

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 (35) hide show
  1. package/dist/chat/index.js +93 -12
  2. package/dist/frontend/assets/{cssMode-DPbQk08M.js → cssMode-DHaUFfVN.js} +1 -1
  3. package/dist/frontend/assets/{freemarker2-DFE4Z2hp.js → freemarker2-CTVtohp4.js} +1 -1
  4. package/dist/frontend/assets/{handlebars-Dw_PK21Y.js → handlebars-CI1GoG3L.js} +1 -1
  5. package/dist/frontend/assets/{html-DxZRNRpl.js → html-C1Kdg4Tl.js} +1 -1
  6. package/dist/frontend/assets/{htmlMode-iGgmnmtJ.js → htmlMode-CslwWXHV.js} +1 -1
  7. package/dist/frontend/assets/{index-CphuwuEr.js → index-CJMxNDke.js} +205 -205
  8. package/dist/frontend/assets/index-DrplZfJY.css +1 -0
  9. package/dist/frontend/assets/{javascript-Bw_u6aiq.js → javascript-D_jyP6QE.js} +1 -1
  10. package/dist/frontend/assets/{jsonMode-iBiD9w9m.js → jsonMode-BT5z_9Wi.js} +1 -1
  11. package/dist/frontend/assets/{liquid-nnkE2sbw.js → liquid-CATjnK_z.js} +1 -1
  12. package/dist/frontend/assets/{mdx-CHi7ETpD.js → mdx-B4zdWJY_.js} +1 -1
  13. package/dist/frontend/assets/{python--OTnmJv2.js → python-B_opZ_lQ.js} +1 -1
  14. package/dist/frontend/assets/{razor-C6OgnQD9.js → razor-BKopqk1g.js} +1 -1
  15. package/dist/frontend/assets/{tsMode-DzsKqqZD.js → tsMode-CMjXzcRc.js} +1 -1
  16. package/dist/frontend/assets/{typescript-BMZ0H3W7.js → typescript-lbL28oip.js} +1 -1
  17. package/dist/frontend/assets/{xml-HTDEz_LP.js → xml-BB8-GMIl.js} +1 -1
  18. package/dist/frontend/assets/{yaml-CVa5ypgE.js → yaml-B5S_93u4.js} +1 -1
  19. package/dist/frontend/index.html +2 -2
  20. package/dist/utils/approval-snapshot-common.js +2 -0
  21. package/dist/utils/chat-session-file-diff.js +120 -0
  22. package/dist/utils/workspace-sync.js +24 -0
  23. package/dist/workspace_files/.opencode/plugins/apply-patch-session-diff.ts +343 -0
  24. package/dist/workspace_files/package.json +1 -1
  25. package/package.json +1 -1
  26. package/prisma/generated/client/edge.js +18 -3
  27. package/prisma/generated/client/index-browser.js +15 -0
  28. package/prisma/generated/client/index.d.ts +2067 -173
  29. package/prisma/generated/client/index.js +18 -3
  30. package/prisma/generated/client/package.json +1 -1
  31. package/prisma/generated/client/schema.prisma +22 -3
  32. package/prisma/generated/client/wasm.js +18 -3
  33. package/prisma/migrations/20260324144454_add_chat_session_tracked_files/migration.sql +21 -0
  34. package/prisma/schema.prisma +20 -1
  35. package/dist/frontend/assets/index-Ds8n3J4W.css +0 -1
@@ -14,6 +14,7 @@ const path_1 = __importDefault(require("path"));
14
14
  const ignore_1 = __importDefault(require("ignore"));
15
15
  const child_process_1 = require("child_process");
16
16
  const util_1 = require("util");
17
+ const crypto_1 = __importDefault(require("crypto"));
17
18
  const assert_1 = require("./assert");
18
19
  const runtime_paths_1 = require("./runtime-paths");
19
20
  const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
@@ -22,6 +23,7 @@ const WORKSPACE_DB_FILENAME = "data.db";
22
23
  const HONEYTOKEN_UUID = "1f9f0b72-5f9f-4c9b-aef1-2fb2e0f6d8c4";
23
24
  const HONEYTOKEN_FILE_NAME = `honeytoken-${HONEYTOKEN_UUID}.txt`;
24
25
  const WORKSPACE_AZURE_PROVIDER_VERSION = "3.0.48";
26
+ const WORKSPACE_INSTALL_STATE_RELATIVE_PATH = path_1.default.join(".opencode", "install-state.json");
25
27
  function getWorkspaceDirFromEnv() {
26
28
  let workspaceDir = (0, assert_1.assertEnv)("WORKSPACE_DIR");
27
29
  if (!path_1.default.isAbsolute(workspaceDir)) {
@@ -59,6 +61,9 @@ function shouldSkipManagedDirectoryContent(relativePath) {
59
61
  if (relativePath === ".opencode/xdg-data" || relativePath.startsWith(".opencode/xdg-data/")) {
60
62
  return true;
61
63
  }
64
+ if (relativePath === normalizeRelativePath(WORKSPACE_INSTALL_STATE_RELATIVE_PATH)) {
65
+ return true;
66
+ }
62
67
  return false;
63
68
  }
64
69
  function evaluateIgnoreRuleSets(relativePath, isDirectory, ruleSets) {
@@ -204,11 +209,30 @@ async function initializeWorkspaceNodeDependencies(workspaceDir) {
204
209
  ...existingPackageJson,
205
210
  dependencies,
206
211
  };
212
+ const installStatePath = path_1.default.join(workspaceDir, WORKSPACE_INSTALL_STATE_RELATIVE_PATH);
213
+ const desiredInstallState = {
214
+ dependencies,
215
+ };
216
+ const desiredInstallStateHash = crypto_1.default
217
+ .createHash("sha256")
218
+ .update(JSON.stringify(desiredInstallState))
219
+ .digest("hex");
220
+ const existingInstallState = fs_1.default.existsSync(installStatePath)
221
+ ? JSON.parse(fs_1.default.readFileSync(installStatePath, "utf-8"))
222
+ : null;
223
+ const hasMatchingInstallState = existingInstallState?.hash === desiredInstallStateHash;
224
+ const nodeModulesPath = path_1.default.join(workspaceDir, "node_modules");
225
+ const packageJsonMatches = JSON.stringify(existingPackageJson) === JSON.stringify(workspacePackageJson);
226
+ if (hasMatchingInstallState && packageJsonMatches && fs_1.default.existsSync(nodeModulesPath)) {
227
+ return;
228
+ }
207
229
  fs_1.default.writeFileSync(workspacePackageJsonPath, JSON.stringify(workspacePackageJson, null, 2), "utf-8");
208
230
  await execFileAsync("npm", ["install"], {
209
231
  cwd: workspaceDir,
210
232
  env: process.env,
211
233
  });
234
+ fs_1.default.mkdirSync(path_1.default.dirname(installStatePath), { recursive: true });
235
+ fs_1.default.writeFileSync(installStatePath, `${JSON.stringify({ hash: desiredInstallStateHash }, null, 2)}\n`, "utf-8");
212
236
  }
213
237
  async function initializeWorkspaceDirectory() {
214
238
  const workspaceDir = getWorkspaceDirFromEnv();
@@ -0,0 +1,343 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import type { Plugin } from "@opencode-ai/plugin"
4
+
5
+ function getApiBaseUrl(): string {
6
+ const port = process.env.TEAMCOPILOT_PORT?.trim()
7
+ if (!port) {
8
+ throw new Error("TEAMCOPILOT_PORT must be set.")
9
+ }
10
+ return `http://localhost:${port}`
11
+ }
12
+
13
+ interface SessionLookupResponse {
14
+ error?: unknown
15
+ data?: {
16
+ id?: string
17
+ parentID?: string
18
+ }
19
+ }
20
+
21
+ type ToolArgs = Record<string, unknown> | undefined
22
+
23
+ function findPatchPayload(value: unknown): string | null {
24
+ if (typeof value === "string") {
25
+ const normalized = value.replace(/\r\n/g, "\n")
26
+ if (normalized.includes("*** Begin Patch")) {
27
+ return normalized
28
+ }
29
+ return null
30
+ }
31
+
32
+ if (!value || typeof value !== "object") {
33
+ return null
34
+ }
35
+
36
+ for (const nestedValue of Object.values(value as Record<string, unknown>)) {
37
+ const match = findPatchPayload(nestedValue)
38
+ if (match) {
39
+ return match
40
+ }
41
+ }
42
+
43
+ return null
44
+ }
45
+
46
+ function extractTrackedPathsFromPatch(patchText: string): string[] {
47
+ const normalized = patchText.replace(/\r\n/g, "\n")
48
+ const fileHeaderMatches = normalized.matchAll(/^\s*\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)
49
+ const paths = Array.from(fileHeaderMatches, (match) => match[1].trim()).filter(
50
+ (candidate) => candidate.length > 0
51
+ )
52
+
53
+ if (paths.length === 0) {
54
+ throw new Error("Could not determine target path from apply_patch payload.")
55
+ }
56
+
57
+ const moveToMatches = normalized.matchAll(/^\s*\*\*\* Move to: (.+)$/gm)
58
+ for (const match of moveToMatches) {
59
+ const targetPath = match[1].trim()
60
+ if (targetPath.length > 0) {
61
+ paths.push(targetPath)
62
+ }
63
+ }
64
+
65
+ return Array.from(new Set(paths.filter((candidate) => candidate.length > 0)))
66
+ }
67
+
68
+ function findNestedStringByKeys(value: unknown, keys: Set<string>): string | null {
69
+ if (!value || typeof value !== "object") {
70
+ return null
71
+ }
72
+
73
+ for (const [key, nestedValue] of Object.entries(value as Record<string, unknown>)) {
74
+ if (keys.has(key) && typeof nestedValue === "string" && nestedValue.trim().length > 0) {
75
+ return nestedValue
76
+ }
77
+
78
+ const nestedMatch = findNestedStringByKeys(nestedValue, keys)
79
+ if (nestedMatch) {
80
+ return nestedMatch
81
+ }
82
+ }
83
+
84
+ return null
85
+ }
86
+
87
+ function tokenizeCommand(command: string): string[] {
88
+ const tokens = command.match(/"[^"]*"|'[^']*'|&&|\|\||[;|]|[^\s]+/g) ?? []
89
+ return tokens.map((token) => {
90
+ if (
91
+ (token.startsWith("\"") && token.endsWith("\"")) ||
92
+ (token.startsWith("'") && token.endsWith("'"))
93
+ ) {
94
+ return token.slice(1, -1)
95
+ }
96
+
97
+ return token
98
+ })
99
+ }
100
+
101
+ const CONTROL_TOKENS = new Set(["&&", "||", ";", "|"])
102
+
103
+ function resolveExecutionDirectory(rawCwd: unknown, fallbackDirectory: string): string {
104
+ if (typeof rawCwd !== "string" || rawCwd.trim() === "") {
105
+ return fallbackDirectory
106
+ }
107
+
108
+ return path.isAbsolute(rawCwd) ? rawCwd : path.resolve(fallbackDirectory, rawCwd)
109
+ }
110
+
111
+ function resolveTrackedDeleteTargets(command: string, executionDirectory: string): string[] {
112
+ const tokens = tokenizeCommand(command.trim())
113
+ if (tokens.length === 0) {
114
+ return []
115
+ }
116
+
117
+ const resolvedTargets: string[] = []
118
+ let currentDirectory = executionDirectory
119
+ let index = 0
120
+ let atCommandStart = true
121
+
122
+ while (index < tokens.length) {
123
+ const token = tokens[index]
124
+
125
+ if (CONTROL_TOKENS.has(token)) {
126
+ atCommandStart = true
127
+ index += 1
128
+ continue
129
+ }
130
+
131
+ if (!atCommandStart) {
132
+ index += 1
133
+ continue
134
+ }
135
+
136
+ if (token === "cd") {
137
+ const destination = tokens[index + 1]
138
+ if (destination && !CONTROL_TOKENS.has(destination)) {
139
+ currentDirectory = path.isAbsolute(destination)
140
+ ? path.resolve(destination)
141
+ : path.resolve(currentDirectory, destination)
142
+ index += 2
143
+ } else {
144
+ index += 1
145
+ }
146
+ atCommandStart = false
147
+ continue
148
+ }
149
+
150
+ if (path.basename(token) === "rm") {
151
+ index += 1
152
+ while (index < tokens.length && !CONTROL_TOKENS.has(tokens[index])) {
153
+ const candidate = tokens[index]
154
+ if (candidate === "--" || candidate.startsWith("-")) {
155
+ index += 1
156
+ continue
157
+ }
158
+
159
+ const resolvedCandidate = path.isAbsolute(candidate)
160
+ ? path.resolve(candidate)
161
+ : path.resolve(currentDirectory, candidate)
162
+ if (fs.existsSync(resolvedCandidate) && fs.statSync(resolvedCandidate).isDirectory()) {
163
+ index += 1
164
+ continue
165
+ }
166
+ resolvedTargets.push(resolvedCandidate)
167
+ index += 1
168
+ }
169
+ atCommandStart = false
170
+ continue
171
+ }
172
+
173
+ atCommandStart = false
174
+ index += 1
175
+ }
176
+
177
+ return Array.from(new Set(resolvedTargets))
178
+ }
179
+
180
+ function normalizeRelativePath(rawPath: string, workspaceDir: string): string {
181
+ const trimmed = rawPath.trim()
182
+ if (!trimmed) {
183
+ throw new Error("apply_patch hook received an empty file path.")
184
+ }
185
+
186
+ const resolvedPath = path.isAbsolute(trimmed)
187
+ ? path.resolve(trimmed)
188
+ : path.resolve(workspaceDir, trimmed)
189
+ const relativePath = path.relative(workspaceDir, resolvedPath)
190
+
191
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
192
+ throw new Error(`apply_patch path '${trimmed}' is outside the workspace.`)
193
+ }
194
+
195
+ return relativePath.split(path.sep).join("/")
196
+ }
197
+
198
+ function tryNormalizeRelativePath(rawPath: string, workspaceDir: string): string | null {
199
+ try {
200
+ return normalizeRelativePath(rawPath, workspaceDir)
201
+ } catch {
202
+ return null
203
+ }
204
+ }
205
+
206
+ async function readErrorMessageFromResponse(
207
+ response: Response,
208
+ fallbackMessage: string
209
+ ): Promise<string> {
210
+ try {
211
+ const text = await response.text()
212
+ if (!text) return fallbackMessage
213
+ try {
214
+ const parsed: unknown = JSON.parse(text)
215
+ if (parsed && typeof parsed === "object" && "message" in parsed) {
216
+ const message = (parsed as { message?: unknown }).message
217
+ if (typeof message === "string" && message.trim().length > 0) {
218
+ return message
219
+ }
220
+ }
221
+ } catch {
222
+ // Fall back to plain text.
223
+ }
224
+ return text.trim().length > 0 ? text : fallbackMessage
225
+ } catch {
226
+ return fallbackMessage
227
+ }
228
+ }
229
+
230
+ function collectTrackedPathsForTool(
231
+ tool: string,
232
+ inputArgs: ToolArgs,
233
+ outputArgs: ToolArgs,
234
+ workspaceDir: string
235
+ ): string[] {
236
+ if (tool === "apply_patch") {
237
+ const patchPayload = findPatchPayload(outputArgs) ?? findPatchPayload(inputArgs)
238
+ if (!patchPayload) {
239
+ return []
240
+ }
241
+
242
+ return extractTrackedPathsFromPatch(patchPayload)
243
+ }
244
+
245
+ if (tool === "write") {
246
+ const filepath =
247
+ findNestedStringByKeys(outputArgs, new Set(["filepath", "filePath"])) ??
248
+ findNestedStringByKeys(inputArgs, new Set(["filepath", "filePath"]))
249
+ if (!filepath) {
250
+ return []
251
+ }
252
+
253
+ return [filepath]
254
+ }
255
+
256
+ if (tool === "bash") {
257
+ const command =
258
+ findNestedStringByKeys(outputArgs, new Set(["command", "cmd", "script", "arguments"])) ??
259
+ findNestedStringByKeys(inputArgs, new Set(["command", "cmd", "script", "arguments"]))
260
+ if (!command) {
261
+ return []
262
+ }
263
+
264
+ const rawCwd =
265
+ (outputArgs && (outputArgs.workdir ?? outputArgs.cwd)) ??
266
+ (inputArgs && (inputArgs.workdir ?? inputArgs.cwd))
267
+ const executionDirectory = resolveExecutionDirectory(rawCwd, workspaceDir)
268
+
269
+ return resolveTrackedDeleteTargets(command, executionDirectory)
270
+ }
271
+
272
+ return []
273
+ }
274
+
275
+ export const ApplyPatchSessionDiffPlugin: Plugin = async ({ client, directory }) => {
276
+ async function resolveRootSessionID(sessionID: string): Promise<string> {
277
+ let currentSessionID = sessionID
278
+
279
+ while (true) {
280
+ const response = (await client.session.get({
281
+ path: {
282
+ id: currentSessionID,
283
+ },
284
+ })) as SessionLookupResponse
285
+ if (response.error) {
286
+ throw new Error(`Failed to resolve root session for ${currentSessionID}`)
287
+ }
288
+
289
+ const parentID = response.data?.parentID
290
+ if (!parentID) {
291
+ return currentSessionID
292
+ }
293
+
294
+ currentSessionID = parentID
295
+ }
296
+ }
297
+
298
+ async function captureBaselineForPath(sessionID: string, relativePath: string): Promise<void> {
299
+ const response = await fetch(`${getApiBaseUrl()}/api/chat/sessions/file-diff/capture-baseline`, {
300
+ method: "POST",
301
+ headers: {
302
+ "Content-Type": "application/json",
303
+ Authorization: `Bearer ${sessionID}`,
304
+ },
305
+ body: JSON.stringify({
306
+ path: relativePath,
307
+ }),
308
+ })
309
+
310
+ if (!response.ok) {
311
+ const errorMessage = await readErrorMessageFromResponse(
312
+ response,
313
+ `Failed to capture file baseline for ${relativePath} (HTTP ${response.status})`
314
+ )
315
+ throw new Error(errorMessage)
316
+ }
317
+ }
318
+
319
+ return {
320
+ "tool.execute.before": async (input, output) => {
321
+ if (!["apply_patch", "write", "bash"].includes(input.tool)) {
322
+ return
323
+ }
324
+
325
+ const rawSessionID = typeof input.sessionID === "string" ? input.sessionID.trim() : ""
326
+ if (!rawSessionID) {
327
+ return
328
+ }
329
+
330
+ const rootSessionID = await resolveRootSessionID(rawSessionID)
331
+ const paths = collectTrackedPathsForTool(input.tool, input.args, output.args, directory)
332
+ for (const candidatePath of paths) {
333
+ const relativePath = tryNormalizeRelativePath(candidatePath, directory)
334
+ if (!relativePath) {
335
+ continue
336
+ }
337
+ await captureBaselineForPath(rootSessionID, relativePath)
338
+ }
339
+ },
340
+ }
341
+ }
342
+
343
+ export default ApplyPatchSessionDiffPlugin
@@ -4,4 +4,4 @@
4
4
  },
5
5
  "devDependencies": {},
6
6
  "scripts": {}
7
- }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamcopilot",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A shared AI Agent for Teams",
5
5
  "homepage": "https://teamcopilot.ai",
6
6
  "repository": {