teamcopilot 0.0.2 → 0.1.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 (43) hide show
  1. package/README.md +6 -26
  2. package/dist/frontend/assets/{cssMode-BDT3WbVs.js → cssMode-Bsa7tmw2.js} +1 -1
  3. package/dist/frontend/assets/{freemarker2-C7-hEgID.js → freemarker2-CrKZYA9-.js} +1 -1
  4. package/dist/frontend/assets/{handlebars-4cwTkPir.js → handlebars-BgBTN99O.js} +1 -1
  5. package/dist/frontend/assets/{html-YNfE1Q0A.js → html-D36ptcmC.js} +1 -1
  6. package/dist/frontend/assets/{htmlMode-opTQ1HoB.js → htmlMode-CGVK_FcF.js} +1 -1
  7. package/dist/frontend/assets/{index-DWyaVa1h.js → index-BcB_fBN5.js} +133 -133
  8. package/dist/frontend/assets/index-Ds8n3J4W.css +1 -0
  9. package/dist/frontend/assets/{javascript-BEwGzk7T.js → javascript-C6sCQzuh.js} +1 -1
  10. package/dist/frontend/assets/{jsonMode-CGhIS5Al.js → jsonMode-Bh2liG7i.js} +1 -1
  11. package/dist/frontend/assets/{liquid-QekTGCGJ.js → liquid-CvdwL9kp.js} +1 -1
  12. package/dist/frontend/assets/{mdx-BAVDaB7v.js → mdx-CdZzWG3e.js} +1 -1
  13. package/dist/frontend/assets/{python-BQlHw7XO.js → python-CSUJS9Bk.js} +1 -1
  14. package/dist/frontend/assets/{razor-Be3Wwc2E.js → razor-ypTGnW2v.js} +1 -1
  15. package/dist/frontend/assets/{tsMode-CIBFoN3z.js → tsMode-Cr9H7DaY.js} +1 -1
  16. package/dist/frontend/assets/{typescript-BuV9wEIE.js → typescript-wuTifRF-.js} +1 -1
  17. package/dist/frontend/assets/{xml-DcDKYaM4.js → xml-DoFtLqDh.js} +1 -1
  18. package/dist/frontend/assets/{yaml-CuBNmOuI.js → yaml-CrPwKSmj.js} +1 -1
  19. package/dist/frontend/index.html +2 -2
  20. package/dist/opencode-auth/index.js +26 -3
  21. package/dist/opencode-server.js +5 -0
  22. package/dist/utils/approval-snapshot-common.js +29 -16
  23. package/dist/utils/opencode-auth.js +112 -0
  24. package/dist/utils/resource-files.js +22 -21
  25. package/dist/utils/skill.js +39 -9
  26. package/dist/utils/workspace-sync.js +12 -7
  27. package/dist/workspace_files/.opencode/opencode.json +4 -1
  28. package/dist/workspace_files/.opencode/package.json +2 -1
  29. package/dist/workspace_files/.opencode/plugins/createSkill.ts +38 -7
  30. package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +34 -3
  31. package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +33 -2
  32. package/dist/workspace_files/.opencode/plugins/findSkill.ts +36 -5
  33. package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +34 -3
  34. package/dist/workspace_files/.opencode/plugins/honeytoken-protection.ts +1 -1
  35. package/dist/workspace_files/.opencode/plugins/listAvailableSkills.ts +33 -2
  36. package/dist/workspace_files/.opencode/plugins/listAvailableWorkflows.ts +33 -2
  37. package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +34 -3
  38. package/dist/workspace_files/.opencode/plugins/skill-command-guard.ts +138 -0
  39. package/dist/workspace_files/AGENTS.md +3 -3
  40. package/dist/workspace_files/package-lock.json +2 -1
  41. package/dist/workspace_files/package.json +3 -1
  42. package/package.json +1 -1
  43. package/dist/frontend/assets/index-lXrsgeTF.css +0 -1
@@ -62,6 +62,32 @@ function assertNoSymlink(absolutePath, resourceLabel) {
62
62
  };
63
63
  }
64
64
  }
65
+ function resolveSnapshotRootPath(rootPath, slug, resourceLabel) {
66
+ if (!fs_1.default.existsSync(rootPath)) {
67
+ throw {
68
+ status: 404,
69
+ message: `${capitalize(resourceLabel)} not found for slug: ${slug}`
70
+ };
71
+ }
72
+ const lstat = fs_1.default.lstatSync(rootPath);
73
+ if (lstat.isSymbolicLink()) {
74
+ const resolvedPath = fs_1.default.realpathSync(rootPath);
75
+ if (!fs_1.default.statSync(resolvedPath).isDirectory()) {
76
+ throw {
77
+ status: 400,
78
+ message: `${capitalize(resourceLabel)} path is not a directory`
79
+ };
80
+ }
81
+ return resolvedPath;
82
+ }
83
+ if (!lstat.isDirectory()) {
84
+ throw {
85
+ status: 400,
86
+ message: `${capitalize(resourceLabel)} path is not a directory`
87
+ };
88
+ }
89
+ return rootPath;
90
+ }
65
91
  function ensureDirectoryExists(absoluteDir) {
66
92
  fs_1.default.mkdirSync(absoluteDir, { recursive: true });
67
93
  }
@@ -150,20 +176,8 @@ function computeSnapshotHash(files) {
150
176
  return hash.digest("hex");
151
177
  }
152
178
  function collectCurrentResourceSnapshot(options) {
153
- const { root_path: rootPath, slug, resource_label: resourceLabel } = options;
154
- if (!fs_1.default.existsSync(rootPath)) {
155
- throw {
156
- status: 404,
157
- message: `${capitalize(resourceLabel)} not found for slug: ${slug}`
158
- };
159
- }
160
- assertNoSymlink(rootPath, resourceLabel);
161
- if (!fs_1.default.statSync(rootPath).isDirectory()) {
162
- throw {
163
- status: 400,
164
- message: `${capitalize(resourceLabel)} path is not a directory`
165
- };
166
- }
179
+ const { root_path: configuredRootPath, slug, resource_label: resourceLabel } = options;
180
+ const rootPath = resolveSnapshotRootPath(configuredRootPath, slug, resourceLabel);
167
181
  const files = [];
168
182
  collectFilesRecursive(rootPath, "", files, resourceLabel);
169
183
  files.sort((a, b) => a.relative_path.localeCompare(b.relative_path));
@@ -322,8 +336,7 @@ async function restoreResourceToApprovedSnapshot(config, userId) {
322
336
  message: "No approved snapshot exists to restore"
323
337
  };
324
338
  }
325
- const rootPath = config.root_path;
326
- assertNoSymlink(rootPath, config.resource_label);
339
+ const rootPath = resolveSnapshotRootPath(config.root_path, config.slug, config.resource_label);
327
340
  const currentPaths = [];
328
341
  collectCurrentNonIgnoredFilePaths(rootPath, "", currentPaths, config.resource_label);
329
342
  const snapshotPathSet = new Set(snapshot.files.map((file) => file.relative_path));
@@ -5,14 +5,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getConfiguredModelProviderId = getConfiguredModelProviderId;
7
7
  exports.initializeOpencodeAuthStorage = initializeOpencodeAuthStorage;
8
+ exports.isServiceManagedProvider = isServiceManagedProvider;
9
+ exports.hasRuntimeProviderCredentials = hasRuntimeProviderCredentials;
8
10
  exports.getRuntimeProviderAuth = getRuntimeProviderAuth;
9
11
  exports.setRuntimeProviderAuth = setRuntimeProviderAuth;
12
+ exports.syncManagedProviderConfiguration = syncManagedProviderConfiguration;
10
13
  const promises_1 = __importDefault(require("fs/promises"));
11
14
  const path_1 = __importDefault(require("path"));
12
15
  const assert_1 = require("./assert");
13
16
  const workspace_sync_1 = require("./workspace-sync");
14
17
  const WORKSPACE_OPENCODE_DIR = ".opencode";
15
18
  const WORKSPACE_AUTH_FILE = "auth.json";
19
+ const WORKSPACE_CONFIG_FILE = "opencode.json";
16
20
  const RUNTIME_DATA_HOME_DIR = "xdg-data";
17
21
  function getWorkspaceOpencodeDir() {
18
22
  return path_1.default.join((0, workspace_sync_1.getWorkspaceDirFromEnv)(), WORKSPACE_OPENCODE_DIR);
@@ -23,6 +27,9 @@ function getRuntimeDataHomePath() {
23
27
  function getRuntimeAuthPath() {
24
28
  return path_1.default.join(getRuntimeDataHomePath(), "opencode", WORKSPACE_AUTH_FILE);
25
29
  }
30
+ function getWorkspaceOpencodeConfigPath() {
31
+ return path_1.default.join(getWorkspaceOpencodeDir(), WORKSPACE_CONFIG_FILE);
32
+ }
26
33
  function normalizeProviderId(providerId) {
27
34
  return providerId.replace(/\/+$/, "");
28
35
  }
@@ -80,6 +87,35 @@ async function writeAuthRecord(filepath, data) {
80
87
  await promises_1.default.rename(tempPath, filepath);
81
88
  await promises_1.default.chmod(filepath, 0o600).catch(() => { });
82
89
  }
90
+ async function readOpencodeConfig(filepath) {
91
+ try {
92
+ const content = await promises_1.default.readFile(filepath, "utf-8");
93
+ const parsed = JSON.parse(content);
94
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
95
+ throw new Error("Invalid opencode config");
96
+ }
97
+ return parsed;
98
+ }
99
+ catch (err) {
100
+ const nodeError = err;
101
+ if (nodeError.code === "ENOENT") {
102
+ return {
103
+ $schema: "https://opencode.ai/config.json",
104
+ };
105
+ }
106
+ throw err;
107
+ }
108
+ }
109
+ async function writeOpencodeConfig(filepath, data) {
110
+ await promises_1.default.mkdir(path_1.default.dirname(filepath), { recursive: true });
111
+ const tempPath = `${filepath}.tmp-${process.pid}-${Date.now()}`;
112
+ await promises_1.default.writeFile(tempPath, `${JSON.stringify(data, null, 2)}\n`, {
113
+ encoding: "utf-8",
114
+ mode: 0o600,
115
+ });
116
+ await promises_1.default.rename(tempPath, filepath);
117
+ await promises_1.default.chmod(filepath, 0o600).catch(() => { });
118
+ }
83
119
  function getConfiguredModelProviderId() {
84
120
  const model = (0, assert_1.assertEnv)("OPENCODE_MODEL");
85
121
  const [providerId, ...parts] = model.split("/");
@@ -88,6 +124,14 @@ function getConfiguredModelProviderId() {
88
124
  }
89
125
  return providerId;
90
126
  }
127
+ function getConfiguredModelId() {
128
+ const model = (0, assert_1.assertEnv)("OPENCODE_MODEL");
129
+ const [providerId, ...parts] = model.split("/");
130
+ if (!providerId || parts.length === 0) {
131
+ throw new Error("OPENCODE_MODEL must be in the format <provider>/<model>");
132
+ }
133
+ return parts.join("/");
134
+ }
91
135
  function configureOpencodeDataHome() {
92
136
  const runtimeDataHome = getRuntimeDataHomePath();
93
137
  process.env.XDG_DATA_HOME = runtimeDataHome;
@@ -112,6 +156,40 @@ function getAuthForProvider(record, providerId) {
112
156
  const normalizedProviderId = normalizeProviderId(providerId);
113
157
  return record[providerId] || record[normalizedProviderId] || record[`${normalizedProviderId}/`];
114
158
  }
159
+ function isAzureCustomProvider(providerId) {
160
+ return normalizeProviderId(providerId).toLowerCase() === "azure-openai";
161
+ }
162
+ function isServiceManagedProvider(providerId) {
163
+ return isAzureCustomProvider(providerId);
164
+ }
165
+ function normalizeAzureEndpoint(endpoint) {
166
+ return endpoint.trim().replace(/\/+$/, "");
167
+ }
168
+ function hasRequiredAzureEnvironment() {
169
+ return isNonEmptyString(process.env.AZURE_API_KEY)
170
+ && isNonEmptyString(process.env.AZURE_OPENAI_ENDPOINT)
171
+ && isNonEmptyString(process.env.AZURE_OPENAI_API_VERSION);
172
+ }
173
+ async function hasAzureProviderConfiguration(providerId) {
174
+ const deployment = getConfiguredModelId().trim();
175
+ const config = await readOpencodeConfig(getWorkspaceOpencodeConfigPath());
176
+ const provider = config.provider?.[normalizeProviderId(providerId)];
177
+ if (!provider || provider.npm !== "@ai-sdk/azure") {
178
+ return false;
179
+ }
180
+ const models = provider.models;
181
+ if (!models || typeof models !== "object" || Array.isArray(models)) {
182
+ return false;
183
+ }
184
+ const deploymentConfig = models[deployment];
185
+ return Boolean(deploymentConfig && typeof deploymentConfig === "object" && !Array.isArray(deploymentConfig));
186
+ }
187
+ async function hasRuntimeProviderCredentials(providerId) {
188
+ if (isAzureCustomProvider(providerId)) {
189
+ return hasRequiredAzureEnvironment() && await hasAzureProviderConfiguration(providerId);
190
+ }
191
+ return Boolean(await getRuntimeProviderAuth(providerId));
192
+ }
115
193
  async function getRuntimeProviderAuth(providerId) {
116
194
  const record = await readAuthRecord(getRuntimeAuthPath());
117
195
  return getAuthForProvider(record, providerId);
@@ -124,3 +202,37 @@ async function setRuntimeProviderAuth(providerId, info) {
124
202
  runtimeRecord[normalizedProviderId] = info;
125
203
  await writeAuthRecord(getRuntimeAuthPath(), runtimeRecord);
126
204
  }
205
+ async function syncManagedProviderConfiguration() {
206
+ const providerId = getConfiguredModelProviderId();
207
+ if (!isAzureCustomProvider(providerId) || !hasRequiredAzureEnvironment()) {
208
+ return;
209
+ }
210
+ const endpoint = normalizeAzureEndpoint((0, assert_1.assertEnv)("AZURE_OPENAI_ENDPOINT"));
211
+ const apiVersion = (0, assert_1.assertEnv)("AZURE_OPENAI_API_VERSION").trim();
212
+ const deployment = getConfiguredModelId().trim();
213
+ const normalizedProviderId = normalizeProviderId(providerId);
214
+ const configPath = getWorkspaceOpencodeConfigPath();
215
+ const configRecord = await readOpencodeConfig(configPath);
216
+ const providerRecord = configRecord.provider ?? {};
217
+ const existingProviderConfig = providerRecord[normalizedProviderId];
218
+ configRecord.provider = {
219
+ ...providerRecord,
220
+ [normalizedProviderId]: {
221
+ ...(existingProviderConfig ?? {}),
222
+ npm: "@ai-sdk/azure",
223
+ name: "Azure OpenAI",
224
+ models: {
225
+ [deployment]: {
226
+ name: deployment,
227
+ },
228
+ },
229
+ options: {
230
+ ...(existingProviderConfig?.options ?? {}),
231
+ baseURL: `${endpoint}/openai`,
232
+ apiVersion,
233
+ useDeploymentBasedUrls: true,
234
+ },
235
+ },
236
+ };
237
+ await writeOpencodeConfig(configPath, configRecord);
238
+ }
@@ -11,8 +11,22 @@ const redact_1 = require("./redact");
11
11
  function createResourceFileManager(options) {
12
12
  const { getResourcePath, resourceLabel, editorLabel } = options;
13
13
  const rootLabel = `${resourceLabel} root`;
14
- const symlinkRootError = `${editorLabel} editor does not support symlinked ${resourceLabel} directories`;
15
14
  const symlinkError = `${editorLabel} editor does not support symlinks`;
15
+ function getResolvedRootPaths(slug) {
16
+ const configuredRoot = getResourcePath(slug);
17
+ try {
18
+ return {
19
+ configuredRoot,
20
+ realRoot: fs_1.default.realpathSync(configuredRoot),
21
+ };
22
+ }
23
+ catch {
24
+ throw {
25
+ status: 404,
26
+ message: `${rootLabel} not found`
27
+ };
28
+ }
29
+ }
16
30
  function toEtag(buffer) {
17
31
  return crypto_1.default.createHash("sha256").update(buffer).digest("hex");
18
32
  }
@@ -64,21 +78,8 @@ function createResourceFileManager(options) {
64
78
  }
65
79
  return absolute;
66
80
  }
67
- function assertRootNotSymlink(slug) {
68
- const resourceRoot = getResourcePath(slug);
69
- if (!fs_1.default.existsSync(resourceRoot)) {
70
- return;
71
- }
72
- const rootLstat = fs_1.default.lstatSync(resourceRoot);
73
- if (rootLstat.isSymbolicLink()) {
74
- throw {
75
- status: 400,
76
- message: symlinkRootError
77
- };
78
- }
79
- }
80
81
  function assertRealPathWithinRoot(slug, absolutePath) {
81
- const realRoot = fs_1.default.realpathSync(getResourcePath(slug));
82
+ const { realRoot } = getResolvedRootPaths(slug);
82
83
  const realTarget = fs_1.default.realpathSync(absolutePath);
83
84
  if (realTarget !== realRoot && !realTarget.startsWith(`${realRoot}${path_1.default.sep}`)) {
84
85
  throw {
@@ -88,9 +89,14 @@ function createResourceFileManager(options) {
88
89
  }
89
90
  }
90
91
  function assertNoSymlinkInAncestors(slug, absolutePath) {
91
- const root = path_1.default.resolve(getResourcePath(slug));
92
+ const { configuredRoot: rawConfiguredRoot, realRoot: rawRealRoot } = getResolvedRootPaths(slug);
93
+ const configuredRoot = path_1.default.resolve(rawConfiguredRoot);
94
+ const realRoot = path_1.default.resolve(rawRealRoot);
92
95
  let current = path_1.default.resolve(absolutePath);
93
96
  while (true) {
97
+ if (current === configuredRoot || current === realRoot) {
98
+ return;
99
+ }
94
100
  const lstat = fs_1.default.lstatSync(current);
95
101
  if (lstat.isSymbolicLink()) {
96
102
  throw {
@@ -98,9 +104,6 @@ function createResourceFileManager(options) {
98
104
  message: symlinkError
99
105
  };
100
106
  }
101
- if (current === root) {
102
- return;
103
- }
104
107
  const parent = path_1.default.dirname(current);
105
108
  if (parent === current) {
106
109
  throw {
@@ -112,12 +115,10 @@ function createResourceFileManager(options) {
112
115
  }
113
116
  }
114
117
  function assertExistingPathIsSafe(slug, absolutePath) {
115
- assertRootNotSymlink(slug);
116
118
  assertRealPathWithinRoot(slug, absolutePath);
117
119
  assertNoSymlinkInAncestors(slug, absolutePath);
118
120
  }
119
121
  function assertParentDirectoryIsSafeForCreate(slug, parentAbsolutePath) {
120
- assertRootNotSymlink(slug);
121
122
  assertRealPathWithinRoot(slug, parentAbsolutePath);
122
123
  assertNoSymlinkInAncestors(slug, parentAbsolutePath);
123
124
  }
@@ -15,7 +15,7 @@ const client_1 = __importDefault(require("../prisma/client"));
15
15
  const workspace_sync_1 = require("./workspace-sync");
16
16
  const permission_common_1 = require("./permission-common");
17
17
  function getSkillsRootPath() {
18
- return path_1.default.join((0, workspace_sync_1.getWorkspaceDirFromEnv)(), "custom-skills");
18
+ return path_1.default.join((0, workspace_sync_1.getWorkspaceDirFromEnv)(), ".agents", "skills");
19
19
  }
20
20
  function getSkillPath(slug) {
21
21
  return path_1.default.join(getSkillsRootPath(), slug);
@@ -66,9 +66,19 @@ function listSkillSlugs() {
66
66
  const entries = fs_1.default.readdirSync(skillsDir, { withFileTypes: true });
67
67
  const slugs = [];
68
68
  for (const entry of entries) {
69
- if (!entry.isDirectory())
70
- continue;
71
- const canonicalManifestPath = path_1.default.join(skillsDir, entry.name, "SKILL.md");
69
+ const skillEntryPath = path_1.default.join(skillsDir, entry.name);
70
+ if (!entry.isDirectory()) {
71
+ if (!entry.isSymbolicLink())
72
+ continue;
73
+ try {
74
+ if (!fs_1.default.statSync(skillEntryPath).isDirectory())
75
+ continue;
76
+ }
77
+ catch {
78
+ continue;
79
+ }
80
+ }
81
+ const canonicalManifestPath = path_1.default.join(skillEntryPath, "SKILL.md");
72
82
  if (fs_1.default.existsSync(canonicalManifestPath)) {
73
83
  slugs.push(entry.name);
74
84
  }
@@ -76,12 +86,32 @@ function listSkillSlugs() {
76
86
  return slugs;
77
87
  }
78
88
  function extractFrontmatterValue(frontmatter, key) {
79
- const pattern = new RegExp(`^${key}\\s*:\\s*(.+)$`, "m");
80
- const match = frontmatter.match(pattern);
81
- if (!match) {
82
- return null;
89
+ const lines = frontmatter.split("\n");
90
+ for (let index = 0; index < lines.length; index += 1) {
91
+ const line = lines[index];
92
+ const match = line.match(new RegExp(`^${key}\\s*:\\s*(.*)$`));
93
+ if (!match) {
94
+ continue;
95
+ }
96
+ const rawValue = match[1]?.trim() ?? "";
97
+ if (rawValue === "|" || rawValue === ">") {
98
+ const blockLines = [];
99
+ for (let blockIndex = index + 1; blockIndex < lines.length; blockIndex += 1) {
100
+ const blockLine = lines[blockIndex] ?? "";
101
+ if (blockLine.length === 0) {
102
+ blockLines.push("");
103
+ continue;
104
+ }
105
+ if (!/^\s+/.test(blockLine)) {
106
+ break;
107
+ }
108
+ blockLines.push(blockLine.replace(/^\s+/, ""));
109
+ }
110
+ return blockLines.join(rawValue === ">" ? " " : "\n").trim();
111
+ }
112
+ return rawValue.replace(/^["']|["']$/g, "");
83
113
  }
84
- return match[1]?.trim().replace(/^["']|["']$/g, "") ?? null;
114
+ return null;
85
115
  }
86
116
  function readSkillManifest(slug) {
87
117
  const skillManifestPath = getSkillManifestPath(slug);
@@ -21,6 +21,7 @@ const WORKSPACE_DB_DIRECTORY = ".sqlite";
21
21
  const WORKSPACE_DB_FILENAME = "data.db";
22
22
  const HONEYTOKEN_UUID = "1f9f0b72-5f9f-4c9b-aef1-2fb2e0f6d8c4";
23
23
  const HONEYTOKEN_FILE_NAME = `honeytoken-${HONEYTOKEN_UUID}.txt`;
24
+ const WORKSPACE_AZURE_PROVIDER_VERSION = "3.0.48";
24
25
  function getWorkspaceDirFromEnv() {
25
26
  let workspaceDir = (0, assert_1.assertEnv)("WORKSPACE_DIR");
26
27
  if (!path_1.default.isAbsolute(workspaceDir)) {
@@ -48,13 +49,11 @@ function ensureWorkspaceDatabaseDirectory() {
48
49
  function normalizeRelativePath(relativePath) {
49
50
  return relativePath.split(path_1.default.sep).join("/");
50
51
  }
51
- const MANAGED_WORKSPACE_DIRECTORIES = new Set([
52
- "workflows",
53
- "custom-skills",
54
- ]);
55
52
  function shouldSkipManagedDirectoryContent(relativePath) {
56
- const [topLevelSegment] = relativePath.split("/");
57
- if (MANAGED_WORKSPACE_DIRECTORIES.has(topLevelSegment)) {
53
+ if (relativePath === "workflows" || relativePath.startsWith("workflows/")) {
54
+ return true;
55
+ }
56
+ if (relativePath === ".agents/skills" || relativePath.startsWith(".agents/skills/")) {
58
57
  return true;
59
58
  }
60
59
  if (relativePath === ".opencode/xdg-data" || relativePath.startsWith(".opencode/xdg-data/")) {
@@ -178,6 +177,9 @@ function syncTemplateDirectory(sourceDirectory, targetDirectory, relativePath, i
178
177
  mergeGitignoreFile(sourceEntryPath, targetEntryPath);
179
178
  continue;
180
179
  }
180
+ if (relativeEntryPath === ".opencode/opencode.json" && fs_1.default.existsSync(targetEntryPath)) {
181
+ continue;
182
+ }
181
183
  if (entry.name === "package.json") {
182
184
  mergePackageJsonFile(sourceEntryPath, targetEntryPath);
183
185
  continue;
@@ -195,6 +197,9 @@ async function initializeWorkspaceNodeDependencies(workspaceDir) {
195
197
  ...(existingPackageJson.dependencies ?? {}),
196
198
  "opencode-ai": "1.1.65",
197
199
  };
200
+ if ((0, assert_1.assertEnv)("OPENCODE_MODEL").startsWith("azure-openai/")) {
201
+ dependencies["@ai-sdk/azure"] = WORKSPACE_AZURE_PROVIDER_VERSION;
202
+ }
198
203
  const workspacePackageJson = {
199
204
  ...existingPackageJson,
200
205
  dependencies,
@@ -210,7 +215,7 @@ async function initializeWorkspaceDirectory() {
210
215
  fs_1.default.mkdirSync(workspaceDir, { recursive: true });
211
216
  const workflowsDir = path_1.default.join(workspaceDir, "workflows");
212
217
  fs_1.default.mkdirSync(workflowsDir, { recursive: true });
213
- const skillsDir = path_1.default.join(workspaceDir, "custom-skills");
218
+ const skillsDir = path_1.default.join(workspaceDir, ".agents", "skills");
214
219
  fs_1.default.mkdirSync(skillsDir, { recursive: true });
215
220
  const honeytokenValue = `DO_NOT_EXPOSE:${HONEYTOKEN_UUID}\n`;
216
221
  fs_1.default.writeFileSync(path_1.default.join(workflowsDir, HONEYTOKEN_FILE_NAME), honeytokenValue, "utf-8");
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
+ "tools": {
4
+ "skill": false
5
+ },
3
6
  "mcp": {
4
7
  "brave-search": {
5
8
  "type": "local",
@@ -14,4 +17,4 @@
14
17
  }
15
18
  }
16
19
  }
17
- }
20
+ }
@@ -10,5 +10,6 @@
10
10
  "devDependencies": {
11
11
  "@types/node": "^20.0.0",
12
12
  "typescript": "^5.0.0"
13
- }
13
+ },
14
+ "scripts": {}
14
15
  }
@@ -24,6 +24,14 @@ interface PermissionResponse {
24
24
  approved: boolean
25
25
  }
26
26
 
27
+ interface SessionLookupResponse {
28
+ error?: unknown
29
+ data?: {
30
+ id?: string
31
+ parentID?: string
32
+ }
33
+ }
34
+
27
35
  async function readErrorMessageFromResponse(
28
36
  response: Response,
29
37
  fallbackMessage: string
@@ -191,7 +199,29 @@ function buildSkillMarkdown(
191
199
  return `---\nname: ${JSON.stringify(slug)}\ndescription: ${JSON.stringify(description)}\n---\n\n${body}\n`
192
200
  }
193
201
 
194
- export const CreateSkillPlugin: Plugin = async (_ctx) => {
202
+ export const CreateSkillPlugin: Plugin = async ({ client }) => {
203
+ async function resolveRootSessionID(sessionID: string): Promise<string> {
204
+ let currentSessionID = sessionID
205
+
206
+ while (true) {
207
+ const response = (await client.session.get({
208
+ path: {
209
+ id: currentSessionID,
210
+ },
211
+ })) as SessionLookupResponse
212
+ if (response.error) {
213
+ throw new Error(`Failed to resolve root session for ${currentSessionID}`)
214
+ }
215
+
216
+ const parentID = response.data?.parentID
217
+ if (!parentID) {
218
+ return currentSessionID
219
+ }
220
+
221
+ currentSessionID = parentID
222
+ }
223
+ }
224
+
195
225
  return {
196
226
  tool: {
197
227
  createSkill: tool({
@@ -210,6 +240,7 @@ export const CreateSkillPlugin: Plugin = async (_ctx) => {
210
240
  },
211
241
  async execute(args, context) {
212
242
  const { directory, sessionID } = context
243
+ const authSessionID = await resolveRootSessionID(sessionID)
213
244
  const slug = args.slug.trim()
214
245
  const description = args.description.trim()
215
246
  const content = args.content.trim()
@@ -238,7 +269,7 @@ export const CreateSkillPlugin: Plugin = async (_ctx) => {
238
269
  throw new Error("Could not determine call id from tool context.")
239
270
  }
240
271
 
241
- const skillsDir = path.join(directory, "custom-skills")
272
+ const skillsDir = path.join(directory, ".agents", "skills")
242
273
  const skillDir = path.join(skillsDir, slug)
243
274
 
244
275
  if (!isPathInside(skillDir, skillsDir)) {
@@ -250,7 +281,7 @@ export const CreateSkillPlugin: Plugin = async (_ctx) => {
250
281
  }
251
282
 
252
283
  await requestCreationPermission(
253
- sessionID,
284
+ authSessionID,
254
285
  messageId,
255
286
  callId
256
287
  )
@@ -259,7 +290,7 @@ export const CreateSkillPlugin: Plugin = async (_ctx) => {
259
290
  method: "POST",
260
291
  headers: {
261
292
  "Content-Type": "application/json",
262
- Authorization: `Bearer ${sessionID}`,
293
+ Authorization: `Bearer ${authSessionID}`,
263
294
  },
264
295
  body: JSON.stringify({
265
296
  name: slug,
@@ -278,7 +309,7 @@ export const CreateSkillPlugin: Plugin = async (_ctx) => {
278
309
  `${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}/files/content?path=${encodeURIComponent("SKILL.md")}`,
279
310
  {
280
311
  headers: {
281
- Authorization: `Bearer ${sessionID}`,
312
+ Authorization: `Bearer ${authSessionID}`,
282
313
  },
283
314
  }
284
315
  )
@@ -302,7 +333,7 @@ export const CreateSkillPlugin: Plugin = async (_ctx) => {
302
333
  method: "PUT",
303
334
  headers: {
304
335
  "Content-Type": "application/json",
305
- Authorization: `Bearer ${sessionID}`,
336
+ Authorization: `Bearer ${authSessionID}`,
306
337
  },
307
338
  body: JSON.stringify({
308
339
  path: "SKILL.md",
@@ -324,7 +355,7 @@ export const CreateSkillPlugin: Plugin = async (_ctx) => {
324
355
  skill: {
325
356
  slug,
326
357
  description,
327
- file_path: `custom-skills/${slug}/SKILL.md`,
358
+ file_path: `.agents/skills/${slug}/SKILL.md`,
328
359
  },
329
360
  },
330
361
  null,
@@ -37,6 +37,14 @@ interface PermissionResponse {
37
37
  approved: boolean
38
38
  }
39
39
 
40
+ interface SessionLookupResponse {
41
+ error?: unknown
42
+ data?: {
43
+ id?: string
44
+ parentID?: string
45
+ }
46
+ }
47
+
40
48
  // ============================================================================
41
49
  // Constants
42
50
  // ============================================================================
@@ -195,7 +203,29 @@ async function requestWorkflowPermission(
195
203
  // Plugin
196
204
  // ============================================================================
197
205
 
198
- export const CreateWorkflowPlugin: Plugin = async (_ctx) => {
206
+ export const CreateWorkflowPlugin: Plugin = async ({ client }) => {
207
+ async function resolveRootSessionID(sessionID: string): Promise<string> {
208
+ let currentSessionID = sessionID
209
+
210
+ while (true) {
211
+ const response = (await client.session.get({
212
+ path: {
213
+ id: currentSessionID,
214
+ },
215
+ })) as SessionLookupResponse
216
+ if (response.error) {
217
+ throw new Error(`Failed to resolve root session for ${currentSessionID}`)
218
+ }
219
+
220
+ const parentID = response.data?.parentID
221
+ if (!parentID) {
222
+ return currentSessionID
223
+ }
224
+
225
+ currentSessionID = parentID
226
+ }
227
+ }
228
+
199
229
  return {
200
230
  tool: {
201
231
  createWorkflow: tool({
@@ -237,6 +267,7 @@ export const CreateWorkflowPlugin: Plugin = async (_ctx) => {
237
267
  },
238
268
  async execute(args, context) {
239
269
  const { directory, sessionID } = context
270
+ const authSessionID = await resolveRootSessionID(sessionID)
240
271
  const { slug, intent_summary, inputs = {}, timeout_seconds = 300 } = args
241
272
  const messageId = extractMessageId(context)
242
273
  const callId = extractCallId(context)
@@ -278,7 +309,7 @@ export const CreateWorkflowPlugin: Plugin = async (_ctx) => {
278
309
 
279
310
  // Request permission after basic validation and existence checks pass.
280
311
  await requestWorkflowPermission(
281
- sessionID,
312
+ authSessionID,
282
313
  messageId,
283
314
  callId
284
315
  )
@@ -316,7 +347,7 @@ export const CreateWorkflowPlugin: Plugin = async (_ctx) => {
316
347
  method: "POST",
317
348
  headers: {
318
349
  "Content-Type": "application/json",
319
- Authorization: `Bearer ${sessionID}`,
350
+ Authorization: `Bearer ${authSessionID}`,
320
351
  },
321
352
  }
322
353
  )