@serviceme/devtools-core 0.1.4 → 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
 
@@ -293,8 +582,20 @@ async function copilotPrompt(options) {
293
582
 
294
583
  // src/env/environmentInspector.ts
295
584
  var import_devtools_protocol3 = require("@serviceme/devtools-protocol");
296
- var TOOL_CHECK_TIMEOUT_MS = 5e3;
585
+ var DEFAULT_TOOL_CHECK_TIMEOUT_MS = 5e3;
586
+ var TOOL_CHECK_TIMEOUT_MS = {
587
+ nuget: 12e3,
588
+ nvm: 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
596
+ };
297
597
  var ERROR_CODE_NOT_FOUND = 127;
598
+ var ERROR_CODE_TIMEOUT = "ETIMEDOUT";
298
599
  var EnvironmentInspector = class {
299
600
  async checkEnvironment() {
300
601
  const results = await Promise.all(
@@ -331,19 +632,39 @@ var EnvironmentInspector = class {
331
632
  const isWindows = process.platform === "win32";
332
633
  const result = await runCommand(isWindows ? "where" : "which", {
333
634
  args: [toolName],
334
- timeoutMs: TOOL_CHECK_TIMEOUT_MS
635
+ timeoutMs: this.getToolTimeout(toolName)
335
636
  });
336
- return result.stdout.trim().split("\n")[0];
637
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
638
+ } catch {
639
+ return void 0;
640
+ }
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];
337
659
  } catch {
338
660
  return void 0;
339
661
  }
340
662
  }
341
663
  async getToolVersion(toolName) {
342
- const command = this.getVersionCommand(toolName);
343
- const isWindows = process.platform === "win32";
344
- const result = await runCommand(isWindows ? "cmd.exe" : "/bin/bash", {
345
- args: isWindows ? ["/c", command] : ["-lc", command],
346
- timeoutMs: TOOL_CHECK_TIMEOUT_MS
664
+ const invocation = await this.getVersionInvocation(toolName);
665
+ const result = await runCommand(invocation.command, {
666
+ args: invocation.args,
667
+ timeoutMs: this.getToolTimeout(toolName)
347
668
  });
348
669
  return this.parseVersion(toolName, result.stdout || result.stderr);
349
670
  }
@@ -352,7 +673,7 @@ var EnvironmentInspector = class {
352
673
  try {
353
674
  const result = await runCommand("cmd.exe", {
354
675
  args: ["/c", "nvm version"],
355
- timeoutMs: TOOL_CHECK_TIMEOUT_MS
676
+ timeoutMs: this.getToolTimeout("nvm")
356
677
  });
357
678
  return {
358
679
  installed: true,
@@ -372,7 +693,7 @@ var EnvironmentInspector = class {
372
693
  "-lc",
373
694
  'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; nvm --version'
374
695
  ],
375
- timeoutMs: TOOL_CHECK_TIMEOUT_MS
696
+ timeoutMs: this.getToolTimeout("nvm")
376
697
  });
377
698
  return {
378
699
  installed: true,
@@ -397,7 +718,7 @@ var EnvironmentInspector = class {
397
718
  try {
398
719
  const result = await runCommand("dotnet", {
399
720
  args: ["nuget", "list", "source"],
400
- timeoutMs: TOOL_CHECK_TIMEOUT_MS
721
+ timeoutMs: this.getToolTimeout("nuget")
401
722
  });
402
723
  const privateSourceUrl = "http://192.168.20.209:10010/nuget";
403
724
  if (result.stdout.includes(privateSourceUrl)) {
@@ -415,17 +736,35 @@ var EnvironmentInspector = class {
415
736
  return this.handleToolCheckError(error);
416
737
  }
417
738
  }
418
- getVersionCommand(toolName) {
739
+ async getVersionInvocation(toolName) {
740
+ const isWindows = process.platform === "win32";
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
+ }
749
+ return {
750
+ command: "cmd.exe",
751
+ args: ["/c", toolName, "--version"]
752
+ };
753
+ }
419
754
  const commands = {
420
- git: "git --version",
421
- node: "node --version",
422
- npm: "npm --version",
423
- pnpm: "pnpm --version",
424
- nvm: "nvm --version",
425
- nrm: "nrm --version",
426
- dotnet: "dotnet --version"
755
+ git: { command: "git", args: ["--version"] },
756
+ node: { command: "node", args: ["--version"] },
757
+ npm: { command: "npm", args: ["--version"] },
758
+ pnpm: { command: "pnpm", args: ["--version"] },
759
+ nvm: { command: "nvm", args: ["--version"] },
760
+ nrm: { command: "nrm", args: ["--version"] },
761
+ rtk: { command: "rtk", args: ["--version"] },
762
+ dotnet: { command: "dotnet", args: ["--version"] }
427
763
  };
428
- return commands[toolName] || `${toolName} --version`;
764
+ return commands[toolName] || { command: toolName, args: ["--version"] };
765
+ }
766
+ getToolTimeout(toolName) {
767
+ return TOOL_CHECK_TIMEOUT_MS[toolName] ?? DEFAULT_TOOL_CHECK_TIMEOUT_MS;
429
768
  }
430
769
  parseVersion(toolName, output) {
431
770
  const cleaned = output.trim();
@@ -452,17 +791,18 @@ var EnvironmentInspector = class {
452
791
  const execError = error;
453
792
  const message = execError.message || String(error);
454
793
  const code = execError.code;
455
- const isNotFound = message.includes("command not found") || message.includes("not recognized") || code === ERROR_CODE_NOT_FOUND;
794
+ const isNotFound = message.includes("command not found") || message.includes("not recognized") || message.includes("ENOENT") || code === "ENOENT" || code === ERROR_CODE_NOT_FOUND;
795
+ const isTimeout = code === ERROR_CODE_TIMEOUT || message.toLowerCase().includes("timed out");
456
796
  return {
457
797
  installed: false,
458
- error: isNotFound ? "Not installed" : message
798
+ error: isNotFound ? "Not installed" : isTimeout ? "Check timed out" : message
459
799
  };
460
800
  }
461
801
  };
462
802
 
463
803
  // src/image/imageTools.ts
464
- var fs = __toESM(require("fs/promises"));
465
- var path = __toESM(require("path"));
804
+ var fs2 = __toESM(require("fs/promises"));
805
+ var path2 = __toESM(require("path"));
466
806
  var import_devtools_protocol4 = require("@serviceme/devtools-protocol");
467
807
  var SUPPORTED_FORMATS = [
468
808
  ".jpg",
@@ -483,7 +823,7 @@ var ImageTools = class {
483
823
  if (!await this.pathExists(filePath)) {
484
824
  return { valid: false };
485
825
  }
486
- if (!SUPPORTED_FORMATS.includes(path.extname(filePath).toLowerCase())) {
826
+ if (!SUPPORTED_FORMATS.includes(path2.extname(filePath).toLowerCase())) {
487
827
  return { valid: false };
488
828
  }
489
829
  await this.getInfo(filePath, sharpModulePath);
@@ -495,7 +835,7 @@ var ImageTools = class {
495
835
  async getInfo(imagePath, sharpModulePath) {
496
836
  const sharp = this.loadSharp(sharpModulePath);
497
837
  const metadata = await sharp(imagePath).metadata();
498
- const stats = await fs.stat(imagePath);
838
+ const stats = await fs2.stat(imagePath);
499
839
  return {
500
840
  width: metadata.width ?? 0,
501
841
  height: metadata.height ?? 0,
@@ -512,7 +852,7 @@ var ImageTools = class {
512
852
  `Invalid image file: ${imagePath}`
513
853
  );
514
854
  }
515
- const originalStats = await fs.stat(imagePath);
855
+ const originalStats = await fs2.stat(imagePath);
516
856
  const originalSize = originalStats.size;
517
857
  const outputPath = this.getOutputPath(imagePath, options);
518
858
  const compressedBuffer = await this.compressWithSharp(imagePath, options);
@@ -527,7 +867,7 @@ var ImageTools = class {
527
867
  outputPath: imagePath
528
868
  };
529
869
  }
530
- await fs.writeFile(outputPath, compressedBuffer);
870
+ await fs2.writeFile(outputPath, compressedBuffer);
531
871
  return {
532
872
  originalSize,
533
873
  compressedSize,
@@ -538,7 +878,7 @@ var ImageTools = class {
538
878
  async compressWithSharp(imagePath, options) {
539
879
  const sharp = this.loadSharp(options.sharpModulePath);
540
880
  let pipeline = sharp(imagePath);
541
- switch (options.format ?? path.extname(imagePath).toLowerCase().slice(1)) {
881
+ switch (options.format ?? path2.extname(imagePath).toLowerCase().slice(1)) {
542
882
  case "jpg":
543
883
  case "jpeg":
544
884
  pipeline = pipeline.jpeg({ quality: options.quality });
@@ -564,14 +904,14 @@ var ImageTools = class {
564
904
  if (options.replaceOriginImage) {
565
905
  return inputPath;
566
906
  }
567
- const dir = path.dirname(inputPath);
568
- const ext = path.extname(inputPath);
569
- const name = path.basename(inputPath, ext);
570
- 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}`);
571
911
  }
572
912
  async pathExists(targetPath) {
573
913
  try {
574
- await fs.access(targetPath);
914
+ await fs2.access(targetPath);
575
915
  return true;
576
916
  } catch {
577
917
  return false;
@@ -727,8 +1067,8 @@ function createConsoleLogger(prefix = "serviceme") {
727
1067
  }
728
1068
 
729
1069
  // src/project/projectTools.ts
730
- var fs2 = __toESM(require("fs/promises"));
731
- var path2 = __toESM(require("path"));
1070
+ var fs3 = __toESM(require("fs/promises"));
1071
+ var path3 = __toESM(require("path"));
732
1072
  var import_devtools_protocol6 = require("@serviceme/devtools-protocol");
733
1073
 
734
1074
  // src/utils/fileUtils.ts
@@ -849,10 +1189,10 @@ var moveFiles = async (sourceDir, destDir, overwrite = false) => {
849
1189
  var ProjectTools = class {
850
1190
  async extractTemplate(zipPath, workspacePath, tempExtractDir, input) {
851
1191
  await unzipFile(zipPath, tempExtractDir);
852
- let sourceDir = path2.join(tempExtractDir, input.extractedDirName);
1192
+ let sourceDir = path3.join(tempExtractDir, input.extractedDirName);
853
1193
  let actualDirName = input.extractedDirName;
854
1194
  if (!await this.pathExists(sourceDir)) {
855
- const entries = await fs2.readdir(tempExtractDir, { withFileTypes: true });
1195
+ const entries = await fs3.readdir(tempExtractDir, { withFileTypes: true });
856
1196
  const directories = entries.filter(
857
1197
  (entry) => entry.isDirectory() && !entry.name.startsWith(".")
858
1198
  );
@@ -864,7 +1204,7 @@ var ProjectTools = class {
864
1204
  );
865
1205
  if (selectedDirectory) {
866
1206
  actualDirName = selectedDirectory;
867
- sourceDir = path2.join(tempExtractDir, actualDirName);
1207
+ sourceDir = path3.join(tempExtractDir, actualDirName);
868
1208
  } else if (directories.length === 0) {
869
1209
  throw new Error(
870
1210
  `No directory found after extraction. Expected directory: ${input.extractedDirName}`
@@ -922,7 +1262,7 @@ var ProjectTools = class {
922
1262
  } else {
923
1263
  for (const scriptPath of scripts) {
924
1264
  try {
925
- await fs2.chmod(scriptPath, 493);
1265
+ await fs3.chmod(scriptPath, 493);
926
1266
  updatedCount += 1;
927
1267
  } catch {
928
1268
  }
@@ -970,12 +1310,12 @@ var ProjectTools = class {
970
1310
  const results = [];
971
1311
  let entries;
972
1312
  try {
973
- entries = await fs2.readdir(dir, { withFileTypes: true });
1313
+ entries = await fs3.readdir(dir, { withFileTypes: true });
974
1314
  } catch {
975
1315
  return results;
976
1316
  }
977
1317
  for (const entry of entries) {
978
- const fullPath = path2.join(dir, entry.name);
1318
+ const fullPath = path3.join(dir, entry.name);
979
1319
  if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
980
1320
  results.push(...await this.findScripts(fullPath, extensions));
981
1321
  } else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
@@ -986,7 +1326,7 @@ var ProjectTools = class {
986
1326
  }
987
1327
  async pathExists(targetPath) {
988
1328
  try {
989
- await fs2.access(targetPath);
1329
+ await fs3.access(targetPath);
990
1330
  return true;
991
1331
  } catch {
992
1332
  return false;
@@ -1005,7 +1345,7 @@ var ProjectTools = class {
1005
1345
  const matches = [];
1006
1346
  for (const directoryName of directoryNames) {
1007
1347
  if (await this.directoryMatchesProjectPattern(
1008
- path2.join(tempExtractDir, directoryName),
1348
+ path3.join(tempExtractDir, directoryName),
1009
1349
  projectFilePattern
1010
1350
  )) {
1011
1351
  matches.push(directoryName);
@@ -1017,7 +1357,7 @@ var ProjectTools = class {
1017
1357
  return null;
1018
1358
  }
1019
1359
  async directoryMatchesProjectPattern(directoryPath, projectFilePattern) {
1020
- const entries = await fs2.readdir(directoryPath);
1360
+ const entries = await fs3.readdir(directoryPath);
1021
1361
  if (projectFilePattern.includes("*")) {
1022
1362
  const regex = new RegExp(`^${projectFilePattern.replace("*", ".*")}$`);
1023
1363
  return entries.some((entry) => regex.test(entry));
@@ -1030,17 +1370,17 @@ function createProjectTools() {
1030
1370
  }
1031
1371
 
1032
1372
  // src/scheduled-tasks/daemon/DaemonLogger.ts
1033
- var fs3 = __toESM(require("fs"));
1034
- var path3 = __toESM(require("path"));
1373
+ var fs4 = __toESM(require("fs"));
1374
+ var path4 = __toESM(require("path"));
1035
1375
  var CONFIG_DIR = ".serviceme";
1036
1376
  var LOG_FILE = "scheduler.log";
1037
1377
  var MAX_LOG_SIZE = 1024 * 1024;
1038
1378
  var DaemonLogger = class {
1039
1379
  constructor(workspacePath) {
1040
- this.logPath = path3.join(workspacePath, CONFIG_DIR, LOG_FILE);
1041
- const dir = path3.dirname(this.logPath);
1042
- if (!fs3.existsSync(dir)) {
1043
- 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 });
1044
1384
  }
1045
1385
  }
1046
1386
  getLogPath() {
@@ -1051,16 +1391,16 @@ var DaemonLogger = class {
1051
1391
  const line = `[${ts}] [${level.toUpperCase()}] ${message}
1052
1392
  `;
1053
1393
  this.rotateIfNeeded();
1054
- fs3.appendFileSync(this.logPath, line, "utf-8");
1394
+ fs4.appendFileSync(this.logPath, line, "utf-8");
1055
1395
  }
1056
1396
  rotateIfNeeded() {
1057
1397
  try {
1058
- const stats = fs3.statSync(this.logPath);
1398
+ const stats = fs4.statSync(this.logPath);
1059
1399
  if (stats.size > MAX_LOG_SIZE) {
1060
- const content = fs3.readFileSync(this.logPath, "utf-8");
1400
+ const content = fs4.readFileSync(this.logPath, "utf-8");
1061
1401
  const halfIdx = content.indexOf("\n", Math.floor(content.length / 2));
1062
1402
  if (halfIdx > 0) {
1063
- fs3.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
1403
+ fs4.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
1064
1404
  }
1065
1405
  }
1066
1406
  } catch {
@@ -1069,33 +1409,33 @@ var DaemonLogger = class {
1069
1409
  };
1070
1410
 
1071
1411
  // src/scheduled-tasks/daemon/PidManager.ts
1072
- var fs4 = __toESM(require("fs"));
1073
- var path4 = __toESM(require("path"));
1412
+ var fs5 = __toESM(require("fs"));
1413
+ var path5 = __toESM(require("path"));
1074
1414
  var CONFIG_DIR2 = ".serviceme";
1075
1415
  var PID_FILE = "scheduler.pid";
1076
1416
  var PidManager = class {
1077
1417
  constructor(workspacePath) {
1078
- this.pidPath = path4.join(workspacePath, CONFIG_DIR2, PID_FILE);
1418
+ this.pidPath = path5.join(workspacePath, CONFIG_DIR2, PID_FILE);
1079
1419
  }
1080
1420
  getPidPath() {
1081
1421
  return this.pidPath;
1082
1422
  }
1083
1423
  writePid(pid) {
1084
- const dir = path4.dirname(this.pidPath);
1085
- if (!fs4.existsSync(dir)) {
1086
- fs4.mkdirSync(dir, { recursive: true });
1424
+ const dir = path5.dirname(this.pidPath);
1425
+ if (!fs5.existsSync(dir)) {
1426
+ fs5.mkdirSync(dir, { recursive: true });
1087
1427
  }
1088
- fs4.writeFileSync(this.pidPath, String(pid), "utf-8");
1428
+ fs5.writeFileSync(this.pidPath, String(pid), "utf-8");
1089
1429
  }
1090
1430
  readPid() {
1091
- if (!fs4.existsSync(this.pidPath)) return null;
1092
- 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();
1093
1433
  const pid = Number.parseInt(raw, 10);
1094
1434
  return Number.isNaN(pid) ? null : pid;
1095
1435
  }
1096
1436
  removePid() {
1097
- if (fs4.existsSync(this.pidPath)) {
1098
- fs4.unlinkSync(this.pidPath);
1437
+ if (fs5.existsSync(this.pidPath)) {
1438
+ fs5.unlinkSync(this.pidPath);
1099
1439
  }
1100
1440
  }
1101
1441
  isProcessRunning(pid) {
@@ -1116,12 +1456,12 @@ var PidManager = class {
1116
1456
  };
1117
1457
 
1118
1458
  // src/scheduled-tasks/daemon/SchedulerDaemon.ts
1119
- var fs9 = __toESM(require("fs"));
1459
+ var fs10 = __toESM(require("fs"));
1120
1460
  var os = __toESM(require("os"));
1121
1461
 
1122
1462
  // src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
1123
1463
  var import_node_child_process2 = require("child_process");
1124
- var fs5 = __toESM(require("fs"));
1464
+ var fs6 = __toESM(require("fs"));
1125
1465
 
1126
1466
  // src/scheduled-tasks/executors/timeout.ts
1127
1467
  function resolveConfiguredTimeoutMs(timeoutSeconds, defaultTimeoutMs) {
@@ -1156,11 +1496,35 @@ function redactArgs(args) {
1156
1496
  function writeDiagnostic(message) {
1157
1497
  const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1158
1498
  if (logPath) {
1159
- fs5.appendFileSync(logPath, message);
1499
+ fs6.appendFileSync(logPath, message);
1160
1500
  return;
1161
1501
  }
1162
1502
  process.stderr.write(message);
1163
1503
  }
1504
+ function resolveGithubCopilotCliExecution(payload) {
1505
+ const args = ["copilot", "prompt", "--prompt", payload.prompt];
1506
+ if (payload.autopilot) {
1507
+ args.push("--autopilot");
1508
+ }
1509
+ if (payload.allowTools && payload.allowTools.length > 0) {
1510
+ args.push("--allow-tools", payload.allowTools.join(","));
1511
+ }
1512
+ if (payload.model) {
1513
+ args.push("--model", payload.model);
1514
+ }
1515
+ if (payload.agent) {
1516
+ args.push("--agent", payload.agent);
1517
+ }
1518
+ args.push("--timeout", String(payload.timeout ?? 0));
1519
+ return {
1520
+ command: "serviceme",
1521
+ args,
1522
+ cwd: payload.workspace,
1523
+ timeoutMs: resolveConfiguredTimeoutMs(payload.timeout, DEFAULT_TIMEOUT_MS2),
1524
+ diagnosticArgs: redactArgs(args),
1525
+ promptLen: payload.prompt.length
1526
+ };
1527
+ }
1164
1528
  var GithubCopilotCliExecutor = class {
1165
1529
  async execute(payload, abortSignal) {
1166
1530
  const authenticated = await isCopilotAuthenticated();
@@ -1183,7 +1547,7 @@ var GithubCopilotCliExecutor = class {
1183
1547
  }
1184
1548
  executeStreaming(payload, onOutput, abortSignal) {
1185
1549
  const p = payload;
1186
- const timeoutMs = resolveConfiguredTimeoutMs(p.timeout, DEFAULT_TIMEOUT_MS2);
1550
+ const execution = resolveGithubCopilotCliExecution(p);
1187
1551
  let resolve;
1188
1552
  const resultPromise = new Promise((r) => {
1189
1553
  resolve = r;
@@ -1205,26 +1569,12 @@ var GithubCopilotCliExecutor = class {
1205
1569
  }
1206
1570
  };
1207
1571
  }
1208
- const args = ["copilot", "prompt", "--prompt", p.prompt];
1209
- if (p.autopilot) {
1210
- args.push("--autopilot");
1211
- }
1212
- if (p.allowTools && p.allowTools.length > 0) {
1213
- args.push("--allow-tools", p.allowTools.join(","));
1214
- }
1215
- if (p.model) {
1216
- args.push("--model", p.model);
1217
- }
1218
- if (p.agent) {
1219
- args.push("--agent", p.agent);
1220
- }
1221
- args.push("--timeout", String(p.timeout ?? 0));
1222
1572
  writeDiagnostic(
1223
- `[GithubCopilotCliExecutor] spawn: command=serviceme, args=${JSON.stringify(redactArgs(args))}, promptLen=${p.prompt.length}, cwd=${p.workspace ?? "(default)"}, platform=${process.platform}, windowsHide=true, pid=${process.pid}
1573
+ `[GithubCopilotCliExecutor] spawn: command=${execution.command}, args=${JSON.stringify(execution.diagnosticArgs)}, promptLen=${execution.promptLen}, cwd=${execution.cwd ?? "(default)"}, platform=${process.platform}, windowsHide=true, pid=${process.pid}
1224
1574
  `
1225
1575
  );
1226
- const child = (0, import_node_child_process2.spawn)("serviceme", args, {
1227
- cwd: p.workspace,
1576
+ const child = (0, import_node_child_process2.spawn)(execution.command, execution.args, {
1577
+ cwd: execution.cwd,
1228
1578
  stdio: ["ignore", "pipe", "pipe"],
1229
1579
  windowsHide: true
1230
1580
  });
@@ -1234,6 +1584,7 @@ var GithubCopilotCliExecutor = class {
1234
1584
  `
1235
1585
  );
1236
1586
  }
1587
+ const timeoutMs = execution.timeoutMs;
1237
1588
  const timer = timeoutMs != null ? setTimeout(() => {
1238
1589
  child.kill("SIGTERM");
1239
1590
  setTimeout(() => {
@@ -1355,15 +1706,15 @@ ${body}`.trim()
1355
1706
 
1356
1707
  // src/scheduled-tasks/executors/ShellExecutor.ts
1357
1708
  var import_node_child_process3 = require("child_process");
1358
- var fs6 = __toESM(require("fs"));
1359
- var path5 = __toESM(require("path"));
1709
+ var fs7 = __toESM(require("fs"));
1710
+ var path6 = __toESM(require("path"));
1360
1711
  var MAX_OUTPUT_BYTES2 = 1024 * 1024;
1361
1712
  var DEFAULT_TIMEOUT_MS4 = 6e4;
1362
1713
  var POSIX_SHELL_CANDIDATES = ["bash.exe", "sh.exe"];
1363
1714
  function resolveShellExecution(script, options = {}) {
1364
1715
  const platform = options.platform ?? process.platform;
1365
1716
  const env = options.env ?? process.env;
1366
- const fileExists = options.fileExists ?? fs6.existsSync;
1717
+ const fileExists = options.fileExists ?? fs7.existsSync;
1367
1718
  if (platform === "win32") {
1368
1719
  const posixShell = usesPosixShellSyntax(script) ? findWindowsPosixShell(env, fileExists) : null;
1369
1720
  if (posixShell) {
@@ -1408,10 +1759,10 @@ function findWindowsPosixShell(env, fileExists) {
1408
1759
  if (fileExists(candidate)) return candidate;
1409
1760
  }
1410
1761
  const pathValue = env.Path ?? env.PATH ?? "";
1411
- for (const dir of pathValue.split(path5.win32.delimiter)) {
1762
+ for (const dir of pathValue.split(path6.win32.delimiter)) {
1412
1763
  if (!dir) continue;
1413
1764
  for (const executable of POSIX_SHELL_CANDIDATES) {
1414
- const candidate = path5.win32.join(dir, executable);
1765
+ const candidate = path6.win32.join(dir, executable);
1415
1766
  if (fileExists(candidate) && !isWindowsWslLauncher(candidate)) {
1416
1767
  return candidate;
1417
1768
  }
@@ -1420,13 +1771,13 @@ function findWindowsPosixShell(env, fileExists) {
1420
1771
  return null;
1421
1772
  }
1422
1773
  function isWindowsWslLauncher(candidate) {
1423
- const normalized = path5.win32.normalize(candidate).toLowerCase();
1774
+ const normalized = path6.win32.normalize(candidate).toLowerCase();
1424
1775
  return normalized.endsWith("\\windows\\system32\\bash.exe") || normalized.endsWith("\\windows\\syswow64\\bash.exe");
1425
1776
  }
1426
1777
  function writeDiagnostic2(message) {
1427
1778
  const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1428
1779
  if (logPath) {
1429
- fs6.appendFileSync(logPath, message);
1780
+ fs7.appendFileSync(logPath, message);
1430
1781
  return;
1431
1782
  }
1432
1783
  process.stderr.write(message);
@@ -1573,8 +1924,8 @@ function getExecutor(taskType) {
1573
1924
 
1574
1925
  // src/scheduled-tasks/TaskConfigManager.ts
1575
1926
  var import_node_crypto = require("crypto");
1576
- var fs7 = __toESM(require("fs"));
1577
- var path6 = __toESM(require("path"));
1927
+ var fs8 = __toESM(require("fs"));
1928
+ var path7 = __toESM(require("path"));
1578
1929
  var import_devtools_protocol7 = require("@serviceme/devtools-protocol");
1579
1930
  var CONFIG_DIR3 = ".serviceme";
1580
1931
  var CONFIG_FILE = "scheduled-tasks.json";
@@ -1611,27 +1962,27 @@ function validateTaskPayload(taskType, payload) {
1611
1962
  }
1612
1963
  var TaskConfigManager = class {
1613
1964
  constructor(workspacePath) {
1614
- this.configPath = path6.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1965
+ this.configPath = path7.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1615
1966
  }
1616
1967
  getConfigPath() {
1617
1968
  return this.configPath;
1618
1969
  }
1619
1970
  readConfig() {
1620
- if (!fs7.existsSync(this.configPath)) {
1971
+ if (!fs8.existsSync(this.configPath)) {
1621
1972
  return emptyConfig();
1622
1973
  }
1623
- const raw = fs7.readFileSync(this.configPath, "utf-8");
1974
+ const raw = fs8.readFileSync(this.configPath, "utf-8");
1624
1975
  const parsed = JSON.parse(raw);
1625
1976
  return parsed;
1626
1977
  }
1627
1978
  writeConfig(config) {
1628
- const dir = path6.dirname(this.configPath);
1629
- if (!fs7.existsSync(dir)) {
1630
- fs7.mkdirSync(dir, { recursive: true });
1979
+ const dir = path7.dirname(this.configPath);
1980
+ if (!fs8.existsSync(dir)) {
1981
+ fs8.mkdirSync(dir, { recursive: true });
1631
1982
  }
1632
1983
  const tmp = `${this.configPath}.tmp`;
1633
- fs7.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1634
- fs7.renameSync(tmp, this.configPath);
1984
+ fs8.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1985
+ fs8.renameSync(tmp, this.configPath);
1635
1986
  }
1636
1987
  listTasks() {
1637
1988
  return this.readConfig().tasks;
@@ -1902,8 +2253,8 @@ var TaskExecutionEngine = class {
1902
2253
 
1903
2254
  // src/scheduled-tasks/TaskLogManager.ts
1904
2255
  var import_node_crypto2 = require("crypto");
1905
- var fs8 = __toESM(require("fs"));
1906
- var path7 = __toESM(require("path"));
2256
+ var fs9 = __toESM(require("fs"));
2257
+ var path8 = __toESM(require("path"));
1907
2258
  var CONFIG_DIR4 = ".serviceme";
1908
2259
  var LOG_FILE2 = "scheduled-tasks-log.json";
1909
2260
  var MAX_LOGS = 200;
@@ -1928,17 +2279,17 @@ function validateAndRepairLogFile(raw) {
1928
2279
  }
1929
2280
  var TaskLogManager = class {
1930
2281
  constructor(workspacePath) {
1931
- this.logPath = path7.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
2282
+ this.logPath = path8.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1932
2283
  }
1933
2284
  getLogPath() {
1934
2285
  return this.logPath;
1935
2286
  }
1936
2287
  readLogFile() {
1937
- if (!fs8.existsSync(this.logPath)) {
2288
+ if (!fs9.existsSync(this.logPath)) {
1938
2289
  return emptyLogFile();
1939
2290
  }
1940
2291
  try {
1941
- const raw = fs8.readFileSync(this.logPath, "utf-8");
2292
+ const raw = fs9.readFileSync(this.logPath, "utf-8");
1942
2293
  const parsed = JSON.parse(raw);
1943
2294
  const file = validateAndRepairLogFile(parsed);
1944
2295
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
@@ -1954,21 +2305,21 @@ var TaskLogManager = class {
1954
2305
  }
1955
2306
  backupCorruptedFile() {
1956
2307
  try {
1957
- if (fs8.existsSync(this.logPath)) {
2308
+ if (fs9.existsSync(this.logPath)) {
1958
2309
  const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
1959
- fs8.copyFileSync(this.logPath, backupPath);
2310
+ fs9.copyFileSync(this.logPath, backupPath);
1960
2311
  }
1961
2312
  } catch {
1962
2313
  }
1963
2314
  }
1964
2315
  writeLogFile(file) {
1965
- const dir = path7.dirname(this.logPath);
1966
- if (!fs8.existsSync(dir)) {
1967
- fs8.mkdirSync(dir, { recursive: true });
2316
+ const dir = path8.dirname(this.logPath);
2317
+ if (!fs9.existsSync(dir)) {
2318
+ fs9.mkdirSync(dir, { recursive: true });
1968
2319
  }
1969
2320
  const tmp = `${this.logPath}.tmp`;
1970
- fs8.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1971
- fs8.renameSync(tmp, this.logPath);
2321
+ fs9.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
2322
+ fs9.renameSync(tmp, this.logPath);
1972
2323
  }
1973
2324
  appendLog(input) {
1974
2325
  const file = this.readLogFile();
@@ -2079,8 +2430,8 @@ var SchedulerDaemon = class {
2079
2430
  const configPath = this.configManager.getConfigPath();
2080
2431
  const dir = configPath.substring(0, configPath.lastIndexOf("/"));
2081
2432
  try {
2082
- if (fs9.existsSync(dir)) {
2083
- this.watcher = fs9.watch(dir, (_eventType, filename) => {
2433
+ if (fs10.existsSync(dir)) {
2434
+ this.watcher = fs10.watch(dir, (_eventType, filename) => {
2084
2435
  if (filename === "scheduled-tasks.json") {
2085
2436
  this.logger.log("info", "Config file changed, reconciling...");
2086
2437
  }
@@ -2232,8 +2583,174 @@ function matchCronField(field, value) {
2232
2583
  const values = field.split(",");
2233
2584
  return values.some((v) => Number.parseInt(v, 10) === value);
2234
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
+ };
2235
2749
  // Annotate the CommonJS export names for ESM import in node:
2236
2750
  0 && (module.exports = {
2751
+ AgentCatalogClient,
2752
+ AgentReconciler,
2753
+ AgentStore,
2237
2754
  DaemonLogger,
2238
2755
  EnvironmentInspector,
2239
2756
  GithubCopilotCliExecutor,
@@ -2243,6 +2760,10 @@ function matchCronField(field, value) {
2243
2760
  ProjectTools,
2244
2761
  SchedulerDaemon,
2245
2762
  ShellExecutor,
2763
+ SkillCatalogClient,
2764
+ SkillReconciler,
2765
+ SkillStore,
2766
+ TOOL_RISK_MAP,
2246
2767
  TaskConfigManager,
2247
2768
  TaskExecutionEngine,
2248
2769
  TaskLogManager,
@@ -2260,6 +2781,7 @@ function matchCronField(field, value) {
2260
2781
  matchesCron,
2261
2782
  moveFiles,
2262
2783
  noopLogger,
2784
+ parseAgentToolPermissions,
2263
2785
  parseIntervalMs,
2264
2786
  resolveTaskExecutionPayload,
2265
2787
  unzipFile,