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,93 @@
1
+ import { type Plugin, tool } from "@opencode-ai/plugin"
2
+
3
+ function getApiBaseUrl(): string {
4
+ const port = process.env.TEAMCOPILOT_PORT?.trim()
5
+ if (!port) {
6
+ throw new Error("TEAMCOPILOT_PORT must be set.")
7
+ }
8
+ return `http://localhost:${port}`
9
+ }
10
+
11
+
12
+
13
+ interface WorkflowSummary {
14
+ slug: string
15
+ name: string
16
+ intent_summary: string
17
+ is_approved: boolean
18
+ can_edit: boolean
19
+ }
20
+
21
+ async function readErrorMessageFromResponse(
22
+ response: Response,
23
+ fallbackMessage: string
24
+ ): Promise<string> {
25
+ try {
26
+ const text = await response.text()
27
+ if (!text) return fallbackMessage
28
+ try {
29
+ const parsed: unknown = JSON.parse(text)
30
+ if (parsed && typeof parsed === "object" && "message" in parsed) {
31
+ const msg = (parsed as { message?: unknown }).message
32
+ if (typeof msg === "string" && msg.trim().length > 0) return msg
33
+ }
34
+ } catch {
35
+ // fall back to plain text
36
+ }
37
+ return text.trim().length > 0 ? text : fallbackMessage
38
+ } catch {
39
+ return fallbackMessage
40
+ }
41
+ }
42
+
43
+ export const ListAvailableWorkflowsPlugin: Plugin = async (_ctx) => {
44
+ return {
45
+ tool: {
46
+ listAvailableWorkflows: tool({
47
+ description:
48
+ "List workflows that are available for this user to use. Returns all workflows the current user can access, including those pending approval.",
49
+ args: {},
50
+ async execute(_args, context) {
51
+ const { sessionID } = context
52
+
53
+ const response = await fetch(`${getApiBaseUrl()}/api/workflows`, {
54
+ headers: {
55
+ Authorization: `Bearer ${sessionID}`,
56
+ },
57
+ })
58
+
59
+ if (!response.ok) {
60
+ const errorMessage = await readErrorMessageFromResponse(
61
+ response,
62
+ `Failed to list workflows (HTTP ${response.status})`
63
+ )
64
+ throw new Error(errorMessage)
65
+ }
66
+
67
+ const payload = (await response.json()) as {
68
+ workflows?: WorkflowSummary[]
69
+ }
70
+
71
+ const availableWorkflows = (payload.workflows ?? [])
72
+ .map((workflow) => ({
73
+ slug: workflow.slug,
74
+ name: workflow.name,
75
+ intent_summary: workflow.intent_summary,
76
+ is_approved: workflow.is_approved,
77
+ }))
78
+
79
+ return JSON.stringify(
80
+ {
81
+ workflows: availableWorkflows,
82
+ total: availableWorkflows.length,
83
+ },
84
+ null,
85
+ 2
86
+ )
87
+ },
88
+ }),
89
+ },
90
+ }
91
+ }
92
+
93
+ export default ListAvailableWorkflowsPlugin
@@ -0,0 +1,184 @@
1
+ import type { Plugin } from "@opencode-ai/plugin"
2
+ import type { Dirent } from "node:fs"
3
+ import fs from "node:fs/promises"
4
+ import path from "node:path"
5
+
6
+ const pythonInterpreterPattern =
7
+ /(^|[\s;|&()])(?:\/[\w./-]*python(?:\d+(?:\.\d+)*)?|python(?:\d+(?:\.\d+)*)?|py|pypy)(?=$|[\s;|&()])/i
8
+
9
+ const tokenizeCommand = (command: string): string[] => {
10
+ const tokens = command.match(/"[^"]*"|'[^']*'|&&|\|\||[;|]|[^\s]+/g) ?? []
11
+ return tokens.map((token) => {
12
+ if (
13
+ (token.startsWith("\"") && token.endsWith("\"")) ||
14
+ (token.startsWith("'") && token.endsWith("'"))
15
+ ) {
16
+ return token.slice(1, -1)
17
+ }
18
+ return token
19
+ })
20
+ }
21
+
22
+ const isPythonInterpreterToken = (token: string): boolean => {
23
+ const base = path.basename(token)
24
+ return /^(?:python(?:\d+(?:\.\d+)*)?|py|pypy)$/i.test(base)
25
+ }
26
+
27
+ const CONTROL_TOKENS = new Set(["&&", "||", ";", "|"])
28
+
29
+ const resolveCommandCwd = (rawCwd: unknown, fallbackDirectory: string): string => {
30
+ if (typeof rawCwd !== "string" || rawCwd.trim() === "") {
31
+ return fallbackDirectory
32
+ }
33
+ return path.isAbsolute(rawCwd) ? rawCwd : path.resolve(fallbackDirectory, rawCwd)
34
+ }
35
+
36
+ const resolveRunPyTarget = (command: string, executionCwd: string): string | null => {
37
+ const tokens = tokenizeCommand(command)
38
+ let currentCwd = executionCwd
39
+
40
+ for (let i = 0; i < tokens.length; i += 1) {
41
+ const token = tokens[i]
42
+
43
+ if (token === "cd") {
44
+ const destination = tokens[i + 1]
45
+ if (destination && !CONTROL_TOKENS.has(destination)) {
46
+ currentCwd = path.resolve(currentCwd, destination)
47
+ i += 1
48
+ }
49
+ continue
50
+ }
51
+
52
+ if (!isPythonInterpreterToken(token)) {
53
+ continue
54
+ }
55
+
56
+ for (let j = i + 1; j < tokens.length; j += 1) {
57
+ const nextToken = tokens[j]
58
+ if (CONTROL_TOKENS.has(nextToken)) {
59
+ break
60
+ }
61
+ if (nextToken === "-m" || nextToken === "-c" || nextToken === "-") {
62
+ break
63
+ }
64
+ if (nextToken.startsWith("-")) {
65
+ continue
66
+ }
67
+ if (path.basename(nextToken) === "run.py") {
68
+ return path.resolve(currentCwd, nextToken)
69
+ }
70
+ break
71
+ }
72
+ }
73
+
74
+ return null
75
+ }
76
+
77
+ const getWorkflowRunPyFilesFromDir = async (workflowsDir: string): Promise<string[]> => {
78
+ let entries: Dirent[]
79
+ try {
80
+ entries = await fs.readdir(workflowsDir, { withFileTypes: true })
81
+ } catch {
82
+ return []
83
+ }
84
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => path.join(workflowsDir, entry.name, "run.py"))
85
+ }
86
+
87
+ const getWorkflowRunPyFiles = async (roots: string[]): Promise<string[]> => {
88
+ const all = new Set<string>()
89
+ for (const root of roots) {
90
+ const candidates = [
91
+ path.join(root, "workflows"),
92
+ path.join(root, "src", "workspace_files", "workflows"),
93
+ ]
94
+ for (const workflowsDir of candidates) {
95
+ const files = await getWorkflowRunPyFilesFromDir(workflowsDir)
96
+ for (const file of files) {
97
+ all.add(file)
98
+ }
99
+ }
100
+ }
101
+ return Array.from(all)
102
+ }
103
+
104
+ const shouldBlockByContentMatch = async (targetRunPy: string, roots: string[]): Promise<boolean> => {
105
+ let targetContent: string
106
+ try {
107
+ targetContent = await fs.readFile(targetRunPy, "utf8")
108
+ } catch {
109
+ return false
110
+ }
111
+ const workflowRunPyFiles = await getWorkflowRunPyFiles(roots)
112
+
113
+ for (const workflowRunPy of workflowRunPyFiles) {
114
+ try {
115
+ const workflowContent = await fs.readFile(workflowRunPy, "utf8")
116
+ if (workflowContent === targetContent) {
117
+ return true
118
+ }
119
+ } catch {
120
+ // Ignore missing/unreadable workflow run.py files.
121
+ }
122
+ }
123
+
124
+ return false
125
+ }
126
+
127
+ export const PythonProtection: Plugin = async ({ directory, worktree }) => {
128
+ const checkCommand = async (command: string, rawCwd?: unknown): Promise<void> => {
129
+ if (!pythonInterpreterPattern.test(command)) {
130
+ return
131
+ }
132
+
133
+ const commandCwd = resolveCommandCwd(rawCwd, directory)
134
+ let targetRunPy = resolveRunPyTarget(command, commandCwd)
135
+ if (!targetRunPy && /(^|[\/\s])run\.py($|[\s;|&()])/.test(command)) {
136
+ targetRunPy = path.resolve(commandCwd, "run.py")
137
+ }
138
+ if (!targetRunPy) {
139
+ return
140
+ }
141
+
142
+ const roots = Array.from(new Set([
143
+ directory,
144
+ worktree,
145
+ path.resolve(directory, ".."),
146
+ path.resolve(directory, "../.."),
147
+ path.resolve(commandCwd, ".."),
148
+ path.resolve(commandCwd, "../.."),
149
+ ].filter(Boolean)))
150
+
151
+ if (await shouldBlockByContentMatch(targetRunPy, roots)) {
152
+ throw new Error("Direct workflow execution via Python is not allowed. Use the runWorkflow tool instead.")
153
+ }
154
+ }
155
+
156
+ return {
157
+ "command.execute.before": async (input) => {
158
+ const command = [input.command, input.arguments].filter(Boolean).join(" ").trim()
159
+ if (!command) {
160
+ return
161
+ }
162
+ await checkCommand(command, directory)
163
+ },
164
+ "tool.execute.before": async (input, output) => {
165
+ const commandCandidates: unknown[] = [
166
+ output.args?.command,
167
+ output.args?.cmd,
168
+ output.args?.arguments,
169
+ output.args?.script,
170
+ (input as { args?: { command?: string; cmd?: string; arguments?: string; script?: string } }).args?.command,
171
+ (input as { args?: { command?: string; cmd?: string; arguments?: string; script?: string } }).args?.cmd,
172
+ (input as { args?: { command?: string; cmd?: string; arguments?: string; script?: string } }).args?.arguments,
173
+ (input as { args?: { command?: string; cmd?: string; arguments?: string; script?: string } }).args?.script,
174
+ ]
175
+ const commandValue = commandCandidates.find((value) => typeof value === "string" && value.trim() !== "")
176
+ if (typeof commandValue !== "string") {
177
+ return
178
+ }
179
+
180
+ const commandCwd = output.args?.workdir ?? output.args?.cwd ?? input.args?.workdir ?? input.args?.cwd
181
+ await checkCommand(commandValue, commandCwd)
182
+ },
183
+ }
184
+ }
@@ -0,0 +1,168 @@
1
+ import { type Plugin, tool } from "@opencode-ai/plugin"
2
+
3
+ function getApiBaseUrl(): string {
4
+ const port = process.env.TEAMCOPILOT_PORT?.trim()
5
+ if (!port) {
6
+ throw new Error("TEAMCOPILOT_PORT must be set.")
7
+ }
8
+ return `http://localhost:${port}`
9
+ }
10
+
11
+
12
+ function extractMessageId(context: unknown): string | null {
13
+ if (!context || typeof context !== "object") {
14
+ return null
15
+ }
16
+
17
+ const candidate = (context as { messageID?: unknown; messageId?: unknown; message_id?: unknown })
18
+ const raw = candidate.messageID ?? candidate.messageId ?? candidate.message_id
19
+ if (typeof raw === "string" && raw.trim().length > 0) {
20
+ return raw
21
+ }
22
+ if (typeof raw === "number" && Number.isFinite(raw)) {
23
+ return String(raw)
24
+ }
25
+ return null
26
+ }
27
+
28
+ function extractCallId(context: unknown): string | null {
29
+ if (!context || typeof context !== "object") {
30
+ return null
31
+ }
32
+
33
+ const candidate = (context as { callID?: unknown; callId?: unknown; call_id?: unknown })
34
+ const raw = candidate.callID ?? candidate.callId ?? candidate.call_id
35
+ if (typeof raw === "string" && raw.trim().length > 0) {
36
+ return raw
37
+ }
38
+ return null
39
+ }
40
+
41
+ async function readErrorMessageFromResponse(
42
+ response: Response,
43
+ fallbackMessage: string
44
+ ): Promise<string> {
45
+ try {
46
+ const text = await response.text()
47
+ if (!text) return fallbackMessage
48
+ try {
49
+ const parsed: unknown = JSON.parse(text)
50
+ if (parsed && typeof parsed === "object" && "message" in parsed) {
51
+ const msg = (parsed as { message?: unknown }).message
52
+ if (typeof msg === "string" && msg.trim().length > 0) return msg
53
+ }
54
+ } catch {
55
+ // fall back to text
56
+ }
57
+ return text.trim().length > 0 ? text : fallbackMessage
58
+ } catch {
59
+ return fallbackMessage
60
+ }
61
+ }
62
+
63
+ function sleep(ms: number): Promise<void> {
64
+ return new Promise((resolve) => setTimeout(resolve, ms))
65
+ }
66
+
67
+ export const RunWorkflowPlugin: Plugin = async (_ctx) => {
68
+ return {
69
+ tool: {
70
+ runWorkflow: tool({
71
+ description:
72
+ "Execute a workflow with the provided inputs. Validates inputs against the workflow's schema defined in workflow.json, runs the workflow's venv Python with run.py and appropriate arguments, streams output in real-time, and enforces the timeout defined in workflow.json.",
73
+ args: {
74
+ slug: tool.schema
75
+ .string()
76
+ .describe(
77
+ "The workflow slug (folder name under workflows/)"
78
+ ),
79
+ inputs: tool.schema
80
+ .record(tool.schema.string(), tool.schema.unknown())
81
+ .optional()
82
+ .default({})
83
+ .describe(
84
+ "Key-value pairs matching the workflow's input schema from workflow.json"
85
+ ),
86
+ },
87
+ async execute(args, context) {
88
+ const { sessionID } = context
89
+ const { slug, inputs = {} } = args
90
+ const messageId = extractMessageId(context)
91
+ const callId = extractCallId(context)
92
+
93
+ if (!messageId) {
94
+ throw new Error("Could not determine message id from tool context.")
95
+ }
96
+
97
+ if (!callId) {
98
+ throw new Error("Could not determine call id from tool context.")
99
+ }
100
+
101
+ const startResponse = await fetch(`${getApiBaseUrl()}/api/workflows/execute`, {
102
+ method: "POST",
103
+ headers: {
104
+ "Content-Type": "application/json",
105
+ Authorization: `Bearer ${sessionID}`,
106
+ },
107
+ body: JSON.stringify({
108
+ slug,
109
+ inputs,
110
+ message_id: messageId,
111
+ call_id: callId,
112
+ }),
113
+ })
114
+
115
+ if (!startResponse.ok) {
116
+ const errorMessage = await readErrorMessageFromResponse(
117
+ startResponse,
118
+ `Failed to execute workflow (HTTP ${startResponse.status})`
119
+ )
120
+ throw new Error(errorMessage)
121
+ }
122
+
123
+ const startedPayload = (await startResponse.json()) as {
124
+ execution_id?: unknown
125
+ }
126
+ const executionId = startedPayload.execution_id
127
+ if (typeof executionId !== "string" || executionId.trim().length === 0) {
128
+ throw new Error("Workflow execute start response did not include execution_id.")
129
+ }
130
+
131
+ while (true) {
132
+ const resultResponse = await fetch(
133
+ `${getApiBaseUrl()}/api/workflows/execute/${encodeURIComponent(executionId)}`,
134
+ {
135
+ method: "GET",
136
+ headers: {
137
+ Authorization: `Bearer ${sessionID}`,
138
+ },
139
+ }
140
+ )
141
+
142
+ if (!resultResponse.ok) {
143
+ const errorMessage = await readErrorMessageFromResponse(
144
+ resultResponse,
145
+ `Failed to fetch workflow execution result (HTTP ${resultResponse.status})`
146
+ )
147
+ throw new Error(errorMessage)
148
+ }
149
+
150
+ const resultPayload = (await resultResponse.json()) as {
151
+ status?: unknown
152
+ }
153
+ if (resultPayload.status === "running") {
154
+ await sleep(500)
155
+ continue
156
+ }
157
+ if (resultPayload.status !== "success") {
158
+ throw new Error(JSON.stringify(resultPayload))
159
+ }
160
+ return JSON.stringify(resultPayload)
161
+ }
162
+ },
163
+ }),
164
+ },
165
+ }
166
+ }
167
+
168
+ export default RunWorkflowPlugin
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": false,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "declaration": true,
11
+ "outDir": "./dist",
12
+ "rootDir": "./plugins"
13
+ },
14
+ "include": ["plugins/**/*.ts"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }