teamcopilot 0.1.16 → 0.2.0

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 (54) hide show
  1. package/README.md +88 -9
  2. package/dist/chat/index.js +23 -1
  3. package/dist/frontend/assets/{cssMode-CH26ItO2.js → cssMode-CM1GmZ3H.js} +1 -1
  4. package/dist/frontend/assets/{freemarker2-CiRHXG8W.js → freemarker2-C8TeljYR.js} +1 -1
  5. package/dist/frontend/assets/{handlebars-DXV-JQiR.js → handlebars-B2e-Wzyt.js} +1 -1
  6. package/dist/frontend/assets/{html-DKdYDRJv.js → html-DtBAvTj2.js} +1 -1
  7. package/dist/frontend/assets/{htmlMode-D466XPJJ.js → htmlMode-Dta08RE6.js} +1 -1
  8. package/dist/frontend/assets/index-BirlyHV4.css +1 -0
  9. package/dist/frontend/assets/{index-CvsPLefz.js → index-Dp0jlIX9.js} +201 -201
  10. package/dist/frontend/assets/{javascript-D5lHN8tF.js → javascript-BYeHq-2v.js} +1 -1
  11. package/dist/frontend/assets/{jsonMode-C9Wdxaho.js → jsonMode-DkJo6l8K.js} +1 -1
  12. package/dist/frontend/assets/{liquid-NIH--tpJ.js → liquid-nmEuajdb.js} +1 -1
  13. package/dist/frontend/assets/{mdx-xwEbqXME.js → mdx-BJybRyf3.js} +1 -1
  14. package/dist/frontend/assets/{python-BzErW_b3.js → python-DRAABm9s.js} +1 -1
  15. package/dist/frontend/assets/{razor-B0v-Bw5B.js → razor-7lH4jzk8.js} +1 -1
  16. package/dist/frontend/assets/{tsMode-B9YN5EEb.js → tsMode-ClcmdG3S.js} +1 -1
  17. package/dist/frontend/assets/{typescript-DIMXtHre.js → typescript-D9oav8M6.js} +1 -1
  18. package/dist/frontend/assets/{xml-DQ5HnppJ.js → xml-B0ks0e6Y.js} +1 -1
  19. package/dist/frontend/assets/{yaml-BQCOKj13.js → yaml-CCDt1oK4.js} +1 -1
  20. package/dist/frontend/index.html +2 -2
  21. package/dist/index.js +99 -90
  22. package/dist/secrets/index.js +74 -0
  23. package/dist/skills/index.js +43 -1
  24. package/dist/users/index.js +98 -0
  25. package/dist/utils/redact.js +52 -5
  26. package/dist/utils/resource-file-routes.js +2 -4
  27. package/dist/utils/resource-files.js +10 -2
  28. package/dist/utils/secret-contract-validation.js +184 -0
  29. package/dist/utils/secrets.js +127 -0
  30. package/dist/utils/skill-files.js +7 -0
  31. package/dist/utils/skill.js +50 -1
  32. package/dist/utils/workflow-runner.js +19 -4
  33. package/dist/utils/workflow.js +13 -1
  34. package/dist/workflows/index.js +10 -1
  35. package/dist/workspace_files/.opencode/plugins/createSkill.ts +1 -26
  36. package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +3 -3
  37. package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +93 -5
  38. package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +31 -49
  39. package/dist/workspace_files/.opencode/plugins/listAvailableSecretKeys.ts +107 -0
  40. package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +2 -2
  41. package/dist/workspace_files/.opencode/plugins/secret-proxy.ts +818 -0
  42. package/dist/workspace_files/AGENTS.md +91 -21
  43. package/package.json +5 -3
  44. package/prisma/generated/client/edge.js +24 -3
  45. package/prisma/generated/client/index-browser.js +21 -0
  46. package/prisma/generated/client/index.d.ts +3139 -128
  47. package/prisma/generated/client/index.js +24 -3
  48. package/prisma/generated/client/package.json +1 -1
  49. package/prisma/generated/client/schema.prisma +27 -0
  50. package/prisma/generated/client/wasm.js +24 -3
  51. package/prisma/migrations/20260402060129_add_secret_management/migration.sql +38 -0
  52. package/prisma/migrations/20260404052800_remove_global_secret_user_fkeys/migration.sql +20 -0
  53. package/prisma/schema.prisma +27 -0
  54. package/dist/frontend/assets/index-B8Ip8I8F.css +0 -1
@@ -20,6 +20,7 @@ interface WorkflowMatch {
20
20
  slug: string
21
21
  similarity: number
22
22
  summary: string
23
+ arguments: WorkflowArgumentGroups
23
24
  }
24
25
 
25
26
  interface WorkflowSummary {
@@ -27,6 +28,35 @@ interface WorkflowSummary {
27
28
  intent_summary: string
28
29
  }
29
30
 
31
+ interface WorkflowInput {
32
+ type: "string" | "number" | "boolean"
33
+ required?: boolean
34
+ default?: string | number | boolean
35
+ description?: string
36
+ }
37
+
38
+ interface WorkflowManifest {
39
+ inputs?: Record<string, WorkflowInput>
40
+ }
41
+
42
+ interface WorkflowDetailsResponse {
43
+ workflow?: {
44
+ manifest?: WorkflowManifest
45
+ }
46
+ }
47
+
48
+ interface WorkflowArgument {
49
+ name: string
50
+ type: "string" | "number" | "boolean"
51
+ description: string
52
+ default?: string | number | boolean
53
+ }
54
+
55
+ interface WorkflowArgumentGroups {
56
+ required: WorkflowArgument[]
57
+ optional: WorkflowArgument[]
58
+ }
59
+
30
60
  interface SessionLookupResponse {
31
61
  error?: unknown
32
62
  data?: {
@@ -57,6 +87,31 @@ async function readErrorMessageFromResponse(
57
87
  }
58
88
  }
59
89
 
90
+ function formatWorkflowArguments(inputs: Record<string, WorkflowInput>): WorkflowArgumentGroups {
91
+ const required: WorkflowArgument[] = []
92
+ const optional: WorkflowArgument[] = []
93
+
94
+ for (const [name, input] of Object.entries(inputs)) {
95
+ const baseArgument: WorkflowArgument = {
96
+ name,
97
+ type: input.type,
98
+ description: input.description ?? "",
99
+ }
100
+
101
+ if (input.required) {
102
+ required.push(baseArgument)
103
+ continue
104
+ }
105
+
106
+ optional.push({
107
+ ...baseArgument,
108
+ ...(input.default !== undefined ? { default: input.default } : {}),
109
+ })
110
+ }
111
+
112
+ return { required, optional }
113
+ }
114
+
60
115
  // ============================================================================
61
116
  // Embedding Functions
62
117
  // ============================================================================
@@ -121,7 +176,7 @@ export const FindSimilarWorkflowPlugin: Plugin = async ({ client }) => {
121
176
  tool: {
122
177
  findSimilarWorkflow: tool({
123
178
  description:
124
- "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.",
179
+ "Query for existing workflows before creating new ones or for searching for a workflow to run. Returns up to N candidate workflows with paths, summaries, and argument information based on semantic similarity to the provided description. Use this to avoid duplicate work and find workflows that can be reused or adapted.",
125
180
  args: {
126
181
  description: tool.schema
127
182
  .string()
@@ -173,7 +228,12 @@ export const FindSimilarWorkflowPlugin: Plugin = async ({ client }) => {
173
228
  // Get embedding for the query description
174
229
  const queryEmbedding = await getEmbedding(description)
175
230
 
176
- const matches: WorkflowMatch[] = []
231
+ const rankedMatches: Array<{
232
+ path: string
233
+ slug: string
234
+ similarity: number
235
+ summary: string
236
+ }> = []
177
237
 
178
238
  for (const workflow of candidateWorkflows) {
179
239
  const summary = workflow.intent_summary
@@ -184,7 +244,7 @@ export const FindSimilarWorkflowPlugin: Plugin = async ({ client }) => {
184
244
  // Calculate cosine similarity
185
245
  const similarity = cosineSimilarity(queryEmbedding, workflowEmbedding)
186
246
 
187
- matches.push({
247
+ rankedMatches.push({
188
248
  slug: workflow.slug,
189
249
  path: `workflows/${workflow.slug}`,
190
250
  similarity: Math.round(similarity * 100) / 100,
@@ -193,8 +253,36 @@ export const FindSimilarWorkflowPlugin: Plugin = async ({ client }) => {
193
253
  }
194
254
 
195
255
  // Sort by similarity descending and take top N
196
- matches.sort((a, b) => b.similarity - a.similarity)
197
- const topMatches = matches.slice(0, limit)
256
+ rankedMatches.sort((a, b) => b.similarity - a.similarity)
257
+ const topRankedMatches = rankedMatches.slice(0, limit)
258
+ const topMatches: WorkflowMatch[] = []
259
+
260
+ for (const match of topRankedMatches) {
261
+ const detailsResponse = await fetch(
262
+ `${getApiBaseUrl()}/api/workflows/${encodeURIComponent(match.slug)}`,
263
+ {
264
+ headers: {
265
+ Authorization: `Bearer ${authSessionID}`,
266
+ },
267
+ }
268
+ )
269
+
270
+ if (!detailsResponse.ok) {
271
+ const errorMessage = await readErrorMessageFromResponse(
272
+ detailsResponse,
273
+ `Failed to fetch workflow details for '${match.slug}' (HTTP ${detailsResponse.status})`
274
+ )
275
+ throw new Error(errorMessage)
276
+ }
277
+
278
+ const detailsPayload = (await detailsResponse.json()) as WorkflowDetailsResponse
279
+ const inputs = detailsPayload.workflow?.manifest?.inputs ?? {}
280
+
281
+ topMatches.push({
282
+ ...match,
283
+ arguments: formatWorkflowArguments(inputs),
284
+ })
285
+ }
198
286
 
199
287
  return JSON.stringify({ matches: topMatches }, null, 2)
200
288
  },
@@ -11,18 +11,6 @@ function getApiBaseUrl(): string {
11
11
 
12
12
  const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
13
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
14
  interface SessionLookupResponse {
27
15
  error?: unknown
28
16
  data?: {
@@ -31,6 +19,19 @@ interface SessionLookupResponse {
31
19
  }
32
20
  }
33
21
 
22
+ function readSessionLookupErrorMessage(error: unknown, fallbackMessage: string): string {
23
+ if (typeof error === "string" && error.trim().length > 0) {
24
+ return error
25
+ }
26
+ if (error && typeof error === "object" && "message" in error) {
27
+ const message = (error as { message?: unknown }).message
28
+ if (typeof message === "string" && message.trim().length > 0) {
29
+ return message
30
+ }
31
+ }
32
+ return fallbackMessage
33
+ }
34
+
34
35
  async function readErrorMessageFromResponse(
35
36
  response: Response,
36
37
  fallbackMessage: string
@@ -64,7 +65,12 @@ export const GetSkillContentPlugin: Plugin = async ({ client }) => {
64
65
  },
65
66
  })) as SessionLookupResponse
66
67
  if (response.error) {
67
- throw new Error(`Failed to resolve root session for ${currentSessionID}`)
68
+ throw new Error(
69
+ readSessionLookupErrorMessage(
70
+ response.error,
71
+ `Failed to resolve root session for ${currentSessionID}`
72
+ )
73
+ )
68
74
  }
69
75
 
70
76
  const parentID = response.data?.parentID
@@ -80,7 +86,7 @@ export const GetSkillContentPlugin: Plugin = async ({ client }) => {
80
86
  tool: {
81
87
  getSkillContent: tool({
82
88
  description:
83
- "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.",
89
+ "Get the contents of a skill by slug. Uses backend auth and permission checks; returns the original unresolved SKILL.md content from disk when the current user has access and all required secrets are configured.",
84
90
  args: {
85
91
  slug: tool.schema
86
92
  .string()
@@ -97,35 +103,8 @@ export const GetSkillContentPlugin: Plugin = async ({ client }) => {
97
103
  )
98
104
  }
99
105
 
100
- const skillDetailsResponse = await fetch(
101
- `${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}`,
102
- {
103
- headers: {
104
- Authorization: `Bearer ${authSessionID}`,
105
- },
106
- }
107
- )
108
-
109
- if (!skillDetailsResponse.ok) {
110
- const errorMessage = await readErrorMessageFromResponse(
111
- skillDetailsResponse,
112
- `Failed to fetch skill metadata for ${slug} (HTTP ${skillDetailsResponse.status})`
113
- )
114
- throw new Error(errorMessage)
115
- }
116
-
117
- const skillDetailsPayload =
118
- (await skillDetailsResponse.json()) as SkillDetailsResponse
119
- const isApproved = skillDetailsPayload.skill?.is_approved === true
120
-
121
- if (!isApproved) {
122
- throw new Error(
123
- `Skill \"${slug}\" is not approved yet. Only approved skills can be read through getSkillContent.`
124
- )
125
- }
126
-
127
106
  const response = await fetch(
128
- `${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}/files/content?path=${encodeURIComponent("SKILL.md")}`,
107
+ `${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}/runtime-content`,
129
108
  {
130
109
  headers: {
131
110
  Authorization: `Bearer ${authSessionID}`,
@@ -136,22 +115,25 @@ export const GetSkillContentPlugin: Plugin = async ({ client }) => {
136
115
  if (!response.ok) {
137
116
  const errorMessage = await readErrorMessageFromResponse(
138
117
  response,
139
- `Failed to get skill content for ${slug} (HTTP ${response.status})`
118
+ `Failed to get runtime skill content for ${slug} (HTTP ${response.status})`
140
119
  )
141
120
  throw new Error(errorMessage)
142
121
  }
143
122
 
144
- const payload = (await response.json()) as SkillFileContentResponse
145
- if (payload.kind !== "text") {
146
- throw new Error(`SKILL.md for ${slug} is not a text file.`)
123
+ const payload = (await response.json()) as {
124
+ skill?: {
125
+ slug?: string
126
+ path?: string
127
+ content?: string
128
+ }
147
129
  }
148
130
 
149
131
  return JSON.stringify(
150
132
  {
151
133
  skill: {
152
- slug,
153
- path: payload.path,
154
- content: payload.content ?? "",
134
+ slug: payload.skill?.slug ?? slug,
135
+ path: payload.skill?.path ?? "SKILL.md",
136
+ content: payload.skill?.content ?? "",
155
137
  },
156
138
  },
157
139
  null,
@@ -0,0 +1,107 @@
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
+ interface SessionLookupResponse {
12
+ error?: unknown
13
+ data?: {
14
+ id?: string
15
+ parentID?: string
16
+ }
17
+ }
18
+
19
+ async function readErrorMessageFromResponse(
20
+ response: Response,
21
+ fallbackMessage: string
22
+ ): Promise<string> {
23
+ try {
24
+ const text = await response.text()
25
+ if (!text) return fallbackMessage
26
+ try {
27
+ const parsed: unknown = JSON.parse(text)
28
+ if (parsed && typeof parsed === "object" && "message" in parsed) {
29
+ const msg = (parsed as { message?: unknown }).message
30
+ if (typeof msg === "string" && msg.trim().length > 0) return msg
31
+ }
32
+ } catch {
33
+ // fall back to plain text
34
+ }
35
+ return text.trim().length > 0 ? text : fallbackMessage
36
+ } catch {
37
+ return fallbackMessage
38
+ }
39
+ }
40
+
41
+ export const ListAvailableSecretKeysPlugin: Plugin = async ({ client }) => {
42
+ async function resolveRootSessionID(sessionID: string): Promise<string> {
43
+ let currentSessionID = sessionID
44
+
45
+ while (true) {
46
+ const response = (await client.session.get({
47
+ path: {
48
+ id: currentSessionID,
49
+ },
50
+ })) as SessionLookupResponse
51
+ if (response.error) {
52
+ throw new Error(`Failed to resolve root session for ${currentSessionID}`)
53
+ }
54
+
55
+ const parentID = response.data?.parentID
56
+ if (!parentID) {
57
+ return currentSessionID
58
+ }
59
+
60
+ currentSessionID = parentID
61
+ }
62
+ }
63
+
64
+ return {
65
+ tool: {
66
+ listAvailableSecretKeys: tool({
67
+ description:
68
+ "Get all secret keys available to the current user. Returns the merged key inventory after applying TeamCopilot's precedence rules: user secret first, then global secret. This tool does not return plaintext secret values.",
69
+ args: {},
70
+ async execute(_args, context) {
71
+ const { sessionID } = context
72
+ const authSessionID = await resolveRootSessionID(sessionID)
73
+
74
+ const response = await fetch(`${getApiBaseUrl()}/api/users/me/resolved-secrets`, {
75
+ headers: {
76
+ Authorization: `Bearer ${authSessionID}`,
77
+ },
78
+ })
79
+
80
+ if (!response.ok) {
81
+ const errorMessage = await readErrorMessageFromResponse(
82
+ response,
83
+ `Failed to get resolved secrets (HTTP ${response.status})`
84
+ )
85
+ throw new Error(errorMessage)
86
+ }
87
+
88
+ const payload = (await response.json()) as {
89
+ secret_keys?: string[]
90
+ total?: number
91
+ }
92
+
93
+ return JSON.stringify(
94
+ {
95
+ secret_keys: Array.isArray(payload.secret_keys) ? payload.secret_keys : [],
96
+ total: typeof payload.total === "number" ? payload.total : 0,
97
+ },
98
+ null,
99
+ 2
100
+ )
101
+ },
102
+ }),
103
+ },
104
+ }
105
+ }
106
+
107
+ export default ListAvailableSecretKeysPlugin
@@ -99,7 +99,7 @@ export const RunWorkflowPlugin: Plugin = async ({ client }) => {
99
99
  tool: {
100
100
  runWorkflow: tool({
101
101
  description:
102
- "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.",
102
+ "Execute a workflow by slug, optionally passing runtime arguments in `inputs`. The `inputs` object is the supported way to provide workflow parameters: it is validated against the schema in workflow.json and then forwarded to the workflow's `run.py` as command-line arguments. The tool streams output in real-time and enforces the timeout defined in workflow.json.",
103
103
  args: {
104
104
  slug: tool.schema
105
105
  .string()
@@ -111,7 +111,7 @@ export const RunWorkflowPlugin: Plugin = async ({ client }) => {
111
111
  .optional()
112
112
  .default({})
113
113
  .describe(
114
- "Key-value pairs matching the workflow's input schema from workflow.json"
114
+ "Runtime workflow arguments. Provide key-value pairs matching the `inputs` schema in workflow.json; these values are validated and passed through to `run.py`."
115
115
  ),
116
116
  },
117
117
  async execute(args, context) {