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,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
|