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,173 @@
1
+ import { type Plugin, tool } from "@opencode-ai/plugin"
2
+ import { pipeline } from "@huggingface/transformers"
3
+
4
+ function getApiBaseUrl(): string {
5
+ const port = process.env.TEAMCOPILOT_PORT?.trim()
6
+ if (!port) {
7
+ throw new Error("TEAMCOPILOT_PORT must be set.")
8
+ }
9
+ return `http://localhost:${port}`
10
+ }
11
+
12
+
13
+
14
+ // ============================================================================
15
+ // Types
16
+ // ============================================================================
17
+
18
+ interface WorkflowMatch {
19
+ path: string
20
+ similarity: number
21
+ summary: string
22
+ }
23
+
24
+ interface WorkflowSummary {
25
+ slug: string
26
+ intent_summary: string
27
+ }
28
+
29
+ async function readErrorMessageFromResponse(
30
+ response: Response,
31
+ fallbackMessage: string
32
+ ): Promise<string> {
33
+ try {
34
+ const text = await response.text()
35
+ if (!text) return fallbackMessage
36
+ try {
37
+ const parsed: unknown = JSON.parse(text)
38
+ if (parsed && typeof parsed === "object" && "message" in parsed) {
39
+ const msg = (parsed as { message?: unknown }).message
40
+ if (typeof msg === "string" && msg.trim().length > 0) return msg
41
+ }
42
+ } catch {
43
+ // fall back to plain text
44
+ }
45
+ return text.trim().length > 0 ? text : fallbackMessage
46
+ } catch {
47
+ return fallbackMessage
48
+ }
49
+ }
50
+
51
+ // ============================================================================
52
+ // Embedding Functions
53
+ // ============================================================================
54
+
55
+ // Cache the extractor pipeline
56
+ let extractor: Awaited<ReturnType<typeof pipeline>> | null = null
57
+
58
+ /**
59
+ * Get embedding vector for text using sentence-transformers/all-MiniLM-L6-v2
60
+ */
61
+ async function getEmbedding(text: string): Promise<number[]> {
62
+ if (extractor === null) {
63
+ extractor = await pipeline(
64
+ "feature-extraction",
65
+ "sentence-transformers/all-MiniLM-L6-v2",
66
+ { dtype: "fp32" }
67
+ )
68
+ }
69
+
70
+ const output = await extractor(text, { pooling: "mean", normalize: true })
71
+ return Array.from(output.data as Float32Array)
72
+ }
73
+
74
+ /**
75
+ * Calculate cosine similarity between two vectors
76
+ */
77
+ function cosineSimilarity(vecA: number[], vecB: number[]): number {
78
+ const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0)
79
+ const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0))
80
+ const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0))
81
+ return dotProduct / (magnitudeA * magnitudeB)
82
+ }
83
+
84
+ // ============================================================================
85
+ // Plugin
86
+ // ============================================================================
87
+
88
+ export const FindSimilarWorkflowPlugin: Plugin = async (_ctx) => {
89
+ return {
90
+ tool: {
91
+ findSimilarWorkflow: tool({
92
+ description:
93
+ "Query for existing workflows before creating new ones or for searching for a workflow to run. Returns up to N candidate workflows with paths and summaries based on semantic similarity to the provided description. Use this to avoid duplicate work and find workflows that can be reused or adapted.",
94
+ args: {
95
+ description: tool.schema
96
+ .string()
97
+ .describe(
98
+ "Natural language description of what you're looking for"
99
+ ),
100
+ limit: tool.schema
101
+ .number()
102
+ .optional()
103
+ .default(5)
104
+ .describe("Maximum number of results to return (default: 5)"),
105
+ },
106
+ async execute(args, context) {
107
+ const { sessionID } = context
108
+ const { description, limit = 5 } = args
109
+
110
+ const workflowsResponse = await fetch(`${getApiBaseUrl()}/api/workflows`, {
111
+ headers: {
112
+ Authorization: `Bearer ${sessionID}`,
113
+ },
114
+ })
115
+
116
+ if (!workflowsResponse.ok) {
117
+ const errorMessage = await readErrorMessageFromResponse(
118
+ workflowsResponse,
119
+ `Failed to list workflows (HTTP ${workflowsResponse.status})`
120
+ )
121
+ throw new Error(errorMessage)
122
+ }
123
+
124
+ const workflowsPayload = (await workflowsResponse.json()) as {
125
+ workflows?: WorkflowSummary[]
126
+ }
127
+ const candidateWorkflows = workflowsPayload.workflows ?? []
128
+
129
+ if (candidateWorkflows.length === 0) {
130
+ return JSON.stringify(
131
+ {
132
+ matches: [],
133
+ message:
134
+ "No workflows available for this user with access permissions.",
135
+ },
136
+ null,
137
+ 2
138
+ )
139
+ }
140
+
141
+ // Get embedding for the query description
142
+ const queryEmbedding = await getEmbedding(description)
143
+
144
+ const matches: WorkflowMatch[] = []
145
+
146
+ for (const workflow of candidateWorkflows) {
147
+ const summary = workflow.intent_summary
148
+
149
+ // Get embedding for the workflow's intent_summary
150
+ const workflowEmbedding = await getEmbedding(summary)
151
+
152
+ // Calculate cosine similarity
153
+ const similarity = cosineSimilarity(queryEmbedding, workflowEmbedding)
154
+
155
+ matches.push({
156
+ path: `workflows/${workflow.slug}`,
157
+ similarity: Math.round(similarity * 100) / 100,
158
+ summary,
159
+ })
160
+ }
161
+
162
+ // Sort by similarity descending and take top N
163
+ matches.sort((a, b) => b.similarity - a.similarity)
164
+ const topMatches = matches.slice(0, limit)
165
+
166
+ return JSON.stringify({ matches: topMatches }, null, 2)
167
+ },
168
+ }),
169
+ },
170
+ }
171
+ }
172
+
173
+ export default FindSimilarWorkflowPlugin
@@ -0,0 +1,211 @@
1
+ import { type Plugin, tool } from "@opencode-ai/plugin"
2
+ import { pipeline } from "@huggingface/transformers"
3
+
4
+ function getApiBaseUrl(): string {
5
+ const port = process.env.TEAMCOPILOT_PORT?.trim()
6
+ if (!port) {
7
+ throw new Error("TEAMCOPILOT_PORT must be set.")
8
+ }
9
+ return `http://localhost:${port}`
10
+ }
11
+
12
+
13
+
14
+ interface SkillSummary {
15
+ slug: string
16
+ name: string
17
+ description: string
18
+ is_approved: boolean
19
+ can_edit: boolean
20
+ }
21
+
22
+ interface SkillFileContentResponse {
23
+ kind: "text" | "binary"
24
+ content?: string
25
+ }
26
+
27
+ interface SkillMatch {
28
+ slug: string
29
+ name: string
30
+ description: string
31
+ similarity: number
32
+ }
33
+
34
+ async function readErrorMessageFromResponse(
35
+ response: Response,
36
+ fallbackMessage: string
37
+ ): Promise<string> {
38
+ try {
39
+ const text = await response.text()
40
+ if (!text) return fallbackMessage
41
+ try {
42
+ const parsed: unknown = JSON.parse(text)
43
+ if (parsed && typeof parsed === "object" && "message" in parsed) {
44
+ const msg = (parsed as { message?: unknown }).message
45
+ if (typeof msg === "string" && msg.trim().length > 0) return msg
46
+ }
47
+ } catch {
48
+ // fall back to plain text
49
+ }
50
+ return text.trim().length > 0 ? text : fallbackMessage
51
+ } catch {
52
+ return fallbackMessage
53
+ }
54
+ }
55
+
56
+ function stripLeadingFrontmatter(markdown: string): string {
57
+ const trimmedStart = markdown.trimStart()
58
+ if (!trimmedStart.startsWith("---\n")) {
59
+ return markdown.trim()
60
+ }
61
+
62
+ const frontmatterEnd = trimmedStart.indexOf("\n---\n", 4)
63
+ if (frontmatterEnd < 0) {
64
+ return markdown.trim()
65
+ }
66
+
67
+ return trimmedStart.slice(frontmatterEnd + "\n---\n".length).trim()
68
+ }
69
+
70
+ let extractor: Awaited<ReturnType<typeof pipeline>> | null = null
71
+
72
+ async function getEmbedding(text: string): Promise<number[]> {
73
+ if (extractor === null) {
74
+ extractor = await pipeline(
75
+ "feature-extraction",
76
+ "sentence-transformers/all-MiniLM-L6-v2",
77
+ { dtype: "fp32" }
78
+ )
79
+ }
80
+
81
+ const output = await extractor(text, { pooling: "mean", normalize: true })
82
+ return Array.from(output.data as Float32Array)
83
+ }
84
+
85
+ function cosineSimilarity(vecA: number[], vecB: number[]): number {
86
+ const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0)
87
+ const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0))
88
+ const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0))
89
+ return dotProduct / (magnitudeA * magnitudeB)
90
+ }
91
+
92
+ async function readSkillMarkdown(
93
+ sessionID: string,
94
+ slug: string
95
+ ): Promise<string> {
96
+ const response = await fetch(
97
+ `${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}/files/content?path=${encodeURIComponent("SKILL.md")}`,
98
+ {
99
+ headers: {
100
+ Authorization: `Bearer ${sessionID}`,
101
+ },
102
+ }
103
+ )
104
+
105
+ if (!response.ok) {
106
+ const errorMessage = await readErrorMessageFromResponse(
107
+ response,
108
+ `Failed to read SKILL.md for ${slug} (HTTP ${response.status})`
109
+ )
110
+ throw new Error(errorMessage)
111
+ }
112
+
113
+ const payload = (await response.json()) as SkillFileContentResponse
114
+ if (payload.kind !== "text") {
115
+ throw new Error(`SKILL.md for ${slug} is not a text file.`)
116
+ }
117
+
118
+ return payload.content ?? ""
119
+ }
120
+
121
+ export const FindSkillPlugin: Plugin = async (_ctx) => {
122
+ return {
123
+ tool: {
124
+ findSkill: tool({
125
+ description:
126
+ "Find similar custom skills. Searches only approved skills that the current user can edit, using both skill descriptions and SKILL.md body content.",
127
+ args: {
128
+ description: tool.schema
129
+ .string()
130
+ .describe("Natural language description of the skill you are looking for"),
131
+ limit: tool.schema
132
+ .number()
133
+ .optional()
134
+ .default(5)
135
+ .describe("Maximum number of results to return (default: 5)"),
136
+ },
137
+ async execute(args, context) {
138
+ const { sessionID } = context
139
+ const description = args.description.trim()
140
+ const limit = args.limit
141
+
142
+ if (!description) {
143
+ throw new Error("description is required")
144
+ }
145
+
146
+ const skillsResponse = await fetch(`${getApiBaseUrl()}/api/skills`, {
147
+ headers: {
148
+ Authorization: `Bearer ${sessionID}`,
149
+ },
150
+ })
151
+
152
+ if (!skillsResponse.ok) {
153
+ const errorMessage = await readErrorMessageFromResponse(
154
+ skillsResponse,
155
+ `Failed to list skills (HTTP ${skillsResponse.status})`
156
+ )
157
+ throw new Error(errorMessage)
158
+ }
159
+
160
+ const skillsPayload = (await skillsResponse.json()) as {
161
+ skills?: SkillSummary[]
162
+ }
163
+ const candidateSkills = (skillsPayload.skills ?? []).filter(
164
+ (skill) => skill.is_approved
165
+ )
166
+
167
+ if (candidateSkills.length === 0) {
168
+ return JSON.stringify(
169
+ {
170
+ matches: [],
171
+ message:
172
+ "No approved skills available for this user with edit access.",
173
+ },
174
+ null,
175
+ 2
176
+ )
177
+ }
178
+
179
+ const queryEmbedding = await getEmbedding(description)
180
+ const matches: SkillMatch[] = []
181
+
182
+ for (const skill of candidateSkills) {
183
+ const markdown = await readSkillMarkdown(sessionID, skill.slug)
184
+ const searchableText = `${skill.description}\n\n${stripLeadingFrontmatter(markdown)}`
185
+ const skillEmbedding = await getEmbedding(searchableText)
186
+ const similarity = cosineSimilarity(queryEmbedding, skillEmbedding)
187
+
188
+ matches.push({
189
+ slug: skill.slug,
190
+ name: skill.name,
191
+ description: skill.description,
192
+ similarity: Math.round(similarity * 100) / 100,
193
+ })
194
+ }
195
+
196
+ matches.sort((a, b) => b.similarity - a.similarity)
197
+
198
+ return JSON.stringify(
199
+ {
200
+ matches: matches.slice(0, limit),
201
+ },
202
+ null,
203
+ 2
204
+ )
205
+ },
206
+ }),
207
+ },
208
+ }
209
+ }
210
+
211
+ export default FindSkillPlugin
@@ -0,0 +1,135 @@
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
+ const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
13
+
14
+ interface SkillFileContentResponse {
15
+ path: string
16
+ kind: "text" | "binary"
17
+ content?: string
18
+ }
19
+
20
+ interface SkillDetailsResponse {
21
+ skill?: {
22
+ is_approved?: boolean
23
+ }
24
+ }
25
+
26
+ async function readErrorMessageFromResponse(
27
+ response: Response,
28
+ fallbackMessage: string
29
+ ): Promise<string> {
30
+ try {
31
+ const text = await response.text()
32
+ if (!text) return fallbackMessage
33
+ try {
34
+ const parsed: unknown = JSON.parse(text)
35
+ if (parsed && typeof parsed === "object" && "message" in parsed) {
36
+ const msg = (parsed as { message?: unknown }).message
37
+ if (typeof msg === "string" && msg.trim().length > 0) return msg
38
+ }
39
+ } catch {
40
+ // fall back to plain text
41
+ }
42
+ return text.trim().length > 0 ? text : fallbackMessage
43
+ } catch {
44
+ return fallbackMessage
45
+ }
46
+ }
47
+
48
+ export const GetSkillContentPlugin: Plugin = async (_ctx) => {
49
+ return {
50
+ tool: {
51
+ getSkillContent: tool({
52
+ description:
53
+ "Get the contents of a skill by slug. Uses backend auth and permission checks; returns SKILL.md content only if the current user has access.",
54
+ args: {
55
+ slug: tool.schema
56
+ .string()
57
+ .describe("Skill slug (lowercase letters/numbers with hyphens)"),
58
+ },
59
+ async execute(args, context) {
60
+ const { sessionID } = context
61
+ const slug = args.slug.trim()
62
+
63
+ if (!SLUG_REGEX.test(slug)) {
64
+ throw new Error(
65
+ `Invalid slug format: "${slug}". Slug must be lowercase alphanumeric with hyphens (e.g., "my-skill-name").`
66
+ )
67
+ }
68
+
69
+ const skillDetailsResponse = await fetch(
70
+ `${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}`,
71
+ {
72
+ headers: {
73
+ Authorization: `Bearer ${sessionID}`,
74
+ },
75
+ }
76
+ )
77
+
78
+ if (!skillDetailsResponse.ok) {
79
+ const errorMessage = await readErrorMessageFromResponse(
80
+ skillDetailsResponse,
81
+ `Failed to fetch skill metadata for ${slug} (HTTP ${skillDetailsResponse.status})`
82
+ )
83
+ throw new Error(errorMessage)
84
+ }
85
+
86
+ const skillDetailsPayload =
87
+ (await skillDetailsResponse.json()) as SkillDetailsResponse
88
+ const isApproved = skillDetailsPayload.skill?.is_approved === true
89
+
90
+ if (!isApproved) {
91
+ throw new Error(
92
+ `Skill \"${slug}\" is not approved yet. Only approved skills can be read through getSkillContent.`
93
+ )
94
+ }
95
+
96
+ const response = await fetch(
97
+ `${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}/files/content?path=${encodeURIComponent("SKILL.md")}`,
98
+ {
99
+ headers: {
100
+ Authorization: `Bearer ${sessionID}`,
101
+ },
102
+ }
103
+ )
104
+
105
+ if (!response.ok) {
106
+ const errorMessage = await readErrorMessageFromResponse(
107
+ response,
108
+ `Failed to get skill content for ${slug} (HTTP ${response.status})`
109
+ )
110
+ throw new Error(errorMessage)
111
+ }
112
+
113
+ const payload = (await response.json()) as SkillFileContentResponse
114
+ if (payload.kind !== "text") {
115
+ throw new Error(`SKILL.md for ${slug} is not a text file.`)
116
+ }
117
+
118
+ return JSON.stringify(
119
+ {
120
+ skill: {
121
+ slug,
122
+ path: payload.path,
123
+ content: payload.content ?? "",
124
+ },
125
+ },
126
+ null,
127
+ 2
128
+ )
129
+ },
130
+ }),
131
+ },
132
+ }
133
+ }
134
+
135
+ export default GetSkillContentPlugin
@@ -0,0 +1,64 @@
1
+ import type { Plugin } from "@opencode-ai/plugin"
2
+ import * as fs from "fs/promises"
3
+ import * as path from "path"
4
+
5
+ const HONEYTOKEN_UUID = "1f9f0b72-5f9f-4c9b-aef1-2fb2e0f6d8c4"
6
+ const HONEYTOKEN_FILE_NAME = `honeytoken-${HONEYTOKEN_UUID}.txt`
7
+ const HONEYTOKEN_MARKER = `DO_NOT_EXPOSE:${HONEYTOKEN_UUID}`
8
+ let isHoneytokenReady = false
9
+
10
+ async function ensureHoneytokenFile(workspaceDirectory: string): Promise<void> {
11
+ if (isHoneytokenReady) {
12
+ return
13
+ }
14
+
15
+ const honeytokenValue = `DO_NOT_EXPOSE:${HONEYTOKEN_UUID}\n`
16
+ const workflowsDirectory = path.join(workspaceDirectory, "workflows")
17
+ await fs.mkdir(workflowsDirectory, { recursive: true })
18
+ const workflowsHoneytokenPath = path.join(workflowsDirectory, HONEYTOKEN_FILE_NAME)
19
+ await fs.writeFile(workflowsHoneytokenPath, honeytokenValue, "utf-8")
20
+
21
+ const skillsDirectory = path.join(workspaceDirectory, "custom-skills")
22
+ await fs.mkdir(skillsDirectory, { recursive: true })
23
+ const honeytokenPath = path.join(skillsDirectory, HONEYTOKEN_FILE_NAME)
24
+ await fs.writeFile(honeytokenPath, honeytokenValue, "utf-8")
25
+ isHoneytokenReady = true
26
+ }
27
+
28
+ function includesHoneytoken(value: unknown): boolean {
29
+ if (value === undefined) {
30
+ return false
31
+ }
32
+
33
+ const text =
34
+ typeof value === "string"
35
+ ? value
36
+ : JSON.stringify(value)
37
+
38
+ if (!text) {
39
+ return false
40
+ }
41
+
42
+ return text.includes(HONEYTOKEN_UUID) || text.includes(HONEYTOKEN_FILE_NAME) || text.includes(HONEYTOKEN_MARKER)
43
+ }
44
+
45
+ export const HoneytokenProtection: Plugin = async ({ directory }) => {
46
+ return {
47
+ "tool.execute.before": async () => {
48
+ await ensureHoneytokenFile(directory)
49
+ },
50
+ "tool.execute.after": async (input, output) => {
51
+ if (
52
+ includesHoneytoken(output.output) ||
53
+ includesHoneytoken(output.metadata) ||
54
+ includesHoneytoken(output.title) ||
55
+ includesHoneytoken(output) ||
56
+ includesHoneytoken(input.args)
57
+ ) {
58
+ throw new Error(`Tool output matched a protected workspace honeytoken and was blocked. To list all skills or workflows, use the listAvailableSkills or listAvailableWorkflows tools.`)
59
+ }
60
+ },
61
+ }
62
+ }
63
+
64
+ export default HoneytokenProtection
@@ -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 SkillSummary {
14
+ slug: string
15
+ name: string
16
+ description: 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 ListAvailableSkillsPlugin: Plugin = async (_ctx) => {
44
+ return {
45
+ tool: {
46
+ listAvailableSkills: tool({
47
+ description:
48
+ "List custom skills that are available for this user to use. Returns only approved skills that the current user has permission to run.",
49
+ args: {},
50
+ async execute(_args, context) {
51
+ const { sessionID } = context
52
+
53
+ const response = await fetch(`${getApiBaseUrl()}/api/skills`, {
54
+ headers: {
55
+ Authorization: `Bearer ${sessionID}`,
56
+ },
57
+ })
58
+
59
+ if (!response.ok) {
60
+ const errorMessage = await readErrorMessageFromResponse(
61
+ response,
62
+ `Failed to list skills (HTTP ${response.status})`
63
+ )
64
+ throw new Error(errorMessage)
65
+ }
66
+
67
+ const payload = (await response.json()) as {
68
+ skills?: SkillSummary[]
69
+ }
70
+
71
+ const availableSkills = (payload.skills ?? [])
72
+ .filter((skill) => skill.is_approved)
73
+ .map((skill) => ({
74
+ slug: skill.slug,
75
+ name: skill.name,
76
+ description: skill.description,
77
+ }))
78
+
79
+ return JSON.stringify(
80
+ {
81
+ skills: availableSkills,
82
+ total: availableSkills.length,
83
+ },
84
+ null,
85
+ 2
86
+ )
87
+ },
88
+ }),
89
+ },
90
+ }
91
+ }
92
+
93
+ export default ListAvailableSkillsPlugin