teamcopilot 0.0.3 → 0.1.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 (40) hide show
  1. package/dist/frontend/assets/{cssMode-BBJnYJ3k.js → cssMode-DPbQk08M.js} +1 -1
  2. package/dist/frontend/assets/{freemarker2-D_OsaDj3.js → freemarker2-DFE4Z2hp.js} +1 -1
  3. package/dist/frontend/assets/{handlebars-OZHda73C.js → handlebars-Dw_PK21Y.js} +1 -1
  4. package/dist/frontend/assets/{html-CdGHj-sb.js → html-DxZRNRpl.js} +1 -1
  5. package/dist/frontend/assets/{htmlMode-DmOnHDtb.js → htmlMode-iGgmnmtJ.js} +1 -1
  6. package/dist/frontend/assets/{index-DNOGp3R4.js → index-CphuwuEr.js} +4 -4
  7. package/dist/frontend/assets/index-Ds8n3J4W.css +1 -0
  8. package/dist/frontend/assets/{javascript--es-Ez5N.js → javascript-Bw_u6aiq.js} +1 -1
  9. package/dist/frontend/assets/{jsonMode-C3d51Wej.js → jsonMode-iBiD9w9m.js} +1 -1
  10. package/dist/frontend/assets/{liquid-D2JYL9PO.js → liquid-nnkE2sbw.js} +1 -1
  11. package/dist/frontend/assets/{mdx-Ch04DugU.js → mdx-CHi7ETpD.js} +1 -1
  12. package/dist/frontend/assets/{python-D-iNySlZ.js → python--OTnmJv2.js} +1 -1
  13. package/dist/frontend/assets/{razor-Bq45_Fpy.js → razor-C6OgnQD9.js} +1 -1
  14. package/dist/frontend/assets/{tsMode-CC8kGhhz.js → tsMode-DzsKqqZD.js} +1 -1
  15. package/dist/frontend/assets/{typescript-CtgyIznM.js → typescript-BMZ0H3W7.js} +1 -1
  16. package/dist/frontend/assets/{xml-D5joow3e.js → xml-HTDEz_LP.js} +1 -1
  17. package/dist/frontend/assets/{yaml-BsRI11pd.js → yaml-CVa5ypgE.js} +1 -1
  18. package/dist/frontend/index.html +2 -2
  19. package/dist/opencode-server.js +3 -0
  20. package/dist/utils/approval-snapshot-common.js +29 -16
  21. package/dist/utils/resource-files.js +22 -21
  22. package/dist/utils/skill.js +39 -9
  23. package/dist/utils/workspace-sync.js +5 -7
  24. package/dist/workspace_files/.opencode/opencode.json +4 -1
  25. package/dist/workspace_files/.opencode/package.json +2 -1
  26. package/dist/workspace_files/.opencode/plugins/createSkill.ts +38 -7
  27. package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +34 -3
  28. package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +33 -2
  29. package/dist/workspace_files/.opencode/plugins/findSkill.ts +36 -5
  30. package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +34 -3
  31. package/dist/workspace_files/.opencode/plugins/honeytoken-protection.ts +1 -1
  32. package/dist/workspace_files/.opencode/plugins/listAvailableSkills.ts +33 -2
  33. package/dist/workspace_files/.opencode/plugins/listAvailableWorkflows.ts +33 -2
  34. package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +34 -3
  35. package/dist/workspace_files/.opencode/plugins/skill-command-guard.ts +138 -0
  36. package/dist/workspace_files/AGENTS.md +3 -3
  37. package/dist/workspace_files/package-lock.json +2 -1
  38. package/dist/workspace_files/package.json +3 -1
  39. package/package.json +1 -1
  40. package/dist/frontend/assets/index-CnBE4Efj.css +0 -1
@@ -18,6 +18,14 @@ interface WorkflowSummary {
18
18
  can_edit: boolean
19
19
  }
20
20
 
21
+ interface SessionLookupResponse {
22
+ error?: unknown
23
+ data?: {
24
+ id?: string
25
+ parentID?: string
26
+ }
27
+ }
28
+
21
29
  async function readErrorMessageFromResponse(
22
30
  response: Response,
23
31
  fallbackMessage: string
@@ -40,7 +48,29 @@ async function readErrorMessageFromResponse(
40
48
  }
41
49
  }
42
50
 
43
- export const ListAvailableWorkflowsPlugin: Plugin = async (_ctx) => {
51
+ export const ListAvailableWorkflowsPlugin: Plugin = async ({ client }) => {
52
+ async function resolveRootSessionID(sessionID: string): Promise<string> {
53
+ let currentSessionID = sessionID
54
+
55
+ while (true) {
56
+ const response = (await client.session.get({
57
+ path: {
58
+ id: currentSessionID,
59
+ },
60
+ })) as SessionLookupResponse
61
+ if (response.error) {
62
+ throw new Error(`Failed to resolve root session for ${currentSessionID}`)
63
+ }
64
+
65
+ const parentID = response.data?.parentID
66
+ if (!parentID) {
67
+ return currentSessionID
68
+ }
69
+
70
+ currentSessionID = parentID
71
+ }
72
+ }
73
+
44
74
  return {
45
75
  tool: {
46
76
  listAvailableWorkflows: tool({
@@ -49,10 +79,11 @@ export const ListAvailableWorkflowsPlugin: Plugin = async (_ctx) => {
49
79
  args: {},
50
80
  async execute(_args, context) {
51
81
  const { sessionID } = context
82
+ const authSessionID = await resolveRootSessionID(sessionID)
52
83
 
53
84
  const response = await fetch(`${getApiBaseUrl()}/api/workflows`, {
54
85
  headers: {
55
- Authorization: `Bearer ${sessionID}`,
86
+ Authorization: `Bearer ${authSessionID}`,
56
87
  },
57
88
  })
58
89
 
@@ -64,7 +64,37 @@ function sleep(ms: number): Promise<void> {
64
64
  return new Promise((resolve) => setTimeout(resolve, ms))
65
65
  }
66
66
 
67
- export const RunWorkflowPlugin: Plugin = async (_ctx) => {
67
+ interface SessionLookupResponse {
68
+ error?: unknown
69
+ data?: {
70
+ id?: string
71
+ parentID?: string
72
+ }
73
+ }
74
+
75
+ export const RunWorkflowPlugin: Plugin = async ({ client }) => {
76
+ async function resolveRootSessionID(sessionID: string): Promise<string> {
77
+ let currentSessionID = sessionID
78
+
79
+ while (true) {
80
+ const response = (await client.session.get({
81
+ path: {
82
+ id: currentSessionID,
83
+ },
84
+ })) as SessionLookupResponse
85
+ if (response.error) {
86
+ throw new Error(`Failed to resolve root session for ${currentSessionID}`)
87
+ }
88
+
89
+ const parentID = response.data?.parentID
90
+ if (!parentID) {
91
+ return currentSessionID
92
+ }
93
+
94
+ currentSessionID = parentID
95
+ }
96
+ }
97
+
68
98
  return {
69
99
  tool: {
70
100
  runWorkflow: tool({
@@ -86,6 +116,7 @@ export const RunWorkflowPlugin: Plugin = async (_ctx) => {
86
116
  },
87
117
  async execute(args, context) {
88
118
  const { sessionID } = context
119
+ const authSessionID = await resolveRootSessionID(sessionID)
89
120
  const { slug, inputs = {} } = args
90
121
  const messageId = extractMessageId(context)
91
122
  const callId = extractCallId(context)
@@ -102,7 +133,7 @@ export const RunWorkflowPlugin: Plugin = async (_ctx) => {
102
133
  method: "POST",
103
134
  headers: {
104
135
  "Content-Type": "application/json",
105
- Authorization: `Bearer ${sessionID}`,
136
+ Authorization: `Bearer ${authSessionID}`,
106
137
  },
107
138
  body: JSON.stringify({
108
139
  slug,
@@ -134,7 +165,7 @@ export const RunWorkflowPlugin: Plugin = async (_ctx) => {
134
165
  {
135
166
  method: "GET",
136
167
  headers: {
137
- Authorization: `Bearer ${sessionID}`,
168
+ Authorization: `Bearer ${authSessionID}`,
138
169
  },
139
170
  }
140
171
  )
@@ -0,0 +1,138 @@
1
+ import type { Plugin } 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 SkillDetailsResponse {
12
+ skill?: {
13
+ is_approved?: boolean
14
+ }
15
+ }
16
+
17
+ async function readErrorMessageFromResponse(
18
+ response: Response,
19
+ fallbackMessage: string
20
+ ): Promise<string> {
21
+ try {
22
+ const text = await response.text()
23
+ if (!text) return fallbackMessage
24
+ try {
25
+ const parsed: unknown = JSON.parse(text)
26
+ if (parsed && typeof parsed === "object" && "message" in parsed) {
27
+ const msg = (parsed as { message?: unknown }).message
28
+ if (typeof msg === "string" && msg.trim().length > 0) return msg
29
+ }
30
+ } catch {
31
+ // fall back to plain text
32
+ }
33
+ return text.trim().length > 0 ? text : fallbackMessage
34
+ } catch {
35
+ return fallbackMessage
36
+ }
37
+ }
38
+
39
+ export function normalizeCommandSlug(command: unknown): string {
40
+ if (typeof command !== "string") {
41
+ return ""
42
+ }
43
+ const trimmed = command.trim()
44
+ const match = trimmed.match(/^\/+([^\s]+)/)
45
+ return match?.[1] ?? ""
46
+ }
47
+
48
+ function getTaskCommand(args: unknown): string | null {
49
+ if (!args || typeof args !== "object" || !("command" in args)) {
50
+ return null
51
+ }
52
+
53
+ const command = (args as { command?: unknown }).command
54
+ const normalized = normalizeCommandSlug(command)
55
+ return normalized.length > 0 ? normalized : null
56
+ }
57
+
58
+ interface SessionLookupResponse {
59
+ error?: unknown
60
+ data?: {
61
+ id?: string
62
+ parentID?: string
63
+ }
64
+ }
65
+
66
+ export const SkillCommandGuard: Plugin = async ({ client }) => {
67
+ async function resolveRootSessionID(sessionID: string): Promise<string> {
68
+ let currentSessionID = sessionID
69
+
70
+ while (true) {
71
+ const response = (await client.session.get({
72
+ path: {
73
+ id: currentSessionID,
74
+ },
75
+ })) as SessionLookupResponse
76
+ if (response.error) {
77
+ throw new Error(`Failed to resolve root session for ${currentSessionID}`)
78
+ }
79
+ const parentID = response.data?.parentID
80
+ if (!parentID) {
81
+ return currentSessionID
82
+ }
83
+ currentSessionID = parentID
84
+ }
85
+ }
86
+
87
+ async function assertAuthorizedSkillCommand(sessionID: string, slug: string): Promise<void> {
88
+ const rootSessionID = await resolveRootSessionID(sessionID)
89
+ const skillDetailsResponse = await fetch(
90
+ `${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}`,
91
+ {
92
+ headers: {
93
+ Authorization: `Bearer ${rootSessionID}`,
94
+ },
95
+ }
96
+ )
97
+
98
+ if (skillDetailsResponse.status === 404) {
99
+ return
100
+ }
101
+
102
+ if (!skillDetailsResponse.ok) {
103
+ const errorMessage = await readErrorMessageFromResponse(
104
+ skillDetailsResponse,
105
+ `Failed to fetch skill metadata for ${slug} (HTTP ${skillDetailsResponse.status})`
106
+ )
107
+ throw new Error(errorMessage)
108
+ }
109
+
110
+ const skillDetailsPayload =
111
+ (await skillDetailsResponse.json()) as SkillDetailsResponse
112
+ const isApproved = skillDetailsPayload.skill?.is_approved === true
113
+
114
+ if (!isApproved) {
115
+ throw new Error(
116
+ `Skill "${slug}" is not approved yet. Only approved skills can be used.`
117
+ )
118
+ }
119
+ }
120
+
121
+ return {
122
+ "tool.execute.before": async (input, output) => {
123
+ if (input.tool !== "task") {
124
+ return
125
+ }
126
+
127
+ const sessionID = typeof input.sessionID === "string" ? input.sessionID.trim() : ""
128
+ const slug = getTaskCommand(output.args)
129
+ if (!sessionID || !slug) {
130
+ return
131
+ }
132
+
133
+ await assertAuthorizedSkillCommand(sessionID, slug)
134
+ },
135
+ }
136
+ }
137
+
138
+ export default SkillCommandGuard
@@ -76,7 +76,7 @@ Before implementing custom instructions or creating new workflow logic, you MUST
76
76
 
77
77
  ### What is a Custom Skill?
78
78
 
79
- A **custom skill** is a reusable instruction package for the agent that lives in `custom-skills/<slug>/`.
79
+ A **custom skill** is a reusable instruction package for the agent that lives in `.agents/skills/<slug>/`.
80
80
  Each custom skill:
81
81
  - Has a unique slug (lowercase, hyphenated, e.g., `triage-support-ticket`)
82
82
  - Uses `SKILL.md` as the canonical manifest/instruction file
@@ -375,7 +375,7 @@ These rules exist to prevent data loss, secret leakage, and unsafe behavior. Vio
375
375
 
376
376
  ### Never delete custom skills
377
377
 
378
- - You must **NEVER delete a custom skill** (folders or files under `custom-skills/<slug>/`).
378
+ - You must **NEVER delete a custom skill** (folders or files under `.agents/skills/<slug>/`).
379
379
  - Custom skill deletions are only supposed to happen via the **UI**.
380
380
  - If cleanup is requested, prefer **deprecating** or updating skill instructions rather than deleting anything.
381
381
 
@@ -398,7 +398,7 @@ These rules exist to prevent data loss, secret leakage, and unsafe behavior. Vio
398
398
 
399
399
  - You MAY read files outside managed workspace areas when needed to understand user requirements, gather context, or answer questions.
400
400
  - Keep any cloned repos, downloaded assets, fixtures, or vendored code **inside** `workflows/<slug>/` only.
401
- - Keep skill artifacts and instruction files inside `custom-skills/<slug>/` only.
401
+ - Keep skill artifacts and instruction files inside `.agents/skills/<slug>/` only.
402
402
 
403
403
  ### No destructive shell actions
404
404
 
@@ -6,7 +6,8 @@
6
6
  "": {
7
7
  "dependencies": {
8
8
  "opencode-ai": "1.1.65"
9
- }
9
+ },
10
+ "devDependencies": {}
10
11
  },
11
12
  "node_modules/opencode-ai": {
12
13
  "version": "1.1.65",
@@ -1,5 +1,7 @@
1
1
  {
2
2
  "dependencies": {
3
3
  "opencode-ai": "1.1.65"
4
- }
4
+ },
5
+ "devDependencies": {},
6
+ "scripts": {}
5
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamcopilot",
3
- "version": "0.0.3",
3
+ "version": "0.1.1",
4
4
  "description": "A shared AI Agent for Teams",
5
5
  "homepage": "https://teamcopilot.ai",
6
6
  "repository": {