@serviceme/devtools-core 0.1.5 → 0.1.6

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/dist/index.js CHANGED
@@ -30,6 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AgentCatalogClient: () => AgentCatalogClient,
34
+ AgentReconciler: () => AgentReconciler,
35
+ AgentStore: () => AgentStore,
33
36
  DaemonLogger: () => DaemonLogger,
34
37
  EnvironmentInspector: () => EnvironmentInspector,
35
38
  GithubCopilotCliExecutor: () => GithubCopilotCliExecutor,
@@ -39,6 +42,10 @@ __export(index_exports, {
39
42
  ProjectTools: () => ProjectTools,
40
43
  SchedulerDaemon: () => SchedulerDaemon,
41
44
  ShellExecutor: () => ShellExecutor,
45
+ SkillCatalogClient: () => SkillCatalogClient,
46
+ SkillReconciler: () => SkillReconciler,
47
+ SkillStore: () => SkillStore,
48
+ TOOL_RISK_MAP: () => TOOL_RISK_MAP,
42
49
  TaskConfigManager: () => TaskConfigManager,
43
50
  TaskExecutionEngine: () => TaskExecutionEngine,
44
51
  TaskLogManager: () => TaskLogManager,
@@ -56,6 +63,7 @@ __export(index_exports, {
56
63
  matchesCron: () => matchesCron,
57
64
  moveFiles: () => moveFiles,
58
65
  noopLogger: () => noopLogger,
66
+ parseAgentToolPermissions: () => parseAgentToolPermissions,
59
67
  parseIntervalMs: () => parseIntervalMs,
60
68
  resolveTaskExecutionPayload: () => resolveTaskExecutionPayload,
61
69
  unzipFile: () => unzipFile,
@@ -63,6 +71,287 @@ __export(index_exports, {
63
71
  });
64
72
  module.exports = __toCommonJS(index_exports);
65
73
 
74
+ // src/agents/AgentCatalogClient.ts
75
+ var AgentCatalogClient = class {
76
+ constructor(options = {}) {
77
+ this.fetchImpl = options.fetchImpl ?? fetch;
78
+ this.baseUrl = options.baseUrl;
79
+ }
80
+ async getCatalog() {
81
+ if (!this.baseUrl) {
82
+ return {
83
+ agents: [],
84
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
85
+ };
86
+ }
87
+ const response = await this.fetchImpl(
88
+ `${this.baseUrl}/api/v1/marketplace/agents`
89
+ );
90
+ if (!response.ok) {
91
+ throw new Error(`Failed to fetch agents catalog: ${response.status}`);
92
+ }
93
+ const data = await response.json();
94
+ return {
95
+ agents: data.agents ?? [],
96
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
97
+ };
98
+ }
99
+ };
100
+
101
+ // src/permissions/agent-permissions.ts
102
+ var TOOL_RISK_MAP = {
103
+ shell: "high",
104
+ terminal: "high",
105
+ run_in_terminal: "high",
106
+ execution_subagent: "high",
107
+ filesystem: "medium",
108
+ fetch: "medium",
109
+ fetch_webpage: "medium",
110
+ create_file: "medium",
111
+ replace_string_in_file: "medium",
112
+ multi_replace_string_in_file: "medium",
113
+ read_file: "low",
114
+ search: "low",
115
+ grep_search: "low",
116
+ file_search: "low",
117
+ semantic_search: "low",
118
+ list_dir: "low"
119
+ };
120
+ var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---/;
121
+ var TOOLS_LINE_REGEX = /^tools:\s*$/m;
122
+ var TOOLS_INLINE_REGEX = /^tools:\s*\[([^\]]*)\]/m;
123
+ var LIST_ITEM_REGEX = /^\s*-\s+(.+)$/;
124
+ function parseAgentToolPermissions(content) {
125
+ const fmMatch = content.match(FRONTMATTER_REGEX);
126
+ if (!fmMatch?.[1]) return [];
127
+ const frontmatter = fmMatch[1];
128
+ const inlineMatch = frontmatter.match(TOOLS_INLINE_REGEX);
129
+ if (inlineMatch?.[1] != null) {
130
+ const raw = inlineMatch[1];
131
+ return raw.split(",").map((t) => t.trim()).filter(Boolean).map((tool) => ({
132
+ tool,
133
+ riskLevel: TOOL_RISK_MAP[tool] ?? "medium"
134
+ }));
135
+ }
136
+ const blockMatch = frontmatter.match(TOOLS_LINE_REGEX);
137
+ if (!blockMatch?.[0]) return [];
138
+ const toolsStartIndex = frontmatter.indexOf(blockMatch[0]) + blockMatch[0].length;
139
+ const remaining = frontmatter.slice(toolsStartIndex);
140
+ const lines = remaining.split(/\r?\n/);
141
+ const tools = [];
142
+ for (const line of lines) {
143
+ const itemMatch = line.match(LIST_ITEM_REGEX);
144
+ if (itemMatch?.[1]) {
145
+ const tool = itemMatch[1].trim();
146
+ tools.push({ tool, riskLevel: TOOL_RISK_MAP[tool] ?? "medium" });
147
+ } else if (line.trim() !== "" && !line.startsWith(" ") && !line.startsWith(" ")) {
148
+ break;
149
+ }
150
+ }
151
+ return tools;
152
+ }
153
+
154
+ // src/agents/AgentReconciler.ts
155
+ var AgentReconciler = class {
156
+ constructor(deps) {
157
+ this.deps = deps;
158
+ }
159
+ async mutate(request) {
160
+ if (request.targetScope !== "workspace" && request.targetScope !== "user") {
161
+ throw new Error(`Invalid target scope: ${String(request.targetScope)}`);
162
+ }
163
+ if (request.action === "uninstall" || request.action === "move" || request.action === "removeExternal") {
164
+ return {
165
+ status: "success",
166
+ changed: true,
167
+ message: `Agent ${request.action} completed.`
168
+ };
169
+ }
170
+ if (request.action !== "install") {
171
+ return {
172
+ status: "blocked",
173
+ changed: false,
174
+ message: `Agent action is not supported by bridge reconciler: ${request.action}`
175
+ };
176
+ }
177
+ const catalog = await this.deps.catalogClient.getCatalog();
178
+ const remoteAgent = catalog.agents.find(
179
+ (agent) => this.deps.agentStore.normalizeRemoteAgentId(agent.id) === request.agentId
180
+ );
181
+ if (!remoteAgent) {
182
+ return {
183
+ status: "blocked",
184
+ changed: false,
185
+ message: "Agent not found in catalog."
186
+ };
187
+ }
188
+ if (!request.confirmed && this.hasHighRiskTool(remoteAgent.tools)) {
189
+ return {
190
+ status: "requires_confirmation",
191
+ changed: false,
192
+ message: "This agent uses high-risk tools that require confirmation.",
193
+ tools: remoteAgent.tools
194
+ };
195
+ }
196
+ return {
197
+ status: "success",
198
+ changed: true,
199
+ message: "Agent installed."
200
+ };
201
+ }
202
+ getPermissionSummary(agentId, agentName, content) {
203
+ const tools = parseAgentToolPermissions(content);
204
+ return {
205
+ agentId,
206
+ agentName,
207
+ tools,
208
+ highRiskCount: tools.filter((tool) => tool.riskLevel === "high").length,
209
+ mediumRiskCount: tools.filter((tool) => tool.riskLevel === "medium").length,
210
+ lowRiskCount: tools.filter((tool) => tool.riskLevel === "low").length
211
+ };
212
+ }
213
+ hasHighRiskTool(tools) {
214
+ return tools.some((tool) => TOOL_RISK_MAP[tool] === "high");
215
+ }
216
+ };
217
+
218
+ // src/agents/AgentStore.ts
219
+ var fs = __toESM(require("fs/promises"));
220
+ var path = __toESM(require("path"));
221
+ var WORKSPACE_AGENTS_ROOT_RELATIVE = ".github/agents";
222
+ var WORKSPACE_AGENTS_STATE_RELATIVE = ".github/.ms-devtools-agents.yml";
223
+ var SAFE_LOCAL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
224
+ function assertSafeLocalAgentId(agentId) {
225
+ if (typeof agentId !== "string" || agentId.length === 0 || agentId === "." || agentId === ".." || agentId.includes("/") || agentId.includes("\\") || !SAFE_LOCAL_ID_PATTERN.test(agentId)) {
226
+ throw new Error(`Invalid agent id: ${agentId}`);
227
+ }
228
+ return agentId;
229
+ }
230
+ var AgentStore = class {
231
+ constructor(options) {
232
+ this.workspacePath = options.workspacePath;
233
+ this.userAgentsRoot = options.userAgentsRoot;
234
+ this.fileSystem = options.fileSystem ?? fs;
235
+ this.schemaVersion = options.schemaVersion ?? 1;
236
+ }
237
+ normalizeRemoteAgentId(remoteId) {
238
+ if (remoteId.startsWith("official/")) {
239
+ return assertSafeLocalAgentId(remoteId.slice("official/".length));
240
+ }
241
+ if (remoteId.startsWith("community/")) {
242
+ const lastSlash = remoteId.lastIndexOf("/");
243
+ return assertSafeLocalAgentId(remoteId.slice(lastSlash + 1));
244
+ }
245
+ return assertSafeLocalAgentId(remoteId);
246
+ }
247
+ getWorkspaceAgentsRootPath() {
248
+ return WORKSPACE_AGENTS_ROOT_RELATIVE;
249
+ }
250
+ getWorkspaceStateFilePath() {
251
+ return WORKSPACE_AGENTS_STATE_RELATIVE;
252
+ }
253
+ getUserAgentsRootPath() {
254
+ return this.userAgentsRoot;
255
+ }
256
+ async listWorkspaceAgentIds() {
257
+ return this.listAgentIds(
258
+ path.join(this.workspacePath, WORKSPACE_AGENTS_ROOT_RELATIVE)
259
+ );
260
+ }
261
+ async listUserAgentIds() {
262
+ return this.listAgentIds(this.userAgentsRoot);
263
+ }
264
+ async readState() {
265
+ try {
266
+ const raw = await this.fileSystem.readFile(
267
+ path.join(this.workspacePath, WORKSPACE_AGENTS_STATE_RELATIVE),
268
+ "utf-8"
269
+ );
270
+ const parsed = JSON.parse(raw);
271
+ if (typeof parsed.schemaVersion !== "number" || !Array.isArray(parsed.installedAgents)) {
272
+ return null;
273
+ }
274
+ return parsed;
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+ async writeState(state) {
280
+ const statePath = path.join(
281
+ this.workspacePath,
282
+ WORKSPACE_AGENTS_STATE_RELATIVE
283
+ );
284
+ await this.fileSystem.mkdir(path.dirname(statePath), { recursive: true });
285
+ await this.fileSystem.writeFile(
286
+ statePath,
287
+ JSON.stringify(state, null, 2),
288
+ "utf-8"
289
+ );
290
+ }
291
+ async addInstalledAgent(entry) {
292
+ const state = await this.readState() ?? {
293
+ schemaVersion: this.schemaVersion,
294
+ installedAgents: []
295
+ };
296
+ state.installedAgents = state.installedAgents.filter(
297
+ (agent) => agent.id !== entry.id
298
+ );
299
+ state.installedAgents.push(entry);
300
+ await this.writeState(state);
301
+ }
302
+ async removeInstalledAgent(agentId) {
303
+ const state = await this.readState();
304
+ if (!state) {
305
+ return;
306
+ }
307
+ state.installedAgents = state.installedAgents.filter(
308
+ (agent) => agent.id !== agentId
309
+ );
310
+ await this.writeState(state);
311
+ }
312
+ async writeAgentFiles(agentId, scope, files) {
313
+ const root = scope === "workspace" ? path.join(this.workspacePath, WORKSPACE_AGENTS_ROOT_RELATIVE) : this.userAgentsRoot;
314
+ const firstFile = files[0];
315
+ const isSingleFlatFile = files.length === 1 && firstFile !== void 0 && firstFile.path === `${agentId}.agent.md` && !firstFile.path.includes("/");
316
+ const targetDir = isSingleFlatFile ? root : path.join(root, agentId);
317
+ await this.fileSystem.mkdir(targetDir, { recursive: true });
318
+ for (const file of files) {
319
+ const filePath = path.join(targetDir, file.path);
320
+ await this.fileSystem.mkdir(path.dirname(filePath), { recursive: true });
321
+ await this.fileSystem.writeFile(filePath, file.content, "utf-8");
322
+ if (file.executable) {
323
+ try {
324
+ await this.fileSystem.chmod(filePath, 493);
325
+ } catch {
326
+ }
327
+ }
328
+ }
329
+ }
330
+ async listAgentIds(dir) {
331
+ try {
332
+ const entries = await this.fileSystem.readdir(dir, {
333
+ withFileTypes: true
334
+ });
335
+ const ids = [];
336
+ for (const entry of entries) {
337
+ if (entry.name.startsWith(".")) {
338
+ continue;
339
+ }
340
+ if (entry.isDirectory()) {
341
+ ids.push(entry.name);
342
+ continue;
343
+ }
344
+ if (entry.isFile() && entry.name.endsWith(".agent.md")) {
345
+ ids.push(entry.name.replace(/\.agent\.md$/, ""));
346
+ }
347
+ }
348
+ return ids.sort();
349
+ } catch {
350
+ return [];
351
+ }
352
+ }
353
+ };
354
+
66
355
  // src/copilot/doctor.ts
67
356
  var import_devtools_protocol = require("@serviceme/devtools-protocol");
68
357
 
@@ -297,7 +586,13 @@ var DEFAULT_TOOL_CHECK_TIMEOUT_MS = 5e3;
297
586
  var TOOL_CHECK_TIMEOUT_MS = {
298
587
  nuget: 12e3,
299
588
  nvm: 8e3,
300
- dotnet: 8e3
589
+ dotnet: 8e3,
590
+ // nvm4w/npm shims and globally-installed .cmd tools (pnpm, nrm) are slow to
591
+ // resolve on cold PATHs. Give them extra headroom so the version probe
592
+ // doesn't fall through to the generic 5s default.
593
+ npm: 15e3,
594
+ pnpm: 15e3,
595
+ nrm: 15e3
301
596
  };
302
597
  var ERROR_CODE_NOT_FOUND = 127;
303
598
  var ERROR_CODE_TIMEOUT = "ETIMEDOUT";
@@ -344,8 +639,29 @@ var EnvironmentInspector = class {
344
639
  return void 0;
345
640
  }
346
641
  }
642
+ /**
643
+ * On Windows, prefer the `.cmd` shim for npm/pnpm/nrm. nvm4w registers
644
+ * BOTH an extensionless entry (pointing to node.exe) and a `<tool>.cmd`
645
+ * wrapper; `where` returns them in that order, and the extensionless one
646
+ * would just print node's own version.
647
+ */
648
+ async getToolShimPath(toolName) {
649
+ try {
650
+ const result = await runCommand("where", {
651
+ args: [toolName],
652
+ timeoutMs: this.getToolTimeout(toolName)
653
+ });
654
+ const candidates = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
655
+ const cmdShim = candidates.find(
656
+ (line) => line.toLowerCase().endsWith(".cmd")
657
+ );
658
+ return cmdShim ?? candidates[0];
659
+ } catch {
660
+ return void 0;
661
+ }
662
+ }
347
663
  async getToolVersion(toolName) {
348
- const invocation = this.getVersionInvocation(toolName);
664
+ const invocation = await this.getVersionInvocation(toolName);
349
665
  const result = await runCommand(invocation.command, {
350
666
  args: invocation.args,
351
667
  timeoutMs: this.getToolTimeout(toolName)
@@ -420,12 +736,19 @@ var EnvironmentInspector = class {
420
736
  return this.handleToolCheckError(error);
421
737
  }
422
738
  }
423
- getVersionInvocation(toolName) {
739
+ async getVersionInvocation(toolName) {
424
740
  const isWindows = process.platform === "win32";
425
741
  if (isWindows && ["npm", "pnpm", "nrm"].includes(toolName)) {
742
+ const shim = await this.getToolShimPath(toolName);
743
+ if (shim) {
744
+ return {
745
+ command: "cmd.exe",
746
+ args: ["/c", shim, "--version"]
747
+ };
748
+ }
426
749
  return {
427
750
  command: "cmd.exe",
428
- args: ["/d", "/s", "/c", `${toolName} --version`]
751
+ args: ["/c", toolName, "--version"]
429
752
  };
430
753
  }
431
754
  const commands = {
@@ -478,8 +801,8 @@ var EnvironmentInspector = class {
478
801
  };
479
802
 
480
803
  // src/image/imageTools.ts
481
- var fs = __toESM(require("fs/promises"));
482
- var path = __toESM(require("path"));
804
+ var fs2 = __toESM(require("fs/promises"));
805
+ var path2 = __toESM(require("path"));
483
806
  var import_devtools_protocol4 = require("@serviceme/devtools-protocol");
484
807
  var SUPPORTED_FORMATS = [
485
808
  ".jpg",
@@ -500,7 +823,7 @@ var ImageTools = class {
500
823
  if (!await this.pathExists(filePath)) {
501
824
  return { valid: false };
502
825
  }
503
- if (!SUPPORTED_FORMATS.includes(path.extname(filePath).toLowerCase())) {
826
+ if (!SUPPORTED_FORMATS.includes(path2.extname(filePath).toLowerCase())) {
504
827
  return { valid: false };
505
828
  }
506
829
  await this.getInfo(filePath, sharpModulePath);
@@ -512,7 +835,7 @@ var ImageTools = class {
512
835
  async getInfo(imagePath, sharpModulePath) {
513
836
  const sharp = this.loadSharp(sharpModulePath);
514
837
  const metadata = await sharp(imagePath).metadata();
515
- const stats = await fs.stat(imagePath);
838
+ const stats = await fs2.stat(imagePath);
516
839
  return {
517
840
  width: metadata.width ?? 0,
518
841
  height: metadata.height ?? 0,
@@ -529,7 +852,7 @@ var ImageTools = class {
529
852
  `Invalid image file: ${imagePath}`
530
853
  );
531
854
  }
532
- const originalStats = await fs.stat(imagePath);
855
+ const originalStats = await fs2.stat(imagePath);
533
856
  const originalSize = originalStats.size;
534
857
  const outputPath = this.getOutputPath(imagePath, options);
535
858
  const compressedBuffer = await this.compressWithSharp(imagePath, options);
@@ -544,7 +867,7 @@ var ImageTools = class {
544
867
  outputPath: imagePath
545
868
  };
546
869
  }
547
- await fs.writeFile(outputPath, compressedBuffer);
870
+ await fs2.writeFile(outputPath, compressedBuffer);
548
871
  return {
549
872
  originalSize,
550
873
  compressedSize,
@@ -555,7 +878,7 @@ var ImageTools = class {
555
878
  async compressWithSharp(imagePath, options) {
556
879
  const sharp = this.loadSharp(options.sharpModulePath);
557
880
  let pipeline = sharp(imagePath);
558
- switch (options.format ?? path.extname(imagePath).toLowerCase().slice(1)) {
881
+ switch (options.format ?? path2.extname(imagePath).toLowerCase().slice(1)) {
559
882
  case "jpg":
560
883
  case "jpeg":
561
884
  pipeline = pipeline.jpeg({ quality: options.quality });
@@ -581,14 +904,14 @@ var ImageTools = class {
581
904
  if (options.replaceOriginImage) {
582
905
  return inputPath;
583
906
  }
584
- const dir = path.dirname(inputPath);
585
- const ext = path.extname(inputPath);
586
- const name = path.basename(inputPath, ext);
587
- return path.join(dir, `${name}_compressed${ext}`);
907
+ const dir = path2.dirname(inputPath);
908
+ const ext = path2.extname(inputPath);
909
+ const name = path2.basename(inputPath, ext);
910
+ return path2.join(dir, `${name}_compressed${ext}`);
588
911
  }
589
912
  async pathExists(targetPath) {
590
913
  try {
591
- await fs.access(targetPath);
914
+ await fs2.access(targetPath);
592
915
  return true;
593
916
  } catch {
594
917
  return false;
@@ -744,8 +1067,8 @@ function createConsoleLogger(prefix = "serviceme") {
744
1067
  }
745
1068
 
746
1069
  // src/project/projectTools.ts
747
- var fs2 = __toESM(require("fs/promises"));
748
- var path2 = __toESM(require("path"));
1070
+ var fs3 = __toESM(require("fs/promises"));
1071
+ var path3 = __toESM(require("path"));
749
1072
  var import_devtools_protocol6 = require("@serviceme/devtools-protocol");
750
1073
 
751
1074
  // src/utils/fileUtils.ts
@@ -866,10 +1189,10 @@ var moveFiles = async (sourceDir, destDir, overwrite = false) => {
866
1189
  var ProjectTools = class {
867
1190
  async extractTemplate(zipPath, workspacePath, tempExtractDir, input) {
868
1191
  await unzipFile(zipPath, tempExtractDir);
869
- let sourceDir = path2.join(tempExtractDir, input.extractedDirName);
1192
+ let sourceDir = path3.join(tempExtractDir, input.extractedDirName);
870
1193
  let actualDirName = input.extractedDirName;
871
1194
  if (!await this.pathExists(sourceDir)) {
872
- const entries = await fs2.readdir(tempExtractDir, { withFileTypes: true });
1195
+ const entries = await fs3.readdir(tempExtractDir, { withFileTypes: true });
873
1196
  const directories = entries.filter(
874
1197
  (entry) => entry.isDirectory() && !entry.name.startsWith(".")
875
1198
  );
@@ -881,7 +1204,7 @@ var ProjectTools = class {
881
1204
  );
882
1205
  if (selectedDirectory) {
883
1206
  actualDirName = selectedDirectory;
884
- sourceDir = path2.join(tempExtractDir, actualDirName);
1207
+ sourceDir = path3.join(tempExtractDir, actualDirName);
885
1208
  } else if (directories.length === 0) {
886
1209
  throw new Error(
887
1210
  `No directory found after extraction. Expected directory: ${input.extractedDirName}`
@@ -939,7 +1262,7 @@ var ProjectTools = class {
939
1262
  } else {
940
1263
  for (const scriptPath of scripts) {
941
1264
  try {
942
- await fs2.chmod(scriptPath, 493);
1265
+ await fs3.chmod(scriptPath, 493);
943
1266
  updatedCount += 1;
944
1267
  } catch {
945
1268
  }
@@ -987,12 +1310,12 @@ var ProjectTools = class {
987
1310
  const results = [];
988
1311
  let entries;
989
1312
  try {
990
- entries = await fs2.readdir(dir, { withFileTypes: true });
1313
+ entries = await fs3.readdir(dir, { withFileTypes: true });
991
1314
  } catch {
992
1315
  return results;
993
1316
  }
994
1317
  for (const entry of entries) {
995
- const fullPath = path2.join(dir, entry.name);
1318
+ const fullPath = path3.join(dir, entry.name);
996
1319
  if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
997
1320
  results.push(...await this.findScripts(fullPath, extensions));
998
1321
  } else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
@@ -1003,7 +1326,7 @@ var ProjectTools = class {
1003
1326
  }
1004
1327
  async pathExists(targetPath) {
1005
1328
  try {
1006
- await fs2.access(targetPath);
1329
+ await fs3.access(targetPath);
1007
1330
  return true;
1008
1331
  } catch {
1009
1332
  return false;
@@ -1022,7 +1345,7 @@ var ProjectTools = class {
1022
1345
  const matches = [];
1023
1346
  for (const directoryName of directoryNames) {
1024
1347
  if (await this.directoryMatchesProjectPattern(
1025
- path2.join(tempExtractDir, directoryName),
1348
+ path3.join(tempExtractDir, directoryName),
1026
1349
  projectFilePattern
1027
1350
  )) {
1028
1351
  matches.push(directoryName);
@@ -1034,7 +1357,7 @@ var ProjectTools = class {
1034
1357
  return null;
1035
1358
  }
1036
1359
  async directoryMatchesProjectPattern(directoryPath, projectFilePattern) {
1037
- const entries = await fs2.readdir(directoryPath);
1360
+ const entries = await fs3.readdir(directoryPath);
1038
1361
  if (projectFilePattern.includes("*")) {
1039
1362
  const regex = new RegExp(`^${projectFilePattern.replace("*", ".*")}$`);
1040
1363
  return entries.some((entry) => regex.test(entry));
@@ -1047,17 +1370,17 @@ function createProjectTools() {
1047
1370
  }
1048
1371
 
1049
1372
  // src/scheduled-tasks/daemon/DaemonLogger.ts
1050
- var fs3 = __toESM(require("fs"));
1051
- var path3 = __toESM(require("path"));
1373
+ var fs4 = __toESM(require("fs"));
1374
+ var path4 = __toESM(require("path"));
1052
1375
  var CONFIG_DIR = ".serviceme";
1053
1376
  var LOG_FILE = "scheduler.log";
1054
1377
  var MAX_LOG_SIZE = 1024 * 1024;
1055
1378
  var DaemonLogger = class {
1056
1379
  constructor(workspacePath) {
1057
- this.logPath = path3.join(workspacePath, CONFIG_DIR, LOG_FILE);
1058
- const dir = path3.dirname(this.logPath);
1059
- if (!fs3.existsSync(dir)) {
1060
- fs3.mkdirSync(dir, { recursive: true });
1380
+ this.logPath = path4.join(workspacePath, CONFIG_DIR, LOG_FILE);
1381
+ const dir = path4.dirname(this.logPath);
1382
+ if (!fs4.existsSync(dir)) {
1383
+ fs4.mkdirSync(dir, { recursive: true });
1061
1384
  }
1062
1385
  }
1063
1386
  getLogPath() {
@@ -1068,16 +1391,16 @@ var DaemonLogger = class {
1068
1391
  const line = `[${ts}] [${level.toUpperCase()}] ${message}
1069
1392
  `;
1070
1393
  this.rotateIfNeeded();
1071
- fs3.appendFileSync(this.logPath, line, "utf-8");
1394
+ fs4.appendFileSync(this.logPath, line, "utf-8");
1072
1395
  }
1073
1396
  rotateIfNeeded() {
1074
1397
  try {
1075
- const stats = fs3.statSync(this.logPath);
1398
+ const stats = fs4.statSync(this.logPath);
1076
1399
  if (stats.size > MAX_LOG_SIZE) {
1077
- const content = fs3.readFileSync(this.logPath, "utf-8");
1400
+ const content = fs4.readFileSync(this.logPath, "utf-8");
1078
1401
  const halfIdx = content.indexOf("\n", Math.floor(content.length / 2));
1079
1402
  if (halfIdx > 0) {
1080
- fs3.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
1403
+ fs4.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
1081
1404
  }
1082
1405
  }
1083
1406
  } catch {
@@ -1086,33 +1409,33 @@ var DaemonLogger = class {
1086
1409
  };
1087
1410
 
1088
1411
  // src/scheduled-tasks/daemon/PidManager.ts
1089
- var fs4 = __toESM(require("fs"));
1090
- var path4 = __toESM(require("path"));
1412
+ var fs5 = __toESM(require("fs"));
1413
+ var path5 = __toESM(require("path"));
1091
1414
  var CONFIG_DIR2 = ".serviceme";
1092
1415
  var PID_FILE = "scheduler.pid";
1093
1416
  var PidManager = class {
1094
1417
  constructor(workspacePath) {
1095
- this.pidPath = path4.join(workspacePath, CONFIG_DIR2, PID_FILE);
1418
+ this.pidPath = path5.join(workspacePath, CONFIG_DIR2, PID_FILE);
1096
1419
  }
1097
1420
  getPidPath() {
1098
1421
  return this.pidPath;
1099
1422
  }
1100
1423
  writePid(pid) {
1101
- const dir = path4.dirname(this.pidPath);
1102
- if (!fs4.existsSync(dir)) {
1103
- fs4.mkdirSync(dir, { recursive: true });
1424
+ const dir = path5.dirname(this.pidPath);
1425
+ if (!fs5.existsSync(dir)) {
1426
+ fs5.mkdirSync(dir, { recursive: true });
1104
1427
  }
1105
- fs4.writeFileSync(this.pidPath, String(pid), "utf-8");
1428
+ fs5.writeFileSync(this.pidPath, String(pid), "utf-8");
1106
1429
  }
1107
1430
  readPid() {
1108
- if (!fs4.existsSync(this.pidPath)) return null;
1109
- const raw = fs4.readFileSync(this.pidPath, "utf-8").trim();
1431
+ if (!fs5.existsSync(this.pidPath)) return null;
1432
+ const raw = fs5.readFileSync(this.pidPath, "utf-8").trim();
1110
1433
  const pid = Number.parseInt(raw, 10);
1111
1434
  return Number.isNaN(pid) ? null : pid;
1112
1435
  }
1113
1436
  removePid() {
1114
- if (fs4.existsSync(this.pidPath)) {
1115
- fs4.unlinkSync(this.pidPath);
1437
+ if (fs5.existsSync(this.pidPath)) {
1438
+ fs5.unlinkSync(this.pidPath);
1116
1439
  }
1117
1440
  }
1118
1441
  isProcessRunning(pid) {
@@ -1133,12 +1456,12 @@ var PidManager = class {
1133
1456
  };
1134
1457
 
1135
1458
  // src/scheduled-tasks/daemon/SchedulerDaemon.ts
1136
- var fs9 = __toESM(require("fs"));
1459
+ var fs10 = __toESM(require("fs"));
1137
1460
  var os = __toESM(require("os"));
1138
1461
 
1139
1462
  // src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
1140
1463
  var import_node_child_process2 = require("child_process");
1141
- var fs5 = __toESM(require("fs"));
1464
+ var fs6 = __toESM(require("fs"));
1142
1465
 
1143
1466
  // src/scheduled-tasks/executors/timeout.ts
1144
1467
  function resolveConfiguredTimeoutMs(timeoutSeconds, defaultTimeoutMs) {
@@ -1173,7 +1496,7 @@ function redactArgs(args) {
1173
1496
  function writeDiagnostic(message) {
1174
1497
  const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1175
1498
  if (logPath) {
1176
- fs5.appendFileSync(logPath, message);
1499
+ fs6.appendFileSync(logPath, message);
1177
1500
  return;
1178
1501
  }
1179
1502
  process.stderr.write(message);
@@ -1383,15 +1706,15 @@ ${body}`.trim()
1383
1706
 
1384
1707
  // src/scheduled-tasks/executors/ShellExecutor.ts
1385
1708
  var import_node_child_process3 = require("child_process");
1386
- var fs6 = __toESM(require("fs"));
1387
- var path5 = __toESM(require("path"));
1709
+ var fs7 = __toESM(require("fs"));
1710
+ var path6 = __toESM(require("path"));
1388
1711
  var MAX_OUTPUT_BYTES2 = 1024 * 1024;
1389
1712
  var DEFAULT_TIMEOUT_MS4 = 6e4;
1390
1713
  var POSIX_SHELL_CANDIDATES = ["bash.exe", "sh.exe"];
1391
1714
  function resolveShellExecution(script, options = {}) {
1392
1715
  const platform = options.platform ?? process.platform;
1393
1716
  const env = options.env ?? process.env;
1394
- const fileExists = options.fileExists ?? fs6.existsSync;
1717
+ const fileExists = options.fileExists ?? fs7.existsSync;
1395
1718
  if (platform === "win32") {
1396
1719
  const posixShell = usesPosixShellSyntax(script) ? findWindowsPosixShell(env, fileExists) : null;
1397
1720
  if (posixShell) {
@@ -1436,10 +1759,10 @@ function findWindowsPosixShell(env, fileExists) {
1436
1759
  if (fileExists(candidate)) return candidate;
1437
1760
  }
1438
1761
  const pathValue = env.Path ?? env.PATH ?? "";
1439
- for (const dir of pathValue.split(path5.win32.delimiter)) {
1762
+ for (const dir of pathValue.split(path6.win32.delimiter)) {
1440
1763
  if (!dir) continue;
1441
1764
  for (const executable of POSIX_SHELL_CANDIDATES) {
1442
- const candidate = path5.win32.join(dir, executable);
1765
+ const candidate = path6.win32.join(dir, executable);
1443
1766
  if (fileExists(candidate) && !isWindowsWslLauncher(candidate)) {
1444
1767
  return candidate;
1445
1768
  }
@@ -1448,13 +1771,13 @@ function findWindowsPosixShell(env, fileExists) {
1448
1771
  return null;
1449
1772
  }
1450
1773
  function isWindowsWslLauncher(candidate) {
1451
- const normalized = path5.win32.normalize(candidate).toLowerCase();
1774
+ const normalized = path6.win32.normalize(candidate).toLowerCase();
1452
1775
  return normalized.endsWith("\\windows\\system32\\bash.exe") || normalized.endsWith("\\windows\\syswow64\\bash.exe");
1453
1776
  }
1454
1777
  function writeDiagnostic2(message) {
1455
1778
  const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1456
1779
  if (logPath) {
1457
- fs6.appendFileSync(logPath, message);
1780
+ fs7.appendFileSync(logPath, message);
1458
1781
  return;
1459
1782
  }
1460
1783
  process.stderr.write(message);
@@ -1601,8 +1924,8 @@ function getExecutor(taskType) {
1601
1924
 
1602
1925
  // src/scheduled-tasks/TaskConfigManager.ts
1603
1926
  var import_node_crypto = require("crypto");
1604
- var fs7 = __toESM(require("fs"));
1605
- var path6 = __toESM(require("path"));
1927
+ var fs8 = __toESM(require("fs"));
1928
+ var path7 = __toESM(require("path"));
1606
1929
  var import_devtools_protocol7 = require("@serviceme/devtools-protocol");
1607
1930
  var CONFIG_DIR3 = ".serviceme";
1608
1931
  var CONFIG_FILE = "scheduled-tasks.json";
@@ -1639,27 +1962,27 @@ function validateTaskPayload(taskType, payload) {
1639
1962
  }
1640
1963
  var TaskConfigManager = class {
1641
1964
  constructor(workspacePath) {
1642
- this.configPath = path6.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1965
+ this.configPath = path7.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1643
1966
  }
1644
1967
  getConfigPath() {
1645
1968
  return this.configPath;
1646
1969
  }
1647
1970
  readConfig() {
1648
- if (!fs7.existsSync(this.configPath)) {
1971
+ if (!fs8.existsSync(this.configPath)) {
1649
1972
  return emptyConfig();
1650
1973
  }
1651
- const raw = fs7.readFileSync(this.configPath, "utf-8");
1974
+ const raw = fs8.readFileSync(this.configPath, "utf-8");
1652
1975
  const parsed = JSON.parse(raw);
1653
1976
  return parsed;
1654
1977
  }
1655
1978
  writeConfig(config) {
1656
- const dir = path6.dirname(this.configPath);
1657
- if (!fs7.existsSync(dir)) {
1658
- fs7.mkdirSync(dir, { recursive: true });
1979
+ const dir = path7.dirname(this.configPath);
1980
+ if (!fs8.existsSync(dir)) {
1981
+ fs8.mkdirSync(dir, { recursive: true });
1659
1982
  }
1660
1983
  const tmp = `${this.configPath}.tmp`;
1661
- fs7.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1662
- fs7.renameSync(tmp, this.configPath);
1984
+ fs8.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1985
+ fs8.renameSync(tmp, this.configPath);
1663
1986
  }
1664
1987
  listTasks() {
1665
1988
  return this.readConfig().tasks;
@@ -1930,8 +2253,8 @@ var TaskExecutionEngine = class {
1930
2253
 
1931
2254
  // src/scheduled-tasks/TaskLogManager.ts
1932
2255
  var import_node_crypto2 = require("crypto");
1933
- var fs8 = __toESM(require("fs"));
1934
- var path7 = __toESM(require("path"));
2256
+ var fs9 = __toESM(require("fs"));
2257
+ var path8 = __toESM(require("path"));
1935
2258
  var CONFIG_DIR4 = ".serviceme";
1936
2259
  var LOG_FILE2 = "scheduled-tasks-log.json";
1937
2260
  var MAX_LOGS = 200;
@@ -1956,17 +2279,17 @@ function validateAndRepairLogFile(raw) {
1956
2279
  }
1957
2280
  var TaskLogManager = class {
1958
2281
  constructor(workspacePath) {
1959
- this.logPath = path7.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
2282
+ this.logPath = path8.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1960
2283
  }
1961
2284
  getLogPath() {
1962
2285
  return this.logPath;
1963
2286
  }
1964
2287
  readLogFile() {
1965
- if (!fs8.existsSync(this.logPath)) {
2288
+ if (!fs9.existsSync(this.logPath)) {
1966
2289
  return emptyLogFile();
1967
2290
  }
1968
2291
  try {
1969
- const raw = fs8.readFileSync(this.logPath, "utf-8");
2292
+ const raw = fs9.readFileSync(this.logPath, "utf-8");
1970
2293
  const parsed = JSON.parse(raw);
1971
2294
  const file = validateAndRepairLogFile(parsed);
1972
2295
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
@@ -1982,21 +2305,21 @@ var TaskLogManager = class {
1982
2305
  }
1983
2306
  backupCorruptedFile() {
1984
2307
  try {
1985
- if (fs8.existsSync(this.logPath)) {
2308
+ if (fs9.existsSync(this.logPath)) {
1986
2309
  const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
1987
- fs8.copyFileSync(this.logPath, backupPath);
2310
+ fs9.copyFileSync(this.logPath, backupPath);
1988
2311
  }
1989
2312
  } catch {
1990
2313
  }
1991
2314
  }
1992
2315
  writeLogFile(file) {
1993
- const dir = path7.dirname(this.logPath);
1994
- if (!fs8.existsSync(dir)) {
1995
- fs8.mkdirSync(dir, { recursive: true });
2316
+ const dir = path8.dirname(this.logPath);
2317
+ if (!fs9.existsSync(dir)) {
2318
+ fs9.mkdirSync(dir, { recursive: true });
1996
2319
  }
1997
2320
  const tmp = `${this.logPath}.tmp`;
1998
- fs8.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1999
- fs8.renameSync(tmp, this.logPath);
2321
+ fs9.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
2322
+ fs9.renameSync(tmp, this.logPath);
2000
2323
  }
2001
2324
  appendLog(input) {
2002
2325
  const file = this.readLogFile();
@@ -2107,8 +2430,8 @@ var SchedulerDaemon = class {
2107
2430
  const configPath = this.configManager.getConfigPath();
2108
2431
  const dir = configPath.substring(0, configPath.lastIndexOf("/"));
2109
2432
  try {
2110
- if (fs9.existsSync(dir)) {
2111
- this.watcher = fs9.watch(dir, (_eventType, filename) => {
2433
+ if (fs10.existsSync(dir)) {
2434
+ this.watcher = fs10.watch(dir, (_eventType, filename) => {
2112
2435
  if (filename === "scheduled-tasks.json") {
2113
2436
  this.logger.log("info", "Config file changed, reconciling...");
2114
2437
  }
@@ -2260,8 +2583,174 @@ function matchCronField(field, value) {
2260
2583
  const values = field.split(",");
2261
2584
  return values.some((v) => Number.parseInt(v, 10) === value);
2262
2585
  }
2586
+
2587
+ // src/skills/SkillCatalogClient.ts
2588
+ var SkillCatalogClient = class {
2589
+ constructor(options = {}) {
2590
+ this.fetchImpl = options.fetchImpl ?? fetch;
2591
+ this.baseUrl = options.baseUrl;
2592
+ }
2593
+ async getCatalog() {
2594
+ if (!this.baseUrl) {
2595
+ return {
2596
+ skills: [],
2597
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2598
+ };
2599
+ }
2600
+ const response = await this.fetchImpl(
2601
+ `${this.baseUrl}/api/v1/marketplace/skills`
2602
+ );
2603
+ if (!response.ok) {
2604
+ throw new Error(`Failed to fetch skills catalog: ${response.status}`);
2605
+ }
2606
+ const data = await response.json();
2607
+ return {
2608
+ skills: data.skills ?? [],
2609
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2610
+ };
2611
+ }
2612
+ };
2613
+
2614
+ // src/skills/SkillReconciler.ts
2615
+ var SkillReconciler = class {
2616
+ constructor(deps) {
2617
+ this.deps = deps;
2618
+ }
2619
+ async mutate(request) {
2620
+ if (request.targetScope !== "workspace" && request.targetScope !== "user") {
2621
+ throw new Error(`Invalid target scope: ${String(request.targetScope)}`);
2622
+ }
2623
+ if (request.action === "uninstall" || request.action === "move" || request.action === "removeExternal") {
2624
+ return {
2625
+ status: "success",
2626
+ changed: true,
2627
+ message: `Skill ${request.action} completed.`
2628
+ };
2629
+ }
2630
+ if (request.action !== "install") {
2631
+ return {
2632
+ status: "blocked",
2633
+ changed: false,
2634
+ message: `Skill action is not supported by bridge reconciler: ${request.action}`
2635
+ };
2636
+ }
2637
+ const catalog = await this.deps.catalogClient.getCatalog();
2638
+ const remoteSkill = catalog.skills.find(
2639
+ (skill) => this.deps.skillStore.normalizeRemoteSkillId(skill.id) === request.skillId
2640
+ );
2641
+ if (!remoteSkill) {
2642
+ return {
2643
+ status: "blocked",
2644
+ changed: false,
2645
+ message: "Skill not found in catalog."
2646
+ };
2647
+ }
2648
+ if (!request.confirmed && (remoteSkill.hasScripts || remoteSkill.hasHooks)) {
2649
+ return {
2650
+ status: "requires_confirmation",
2651
+ changed: false,
2652
+ hasScripts: Boolean(remoteSkill.hasScripts),
2653
+ hasHooks: Boolean(remoteSkill.hasHooks),
2654
+ message: "This skill contains executable scripts that require confirmation."
2655
+ };
2656
+ }
2657
+ return {
2658
+ status: "success",
2659
+ changed: true,
2660
+ message: "Skill installed."
2661
+ };
2662
+ }
2663
+ };
2664
+
2665
+ // src/skills/SkillStore.ts
2666
+ var fs11 = __toESM(require("fs/promises"));
2667
+ var path9 = __toESM(require("path"));
2668
+ var USER_SKILL_MARKER_FILE = ".ms-devtools-skill.json";
2669
+ var WORKSPACE_SKILLS_ROOT_RELATIVE = ".github/skills";
2670
+ var WORKSPACE_SKILLS_MARKER_RELATIVE = ".github/.ms-devtools-skills.yml";
2671
+ var SAFE_LOCAL_ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
2672
+ function assertSafeLocalSkillId(skillId) {
2673
+ if (typeof skillId !== "string" || skillId.length === 0 || skillId === "." || skillId === ".." || skillId.includes("/") || skillId.includes("\\") || !SAFE_LOCAL_ID_PATTERN2.test(skillId)) {
2674
+ throw new Error(`Invalid skill id: ${skillId}`);
2675
+ }
2676
+ return skillId;
2677
+ }
2678
+ var SkillStore = class {
2679
+ constructor(options) {
2680
+ this.workspacePath = options.workspacePath;
2681
+ this.userSkillsRoot = options.userSkillsRoot;
2682
+ this.fileSystem = options.fileSystem ?? fs11;
2683
+ }
2684
+ normalizeRemoteSkillId(remoteId) {
2685
+ if (remoteId.startsWith("official/")) {
2686
+ return assertSafeLocalSkillId(remoteId.slice("official/".length));
2687
+ }
2688
+ if (remoteId.startsWith("community/")) {
2689
+ const lastSlash = remoteId.lastIndexOf("/");
2690
+ return assertSafeLocalSkillId(remoteId.slice(lastSlash + 1));
2691
+ }
2692
+ return assertSafeLocalSkillId(remoteId);
2693
+ }
2694
+ getWorkspaceSkillPath(skillId) {
2695
+ return `${WORKSPACE_SKILLS_ROOT_RELATIVE}/${skillId}`;
2696
+ }
2697
+ getWorkspaceMarkerPath() {
2698
+ return WORKSPACE_SKILLS_MARKER_RELATIVE;
2699
+ }
2700
+ getUserSkillPath(skillId) {
2701
+ return path9.join(this.userSkillsRoot, skillId);
2702
+ }
2703
+ async listWorkspaceSkillIds() {
2704
+ const skillsRootPath = path9.join(
2705
+ this.workspacePath,
2706
+ WORKSPACE_SKILLS_ROOT_RELATIVE
2707
+ );
2708
+ try {
2709
+ const entries = await this.fileSystem.readdir(skillsRootPath, {
2710
+ withFileTypes: true
2711
+ });
2712
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
2713
+ } catch {
2714
+ return [];
2715
+ }
2716
+ }
2717
+ async listUserSkillIds() {
2718
+ try {
2719
+ const entries = await this.fileSystem.readdir(this.userSkillsRoot, {
2720
+ withFileTypes: true
2721
+ });
2722
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
2723
+ } catch {
2724
+ return [];
2725
+ }
2726
+ }
2727
+ async writeManagedUserSkillMarker(skillId) {
2728
+ const targetDir = this.getUserSkillPath(skillId);
2729
+ await this.fileSystem.mkdir(targetDir, { recursive: true });
2730
+ await this.fileSystem.writeFile(
2731
+ path9.join(targetDir, USER_SKILL_MARKER_FILE),
2732
+ JSON.stringify({ skillId, installedBy: "ms-devtools" }, null, 2),
2733
+ "utf-8"
2734
+ );
2735
+ }
2736
+ async isManagedUserSkill(skillId) {
2737
+ try {
2738
+ const marker = await this.fileSystem.readFile(
2739
+ path9.join(this.getUserSkillPath(skillId), USER_SKILL_MARKER_FILE),
2740
+ "utf-8"
2741
+ );
2742
+ const parsed = JSON.parse(marker);
2743
+ return parsed.skillId === skillId;
2744
+ } catch {
2745
+ return false;
2746
+ }
2747
+ }
2748
+ };
2263
2749
  // Annotate the CommonJS export names for ESM import in node:
2264
2750
  0 && (module.exports = {
2751
+ AgentCatalogClient,
2752
+ AgentReconciler,
2753
+ AgentStore,
2265
2754
  DaemonLogger,
2266
2755
  EnvironmentInspector,
2267
2756
  GithubCopilotCliExecutor,
@@ -2271,6 +2760,10 @@ function matchCronField(field, value) {
2271
2760
  ProjectTools,
2272
2761
  SchedulerDaemon,
2273
2762
  ShellExecutor,
2763
+ SkillCatalogClient,
2764
+ SkillReconciler,
2765
+ SkillStore,
2766
+ TOOL_RISK_MAP,
2274
2767
  TaskConfigManager,
2275
2768
  TaskExecutionEngine,
2276
2769
  TaskLogManager,
@@ -2288,6 +2781,7 @@ function matchCronField(field, value) {
2288
2781
  matchesCron,
2289
2782
  moveFiles,
2290
2783
  noopLogger,
2784
+ parseAgentToolPermissions,
2291
2785
  parseIntervalMs,
2292
2786
  resolveTaskExecutionPayload,
2293
2787
  unzipFile,