@serviceme/devtools-core 0.1.5 → 0.1.7

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,8 +71,314 @@ __export(index_exports, {
63
71
  });
64
72
  module.exports = __toCommonJS(index_exports);
65
73
 
66
- // src/copilot/doctor.ts
74
+ // src/agents/AgentCatalogClient.ts
67
75
  var import_devtools_protocol = require("@serviceme/devtools-protocol");
76
+ var AgentCatalogClient = class {
77
+ constructor(options = {}) {
78
+ this.fetchImpl = options.fetchImpl ?? fetch;
79
+ this.baseUrl = options.baseUrl;
80
+ }
81
+ async getCatalog() {
82
+ if (!this.baseUrl) {
83
+ return {
84
+ agents: [],
85
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
86
+ };
87
+ }
88
+ const response = await this.fetchImpl(
89
+ `${this.baseUrl}/api/v1/marketplace/agents`
90
+ );
91
+ if (!response.ok) {
92
+ throw new Error(`Failed to fetch agents catalog: ${response.status}`);
93
+ }
94
+ const data = await response.json();
95
+ return {
96
+ agents: data.agents ?? [],
97
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
98
+ };
99
+ }
100
+ async downloadAgent(remoteId) {
101
+ if (!this.baseUrl) {
102
+ throw (0, import_devtools_protocol.createServicemeError)(
103
+ "workspace_not_found",
104
+ "Agent catalog baseUrl is not configured."
105
+ );
106
+ }
107
+ const response = await this.fetchImpl(
108
+ `${this.baseUrl}/api/v1/marketplace/agents/download/${remoteId}`
109
+ );
110
+ if (!response.ok) {
111
+ if (response.status === 404) {
112
+ throw (0, import_devtools_protocol.createServicemeError)(
113
+ "not_found",
114
+ `Agent '${remoteId}' not found`
115
+ );
116
+ }
117
+ throw new Error(
118
+ `Failed to download agent ${remoteId}: ${response.status}`
119
+ );
120
+ }
121
+ const payload = await response.json();
122
+ return payload.data?.files ?? [];
123
+ }
124
+ };
125
+
126
+ // src/permissions/agent-permissions.ts
127
+ var TOOL_RISK_MAP = {
128
+ shell: "high",
129
+ terminal: "high",
130
+ run_in_terminal: "high",
131
+ execution_subagent: "high",
132
+ filesystem: "medium",
133
+ fetch: "medium",
134
+ fetch_webpage: "medium",
135
+ create_file: "medium",
136
+ replace_string_in_file: "medium",
137
+ multi_replace_string_in_file: "medium",
138
+ read_file: "low",
139
+ search: "low",
140
+ grep_search: "low",
141
+ file_search: "low",
142
+ semantic_search: "low",
143
+ list_dir: "low"
144
+ };
145
+ var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---/;
146
+ var TOOLS_LINE_REGEX = /^tools:\s*$/m;
147
+ var TOOLS_INLINE_REGEX = /^tools:\s*\[([^\]]*)\]/m;
148
+ var LIST_ITEM_REGEX = /^\s*-\s+(.+)$/;
149
+ function parseAgentToolPermissions(content) {
150
+ const fmMatch = content.match(FRONTMATTER_REGEX);
151
+ if (!fmMatch?.[1]) return [];
152
+ const frontmatter = fmMatch[1];
153
+ const inlineMatch = frontmatter.match(TOOLS_INLINE_REGEX);
154
+ if (inlineMatch?.[1] != null) {
155
+ const raw = inlineMatch[1];
156
+ return raw.split(",").map((t) => t.trim()).filter(Boolean).map((tool) => ({
157
+ tool,
158
+ riskLevel: TOOL_RISK_MAP[tool] ?? "medium"
159
+ }));
160
+ }
161
+ const blockMatch = frontmatter.match(TOOLS_LINE_REGEX);
162
+ if (!blockMatch?.[0]) return [];
163
+ const toolsStartIndex = frontmatter.indexOf(blockMatch[0]) + blockMatch[0].length;
164
+ const remaining = frontmatter.slice(toolsStartIndex);
165
+ const lines = remaining.split(/\r?\n/);
166
+ const tools = [];
167
+ for (const line of lines) {
168
+ const itemMatch = line.match(LIST_ITEM_REGEX);
169
+ if (itemMatch?.[1]) {
170
+ const tool = itemMatch[1].trim();
171
+ tools.push({ tool, riskLevel: TOOL_RISK_MAP[tool] ?? "medium" });
172
+ } else if (line.trim() !== "" && !line.startsWith(" ") && !line.startsWith(" ")) {
173
+ break;
174
+ }
175
+ }
176
+ return tools;
177
+ }
178
+
179
+ // src/agents/AgentReconciler.ts
180
+ var AgentReconciler = class {
181
+ constructor(deps) {
182
+ this.deps = deps;
183
+ }
184
+ async mutate(request) {
185
+ if (request.targetScope !== "workspace" && request.targetScope !== "user") {
186
+ throw new Error(`Invalid target scope: ${String(request.targetScope)}`);
187
+ }
188
+ if (request.action === "uninstall" || request.action === "move" || request.action === "removeExternal") {
189
+ return {
190
+ status: "success",
191
+ changed: true,
192
+ message: `Agent ${request.action} completed.`
193
+ };
194
+ }
195
+ if (request.action !== "install") {
196
+ return {
197
+ status: "blocked",
198
+ changed: false,
199
+ message: `Agent action is not supported by bridge reconciler: ${request.action}`
200
+ };
201
+ }
202
+ const catalog = await this.deps.catalogClient.getCatalog();
203
+ const remoteAgent = catalog.agents.find(
204
+ (agent) => this.deps.agentStore.normalizeRemoteAgentId(agent.id) === request.agentId
205
+ );
206
+ if (!remoteAgent) {
207
+ return {
208
+ status: "blocked",
209
+ changed: false,
210
+ message: "Agent not found in catalog."
211
+ };
212
+ }
213
+ if (!request.confirmed && this.hasHighRiskTool(remoteAgent.tools)) {
214
+ return {
215
+ status: "requires_confirmation",
216
+ changed: false,
217
+ message: "This agent uses high-risk tools that require confirmation.",
218
+ tools: remoteAgent.tools
219
+ };
220
+ }
221
+ return {
222
+ status: "success",
223
+ changed: true,
224
+ message: "Agent installed."
225
+ };
226
+ }
227
+ getPermissionSummary(agentId, agentName, content) {
228
+ const tools = parseAgentToolPermissions(content);
229
+ return {
230
+ agentId,
231
+ agentName,
232
+ tools,
233
+ highRiskCount: tools.filter((tool) => tool.riskLevel === "high").length,
234
+ mediumRiskCount: tools.filter((tool) => tool.riskLevel === "medium").length,
235
+ lowRiskCount: tools.filter((tool) => tool.riskLevel === "low").length
236
+ };
237
+ }
238
+ hasHighRiskTool(tools) {
239
+ return tools.some((tool) => TOOL_RISK_MAP[tool] === "high");
240
+ }
241
+ };
242
+
243
+ // src/agents/AgentStore.ts
244
+ var fs = __toESM(require("fs/promises"));
245
+ var path = __toESM(require("path"));
246
+ var WORKSPACE_AGENTS_ROOT_RELATIVE = ".github/agents";
247
+ var WORKSPACE_AGENTS_STATE_RELATIVE = ".github/.ms-devtools-agents.yml";
248
+ var SAFE_LOCAL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
249
+ function assertSafeLocalAgentId(agentId) {
250
+ if (typeof agentId !== "string" || agentId.length === 0 || agentId === "." || agentId === ".." || agentId.includes("/") || agentId.includes("\\") || !SAFE_LOCAL_ID_PATTERN.test(agentId)) {
251
+ throw new Error(`Invalid agent id: ${agentId}`);
252
+ }
253
+ return agentId;
254
+ }
255
+ var AgentStore = class {
256
+ constructor(options) {
257
+ this.workspacePath = options.workspacePath;
258
+ this.userAgentsRoot = options.userAgentsRoot;
259
+ this.fileSystem = options.fileSystem ?? fs;
260
+ this.schemaVersion = options.schemaVersion ?? 1;
261
+ }
262
+ normalizeRemoteAgentId(remoteId) {
263
+ if (remoteId.startsWith("official/")) {
264
+ return assertSafeLocalAgentId(remoteId.slice("official/".length));
265
+ }
266
+ if (remoteId.startsWith("community/")) {
267
+ const lastSlash = remoteId.lastIndexOf("/");
268
+ return assertSafeLocalAgentId(remoteId.slice(lastSlash + 1));
269
+ }
270
+ return assertSafeLocalAgentId(remoteId);
271
+ }
272
+ getWorkspaceAgentsRootPath() {
273
+ return WORKSPACE_AGENTS_ROOT_RELATIVE;
274
+ }
275
+ getWorkspaceStateFilePath() {
276
+ return WORKSPACE_AGENTS_STATE_RELATIVE;
277
+ }
278
+ getUserAgentsRootPath() {
279
+ return this.userAgentsRoot;
280
+ }
281
+ async listWorkspaceAgentIds() {
282
+ return this.listAgentIds(
283
+ path.join(this.workspacePath, WORKSPACE_AGENTS_ROOT_RELATIVE)
284
+ );
285
+ }
286
+ async listUserAgentIds() {
287
+ return this.listAgentIds(this.userAgentsRoot);
288
+ }
289
+ async readState() {
290
+ try {
291
+ const raw = await this.fileSystem.readFile(
292
+ path.join(this.workspacePath, WORKSPACE_AGENTS_STATE_RELATIVE),
293
+ "utf-8"
294
+ );
295
+ const parsed = JSON.parse(raw);
296
+ if (typeof parsed.schemaVersion !== "number" || !Array.isArray(parsed.installedAgents)) {
297
+ return null;
298
+ }
299
+ return parsed;
300
+ } catch {
301
+ return null;
302
+ }
303
+ }
304
+ async writeState(state) {
305
+ const statePath = path.join(
306
+ this.workspacePath,
307
+ WORKSPACE_AGENTS_STATE_RELATIVE
308
+ );
309
+ await this.fileSystem.mkdir(path.dirname(statePath), { recursive: true });
310
+ await this.fileSystem.writeFile(
311
+ statePath,
312
+ JSON.stringify(state, null, 2),
313
+ "utf-8"
314
+ );
315
+ }
316
+ async addInstalledAgent(entry) {
317
+ const state = await this.readState() ?? {
318
+ schemaVersion: this.schemaVersion,
319
+ installedAgents: []
320
+ };
321
+ state.installedAgents = state.installedAgents.filter(
322
+ (agent) => agent.id !== entry.id
323
+ );
324
+ state.installedAgents.push(entry);
325
+ await this.writeState(state);
326
+ }
327
+ async removeInstalledAgent(agentId) {
328
+ const state = await this.readState();
329
+ if (!state) {
330
+ return;
331
+ }
332
+ state.installedAgents = state.installedAgents.filter(
333
+ (agent) => agent.id !== agentId
334
+ );
335
+ await this.writeState(state);
336
+ }
337
+ async writeAgentFiles(agentId, scope, files) {
338
+ const root = scope === "workspace" ? path.join(this.workspacePath, WORKSPACE_AGENTS_ROOT_RELATIVE) : this.userAgentsRoot;
339
+ const firstFile = files[0];
340
+ const isSingleFlatFile = files.length === 1 && firstFile !== void 0 && firstFile.path === `${agentId}.agent.md` && !firstFile.path.includes("/");
341
+ const targetDir = isSingleFlatFile ? root : path.join(root, agentId);
342
+ await this.fileSystem.mkdir(targetDir, { recursive: true });
343
+ for (const file of files) {
344
+ const filePath = path.join(targetDir, file.path);
345
+ await this.fileSystem.mkdir(path.dirname(filePath), { recursive: true });
346
+ await this.fileSystem.writeFile(filePath, file.content, "utf-8");
347
+ if (file.executable) {
348
+ try {
349
+ await this.fileSystem.chmod(filePath, 493);
350
+ } catch {
351
+ }
352
+ }
353
+ }
354
+ }
355
+ async listAgentIds(dir) {
356
+ try {
357
+ const entries = await this.fileSystem.readdir(dir, {
358
+ withFileTypes: true
359
+ });
360
+ const ids = [];
361
+ for (const entry of entries) {
362
+ if (entry.name.startsWith(".")) {
363
+ continue;
364
+ }
365
+ if (entry.isDirectory()) {
366
+ ids.push(entry.name);
367
+ continue;
368
+ }
369
+ if (entry.isFile() && entry.name.endsWith(".agent.md")) {
370
+ ids.push(entry.name.replace(/\.agent\.md$/, ""));
371
+ }
372
+ }
373
+ return ids.sort();
374
+ } catch {
375
+ return [];
376
+ }
377
+ }
378
+ };
379
+
380
+ // src/copilot/doctor.ts
381
+ var import_devtools_protocol2 = require("@serviceme/devtools-protocol");
68
382
 
69
383
  // src/process/runCommand.ts
70
384
  var import_node_child_process = require("child_process");
@@ -208,20 +522,20 @@ async function copilotDoctor() {
208
522
  return { installed: true, version, authenticated };
209
523
  }
210
524
  function createCopilotNotInstalledError() {
211
- return (0, import_devtools_protocol.createServicemeError)(
212
- import_devtools_protocol.COPILOT_ERROR_CODES.NOT_INSTALLED,
525
+ return (0, import_devtools_protocol2.createServicemeError)(
526
+ import_devtools_protocol2.COPILOT_ERROR_CODES.NOT_INSTALLED,
213
527
  "GitHub Copilot CLI is not installed. Visit https://docs.github.com/en/copilot/using-github-copilot/using-github-copilot-in-the-command-line to install."
214
528
  );
215
529
  }
216
530
  function createCopilotAuthRequiredError() {
217
- return (0, import_devtools_protocol.createServicemeError)(
218
- import_devtools_protocol.COPILOT_ERROR_CODES.AUTH_REQUIRED,
531
+ return (0, import_devtools_protocol2.createServicemeError)(
532
+ import_devtools_protocol2.COPILOT_ERROR_CODES.AUTH_REQUIRED,
219
533
  "GitHub Copilot CLI requires authentication. Run `copilot auth login` to sign in."
220
534
  );
221
535
  }
222
536
 
223
537
  // src/copilot/prompt.ts
224
- var import_devtools_protocol2 = require("@serviceme/devtools-protocol");
538
+ var import_devtools_protocol3 = require("@serviceme/devtools-protocol");
225
539
  var COPILOT_COMMAND2 = "copilot";
226
540
  var DEFAULT_TIMEOUT_MS = 12e4;
227
541
  function stripAnsi(text) {
@@ -255,8 +569,8 @@ async function copilotPrompt(options) {
255
569
  });
256
570
  const output = stripAnsi(result.stdout);
257
571
  if (output.includes("I'm sorry, but I cannot assist") && result.stderr?.includes("Stream completed without a response")) {
258
- throw (0, import_devtools_protocol2.createServicemeError)(
259
- import_devtools_protocol2.COPILOT_ERROR_CODES.AUTH_REQUIRED,
572
+ throw (0, import_devtools_protocol3.createServicemeError)(
573
+ import_devtools_protocol3.COPILOT_ERROR_CODES.AUTH_REQUIRED,
260
574
  "Copilot CLI returned a refusal response. This typically indicates an expired authentication token. Run `gh auth login` to re-authenticate.",
261
575
  { exitCode: 0, output, stderr: result.stderr }
262
576
  );
@@ -265,8 +579,8 @@ async function copilotPrompt(options) {
265
579
  } catch (error) {
266
580
  const err = error;
267
581
  if (err.code === "ETIMEDOUT") {
268
- throw (0, import_devtools_protocol2.createServicemeError)(
269
- import_devtools_protocol2.COPILOT_ERROR_CODES.TIMEOUT,
582
+ throw (0, import_devtools_protocol3.createServicemeError)(
583
+ import_devtools_protocol3.COPILOT_ERROR_CODES.TIMEOUT,
270
584
  `Copilot prompt timed out after ${String(timeout)}ms.`
271
585
  );
272
586
  }
@@ -274,8 +588,8 @@ async function copilotPrompt(options) {
274
588
  const output = stripAnsi(err.stdout ?? "");
275
589
  const stderr = err.stderr ?? err.message ?? "";
276
590
  if (output.includes("I'm sorry, but I cannot assist") || stderr.includes("Stream completed without a response")) {
277
- throw (0, import_devtools_protocol2.createServicemeError)(
278
- import_devtools_protocol2.COPILOT_ERROR_CODES.AUTH_REQUIRED,
591
+ throw (0, import_devtools_protocol3.createServicemeError)(
592
+ import_devtools_protocol3.COPILOT_ERROR_CODES.AUTH_REQUIRED,
279
593
  "Copilot CLI returned a refusal response. This typically indicates an expired authentication token. Run `gh auth login` to re-authenticate.",
280
594
  { exitCode, output, stderr }
281
595
  );
@@ -283,8 +597,8 @@ async function copilotPrompt(options) {
283
597
  if (exitCode !== 0 && output) {
284
598
  return { output, exitCode };
285
599
  }
286
- throw (0, import_devtools_protocol2.createServicemeError)(
287
- import_devtools_protocol2.COPILOT_ERROR_CODES.EXECUTION_FAILED,
600
+ throw (0, import_devtools_protocol3.createServicemeError)(
601
+ import_devtools_protocol3.COPILOT_ERROR_CODES.EXECUTION_FAILED,
288
602
  stderr || `Copilot exited with code ${String(exitCode)}.`,
289
603
  { exitCode, stderr }
290
604
  );
@@ -292,19 +606,25 @@ async function copilotPrompt(options) {
292
606
  }
293
607
 
294
608
  // src/env/environmentInspector.ts
295
- var import_devtools_protocol3 = require("@serviceme/devtools-protocol");
609
+ var import_devtools_protocol4 = require("@serviceme/devtools-protocol");
296
610
  var DEFAULT_TOOL_CHECK_TIMEOUT_MS = 5e3;
297
611
  var TOOL_CHECK_TIMEOUT_MS = {
298
612
  nuget: 12e3,
299
613
  nvm: 8e3,
300
- dotnet: 8e3
614
+ dotnet: 8e3,
615
+ // nvm4w/npm shims and globally-installed .cmd tools (pnpm, nrm) are slow to
616
+ // resolve on cold PATHs. Give them extra headroom so the version probe
617
+ // doesn't fall through to the generic 5s default.
618
+ npm: 15e3,
619
+ pnpm: 15e3,
620
+ nrm: 15e3
301
621
  };
302
622
  var ERROR_CODE_NOT_FOUND = 127;
303
623
  var ERROR_CODE_TIMEOUT = "ETIMEDOUT";
304
624
  var EnvironmentInspector = class {
305
625
  async checkEnvironment() {
306
626
  const results = await Promise.all(
307
- import_devtools_protocol3.KNOWN_ENVIRONMENT_TOOLS.map(
627
+ import_devtools_protocol4.KNOWN_ENVIRONMENT_TOOLS.map(
308
628
  async (tool) => [tool, await this.checkTool(tool)]
309
629
  )
310
630
  );
@@ -312,7 +632,7 @@ var EnvironmentInspector = class {
312
632
  }
313
633
  async checkTool(toolName) {
314
634
  if (!/^[a-zA-Z0-9-]+$/.test(toolName)) {
315
- throw (0, import_devtools_protocol3.createServicemeError)("invalid_params", "Invalid tool name.");
635
+ throw (0, import_devtools_protocol4.createServicemeError)("invalid_params", "Invalid tool name.");
316
636
  }
317
637
  try {
318
638
  if (toolName === "nvm") {
@@ -344,8 +664,29 @@ var EnvironmentInspector = class {
344
664
  return void 0;
345
665
  }
346
666
  }
667
+ /**
668
+ * On Windows, prefer the `.cmd` shim for npm/pnpm/nrm. nvm4w registers
669
+ * BOTH an extensionless entry (pointing to node.exe) and a `<tool>.cmd`
670
+ * wrapper; `where` returns them in that order, and the extensionless one
671
+ * would just print node's own version.
672
+ */
673
+ async getToolShimPath(toolName) {
674
+ try {
675
+ const result = await runCommand("where", {
676
+ args: [toolName],
677
+ timeoutMs: this.getToolTimeout(toolName)
678
+ });
679
+ const candidates = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
680
+ const cmdShim = candidates.find(
681
+ (line) => line.toLowerCase().endsWith(".cmd")
682
+ );
683
+ return cmdShim ?? candidates[0];
684
+ } catch {
685
+ return void 0;
686
+ }
687
+ }
347
688
  async getToolVersion(toolName) {
348
- const invocation = this.getVersionInvocation(toolName);
689
+ const invocation = await this.getVersionInvocation(toolName);
349
690
  const result = await runCommand(invocation.command, {
350
691
  args: invocation.args,
351
692
  timeoutMs: this.getToolTimeout(toolName)
@@ -420,12 +761,19 @@ var EnvironmentInspector = class {
420
761
  return this.handleToolCheckError(error);
421
762
  }
422
763
  }
423
- getVersionInvocation(toolName) {
764
+ async getVersionInvocation(toolName) {
424
765
  const isWindows = process.platform === "win32";
425
766
  if (isWindows && ["npm", "pnpm", "nrm"].includes(toolName)) {
767
+ const shim = await this.getToolShimPath(toolName);
768
+ if (shim) {
769
+ return {
770
+ command: "cmd.exe",
771
+ args: ["/c", shim, "--version"]
772
+ };
773
+ }
426
774
  return {
427
775
  command: "cmd.exe",
428
- args: ["/d", "/s", "/c", `${toolName} --version`]
776
+ args: ["/c", toolName, "--version"]
429
777
  };
430
778
  }
431
779
  const commands = {
@@ -468,7 +816,7 @@ var EnvironmentInspector = class {
468
816
  const execError = error;
469
817
  const message = execError.message || String(error);
470
818
  const code = execError.code;
471
- const isNotFound = message.includes("command not found") || message.includes("not recognized") || message.includes("ENOENT") || code === "ENOENT" || code === ERROR_CODE_NOT_FOUND;
819
+ const isNotFound = message.includes("command not found") || message.includes("not recognized") || message.includes("ENOENT") || message.includes("EACCES") || code === "ENOENT" || code === "EACCES" || code === ERROR_CODE_NOT_FOUND;
472
820
  const isTimeout = code === ERROR_CODE_TIMEOUT || message.toLowerCase().includes("timed out");
473
821
  return {
474
822
  installed: false,
@@ -478,9 +826,9 @@ var EnvironmentInspector = class {
478
826
  };
479
827
 
480
828
  // src/image/imageTools.ts
481
- var fs = __toESM(require("fs/promises"));
482
- var path = __toESM(require("path"));
483
- var import_devtools_protocol4 = require("@serviceme/devtools-protocol");
829
+ var fs2 = __toESM(require("fs/promises"));
830
+ var path2 = __toESM(require("path"));
831
+ var import_devtools_protocol5 = require("@serviceme/devtools-protocol");
484
832
  var SUPPORTED_FORMATS = [
485
833
  ".jpg",
486
834
  ".jpeg",
@@ -500,7 +848,7 @@ var ImageTools = class {
500
848
  if (!await this.pathExists(filePath)) {
501
849
  return { valid: false };
502
850
  }
503
- if (!SUPPORTED_FORMATS.includes(path.extname(filePath).toLowerCase())) {
851
+ if (!SUPPORTED_FORMATS.includes(path2.extname(filePath).toLowerCase())) {
504
852
  return { valid: false };
505
853
  }
506
854
  await this.getInfo(filePath, sharpModulePath);
@@ -512,7 +860,7 @@ var ImageTools = class {
512
860
  async getInfo(imagePath, sharpModulePath) {
513
861
  const sharp = this.loadSharp(sharpModulePath);
514
862
  const metadata = await sharp(imagePath).metadata();
515
- const stats = await fs.stat(imagePath);
863
+ const stats = await fs2.stat(imagePath);
516
864
  return {
517
865
  width: metadata.width ?? 0,
518
866
  height: metadata.height ?? 0,
@@ -524,12 +872,12 @@ var ImageTools = class {
524
872
  async compress(imagePath, options) {
525
873
  const validation = await this.validate(imagePath, options.sharpModulePath);
526
874
  if (!validation.valid) {
527
- throw (0, import_devtools_protocol4.createServicemeError)(
875
+ throw (0, import_devtools_protocol5.createServicemeError)(
528
876
  "invalid_params",
529
877
  `Invalid image file: ${imagePath}`
530
878
  );
531
879
  }
532
- const originalStats = await fs.stat(imagePath);
880
+ const originalStats = await fs2.stat(imagePath);
533
881
  const originalSize = originalStats.size;
534
882
  const outputPath = this.getOutputPath(imagePath, options);
535
883
  const compressedBuffer = await this.compressWithSharp(imagePath, options);
@@ -544,7 +892,7 @@ var ImageTools = class {
544
892
  outputPath: imagePath
545
893
  };
546
894
  }
547
- await fs.writeFile(outputPath, compressedBuffer);
895
+ await fs2.writeFile(outputPath, compressedBuffer);
548
896
  return {
549
897
  originalSize,
550
898
  compressedSize,
@@ -555,7 +903,7 @@ var ImageTools = class {
555
903
  async compressWithSharp(imagePath, options) {
556
904
  const sharp = this.loadSharp(options.sharpModulePath);
557
905
  let pipeline = sharp(imagePath);
558
- switch (options.format ?? path.extname(imagePath).toLowerCase().slice(1)) {
906
+ switch (options.format ?? path2.extname(imagePath).toLowerCase().slice(1)) {
559
907
  case "jpg":
560
908
  case "jpeg":
561
909
  pipeline = pipeline.jpeg({ quality: options.quality });
@@ -581,14 +929,14 @@ var ImageTools = class {
581
929
  if (options.replaceOriginImage) {
582
930
  return inputPath;
583
931
  }
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}`);
932
+ const dir = path2.dirname(inputPath);
933
+ const ext = path2.extname(inputPath);
934
+ const name = path2.basename(inputPath, ext);
935
+ return path2.join(dir, `${name}_compressed${ext}`);
588
936
  }
589
937
  async pathExists(targetPath) {
590
938
  try {
591
- await fs.access(targetPath);
939
+ await fs2.access(targetPath);
592
940
  return true;
593
941
  } catch {
594
942
  return false;
@@ -598,7 +946,7 @@ var ImageTools = class {
598
946
  try {
599
947
  return sharpModulePath ? require(sharpModulePath) : require("sharp");
600
948
  } catch (error) {
601
- throw (0, import_devtools_protocol4.createServicemeError)(
949
+ throw (0, import_devtools_protocol5.createServicemeError)(
602
950
  "internal_error",
603
951
  `sharp is required for image operations: ${error instanceof Error ? error.message : String(error)}`
604
952
  );
@@ -610,13 +958,13 @@ function createImageTools() {
610
958
  }
611
959
 
612
960
  // src/json/jsonTools.ts
613
- var import_devtools_protocol5 = require("@serviceme/devtools-protocol");
961
+ var import_devtools_protocol6 = require("@serviceme/devtools-protocol");
614
962
  var import_comment_json = require("comment-json");
615
963
  var import_json5 = __toESM(require("json5"));
616
964
  function createJsonTools() {
617
965
  return {
618
966
  sort(text, options) {
619
- const finalOptions = { ...import_devtools_protocol5.DEFAULT_JSON_SORT_OPTIONS, ...options };
967
+ const finalOptions = { ...import_devtools_protocol6.DEFAULT_JSON_SORT_OPTIONS, ...options };
620
968
  const json = parseJson(text);
621
969
  const sorted = sortObject(json, finalOptions);
622
970
  return JSON.stringify(sorted, null, detectIndent(text));
@@ -652,7 +1000,7 @@ function parseJson(text) {
652
1000
  } catch {
653
1001
  }
654
1002
  }
655
- throw (0, import_devtools_protocol5.createServicemeError)("json_invalid_input", "Invalid JSON format.");
1003
+ throw (0, import_devtools_protocol6.createServicemeError)("json_invalid_input", "Invalid JSON format.");
656
1004
  }
657
1005
  function sortObject(obj, options) {
658
1006
  if (Array.isArray(obj)) {
@@ -744,9 +1092,9 @@ function createConsoleLogger(prefix = "serviceme") {
744
1092
  }
745
1093
 
746
1094
  // src/project/projectTools.ts
747
- var fs2 = __toESM(require("fs/promises"));
748
- var path2 = __toESM(require("path"));
749
- var import_devtools_protocol6 = require("@serviceme/devtools-protocol");
1095
+ var fs3 = __toESM(require("fs/promises"));
1096
+ var path3 = __toESM(require("path"));
1097
+ var import_devtools_protocol7 = require("@serviceme/devtools-protocol");
750
1098
 
751
1099
  // src/utils/fileUtils.ts
752
1100
  var import_node_fs = require("fs");
@@ -866,10 +1214,10 @@ var moveFiles = async (sourceDir, destDir, overwrite = false) => {
866
1214
  var ProjectTools = class {
867
1215
  async extractTemplate(zipPath, workspacePath, tempExtractDir, input) {
868
1216
  await unzipFile(zipPath, tempExtractDir);
869
- let sourceDir = path2.join(tempExtractDir, input.extractedDirName);
1217
+ let sourceDir = path3.join(tempExtractDir, input.extractedDirName);
870
1218
  let actualDirName = input.extractedDirName;
871
1219
  if (!await this.pathExists(sourceDir)) {
872
- const entries = await fs2.readdir(tempExtractDir, { withFileTypes: true });
1220
+ const entries = await fs3.readdir(tempExtractDir, { withFileTypes: true });
873
1221
  const directories = entries.filter(
874
1222
  (entry) => entry.isDirectory() && !entry.name.startsWith(".")
875
1223
  );
@@ -881,7 +1229,7 @@ var ProjectTools = class {
881
1229
  );
882
1230
  if (selectedDirectory) {
883
1231
  actualDirName = selectedDirectory;
884
- sourceDir = path2.join(tempExtractDir, actualDirName);
1232
+ sourceDir = path3.join(tempExtractDir, actualDirName);
885
1233
  } else if (directories.length === 0) {
886
1234
  throw new Error(
887
1235
  `No directory found after extraction. Expected directory: ${input.extractedDirName}`
@@ -902,7 +1250,7 @@ var ProjectTools = class {
902
1250
  }
903
1251
  async installDependencies(workspacePath, command) {
904
1252
  if (!command) {
905
- throw (0, import_devtools_protocol6.createServicemeError)("invalid_params", "Expected install command.");
1253
+ throw (0, import_devtools_protocol7.createServicemeError)("invalid_params", "Expected install command.");
906
1254
  }
907
1255
  await runCommand(command, {
908
1256
  cwd: workspacePath,
@@ -939,7 +1287,7 @@ var ProjectTools = class {
939
1287
  } else {
940
1288
  for (const scriptPath of scripts) {
941
1289
  try {
942
- await fs2.chmod(scriptPath, 493);
1290
+ await fs3.chmod(scriptPath, 493);
943
1291
  updatedCount += 1;
944
1292
  } catch {
945
1293
  }
@@ -965,13 +1313,16 @@ var ProjectTools = class {
965
1313
  }
966
1314
  async runScaffoldPrune(workspacePath, preset) {
967
1315
  if (!preset) {
968
- throw (0, import_devtools_protocol6.createServicemeError)("invalid_params", "Expected scaffold preset.");
1316
+ throw (0, import_devtools_protocol7.createServicemeError)("invalid_params", "Expected scaffold preset.");
969
1317
  }
970
- const commands = [
971
- `pnpm run scaffold:prune -- --preset ${preset} --project-root .`,
972
- `pnpm run scaffold:init-metadata -- --preset ${preset} --project-root . --force`
973
- ];
1318
+ await this.ensurePresetManifest(workspacePath, preset);
1319
+ const pruneCommand = `pnpm run scaffold:prune -- --preset ${preset} --project-root .`;
1320
+ const initMetadataCommand = `pnpm run scaffold:init-metadata -- --preset ${preset} --project-root . --force`;
1321
+ const commands = [pruneCommand, initMetadataCommand];
974
1322
  for (const command of commands) {
1323
+ if (command === initMetadataCommand) {
1324
+ await this.ensurePresetManifest(workspacePath, preset);
1325
+ }
975
1326
  await runCommand(command, {
976
1327
  cwd: workspacePath,
977
1328
  shell: true
@@ -983,16 +1334,54 @@ var ProjectTools = class {
983
1334
  commands
984
1335
  };
985
1336
  }
1337
+ async ensurePresetManifest(workspacePath, preset) {
1338
+ const presetManifestPath = path3.join(
1339
+ workspacePath,
1340
+ ".ms-scaffold",
1341
+ "presets",
1342
+ `${preset}.json`
1343
+ );
1344
+ if (await this.pathExists(presetManifestPath)) {
1345
+ return;
1346
+ }
1347
+ const projectModePath = path3.join(
1348
+ workspacePath,
1349
+ ".ms-scaffold",
1350
+ "project-mode.json"
1351
+ );
1352
+ if (!await this.pathExists(projectModePath)) {
1353
+ return;
1354
+ }
1355
+ const projectModeRaw = await fs3.readFile(projectModePath, "utf8");
1356
+ const projectMode = JSON.parse(projectModeRaw);
1357
+ const synthesizedPreset = {
1358
+ preset,
1359
+ selectedModules: Array.isArray(projectMode.selectedModules) ? projectMode.selectedModules : [],
1360
+ prunedModules: Array.isArray(projectMode.prunedModules) ? projectMode.prunedModules : [],
1361
+ exclude: [],
1362
+ recommendedFollowUps: [],
1363
+ managedFiles: [],
1364
+ mergeManagedFiles: [],
1365
+ userOwnedPaths: []
1366
+ };
1367
+ await fs3.mkdir(path3.dirname(presetManifestPath), { recursive: true });
1368
+ await fs3.writeFile(
1369
+ presetManifestPath,
1370
+ `${JSON.stringify(synthesizedPreset, null, 2)}
1371
+ `,
1372
+ "utf8"
1373
+ );
1374
+ }
986
1375
  async findScripts(dir, extensions) {
987
1376
  const results = [];
988
1377
  let entries;
989
1378
  try {
990
- entries = await fs2.readdir(dir, { withFileTypes: true });
1379
+ entries = await fs3.readdir(dir, { withFileTypes: true });
991
1380
  } catch {
992
1381
  return results;
993
1382
  }
994
1383
  for (const entry of entries) {
995
- const fullPath = path2.join(dir, entry.name);
1384
+ const fullPath = path3.join(dir, entry.name);
996
1385
  if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
997
1386
  results.push(...await this.findScripts(fullPath, extensions));
998
1387
  } else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
@@ -1003,7 +1392,7 @@ var ProjectTools = class {
1003
1392
  }
1004
1393
  async pathExists(targetPath) {
1005
1394
  try {
1006
- await fs2.access(targetPath);
1395
+ await fs3.access(targetPath);
1007
1396
  return true;
1008
1397
  } catch {
1009
1398
  return false;
@@ -1022,7 +1411,7 @@ var ProjectTools = class {
1022
1411
  const matches = [];
1023
1412
  for (const directoryName of directoryNames) {
1024
1413
  if (await this.directoryMatchesProjectPattern(
1025
- path2.join(tempExtractDir, directoryName),
1414
+ path3.join(tempExtractDir, directoryName),
1026
1415
  projectFilePattern
1027
1416
  )) {
1028
1417
  matches.push(directoryName);
@@ -1034,7 +1423,7 @@ var ProjectTools = class {
1034
1423
  return null;
1035
1424
  }
1036
1425
  async directoryMatchesProjectPattern(directoryPath, projectFilePattern) {
1037
- const entries = await fs2.readdir(directoryPath);
1426
+ const entries = await fs3.readdir(directoryPath);
1038
1427
  if (projectFilePattern.includes("*")) {
1039
1428
  const regex = new RegExp(`^${projectFilePattern.replace("*", ".*")}$`);
1040
1429
  return entries.some((entry) => regex.test(entry));
@@ -1047,17 +1436,17 @@ function createProjectTools() {
1047
1436
  }
1048
1437
 
1049
1438
  // src/scheduled-tasks/daemon/DaemonLogger.ts
1050
- var fs3 = __toESM(require("fs"));
1051
- var path3 = __toESM(require("path"));
1439
+ var fs4 = __toESM(require("fs"));
1440
+ var path4 = __toESM(require("path"));
1052
1441
  var CONFIG_DIR = ".serviceme";
1053
1442
  var LOG_FILE = "scheduler.log";
1054
1443
  var MAX_LOG_SIZE = 1024 * 1024;
1055
1444
  var DaemonLogger = class {
1056
1445
  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 });
1446
+ this.logPath = path4.join(workspacePath, CONFIG_DIR, LOG_FILE);
1447
+ const dir = path4.dirname(this.logPath);
1448
+ if (!fs4.existsSync(dir)) {
1449
+ fs4.mkdirSync(dir, { recursive: true });
1061
1450
  }
1062
1451
  }
1063
1452
  getLogPath() {
@@ -1068,16 +1457,16 @@ var DaemonLogger = class {
1068
1457
  const line = `[${ts}] [${level.toUpperCase()}] ${message}
1069
1458
  `;
1070
1459
  this.rotateIfNeeded();
1071
- fs3.appendFileSync(this.logPath, line, "utf-8");
1460
+ fs4.appendFileSync(this.logPath, line, "utf-8");
1072
1461
  }
1073
1462
  rotateIfNeeded() {
1074
1463
  try {
1075
- const stats = fs3.statSync(this.logPath);
1464
+ const stats = fs4.statSync(this.logPath);
1076
1465
  if (stats.size > MAX_LOG_SIZE) {
1077
- const content = fs3.readFileSync(this.logPath, "utf-8");
1466
+ const content = fs4.readFileSync(this.logPath, "utf-8");
1078
1467
  const halfIdx = content.indexOf("\n", Math.floor(content.length / 2));
1079
1468
  if (halfIdx > 0) {
1080
- fs3.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
1469
+ fs4.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
1081
1470
  }
1082
1471
  }
1083
1472
  } catch {
@@ -1086,33 +1475,33 @@ var DaemonLogger = class {
1086
1475
  };
1087
1476
 
1088
1477
  // src/scheduled-tasks/daemon/PidManager.ts
1089
- var fs4 = __toESM(require("fs"));
1090
- var path4 = __toESM(require("path"));
1478
+ var fs5 = __toESM(require("fs"));
1479
+ var path5 = __toESM(require("path"));
1091
1480
  var CONFIG_DIR2 = ".serviceme";
1092
1481
  var PID_FILE = "scheduler.pid";
1093
1482
  var PidManager = class {
1094
1483
  constructor(workspacePath) {
1095
- this.pidPath = path4.join(workspacePath, CONFIG_DIR2, PID_FILE);
1484
+ this.pidPath = path5.join(workspacePath, CONFIG_DIR2, PID_FILE);
1096
1485
  }
1097
1486
  getPidPath() {
1098
1487
  return this.pidPath;
1099
1488
  }
1100
1489
  writePid(pid) {
1101
- const dir = path4.dirname(this.pidPath);
1102
- if (!fs4.existsSync(dir)) {
1103
- fs4.mkdirSync(dir, { recursive: true });
1490
+ const dir = path5.dirname(this.pidPath);
1491
+ if (!fs5.existsSync(dir)) {
1492
+ fs5.mkdirSync(dir, { recursive: true });
1104
1493
  }
1105
- fs4.writeFileSync(this.pidPath, String(pid), "utf-8");
1494
+ fs5.writeFileSync(this.pidPath, String(pid), "utf-8");
1106
1495
  }
1107
1496
  readPid() {
1108
- if (!fs4.existsSync(this.pidPath)) return null;
1109
- const raw = fs4.readFileSync(this.pidPath, "utf-8").trim();
1497
+ if (!fs5.existsSync(this.pidPath)) return null;
1498
+ const raw = fs5.readFileSync(this.pidPath, "utf-8").trim();
1110
1499
  const pid = Number.parseInt(raw, 10);
1111
1500
  return Number.isNaN(pid) ? null : pid;
1112
1501
  }
1113
1502
  removePid() {
1114
- if (fs4.existsSync(this.pidPath)) {
1115
- fs4.unlinkSync(this.pidPath);
1503
+ if (fs5.existsSync(this.pidPath)) {
1504
+ fs5.unlinkSync(this.pidPath);
1116
1505
  }
1117
1506
  }
1118
1507
  isProcessRunning(pid) {
@@ -1133,12 +1522,12 @@ var PidManager = class {
1133
1522
  };
1134
1523
 
1135
1524
  // src/scheduled-tasks/daemon/SchedulerDaemon.ts
1136
- var fs9 = __toESM(require("fs"));
1525
+ var fs10 = __toESM(require("fs"));
1137
1526
  var os = __toESM(require("os"));
1138
1527
 
1139
1528
  // src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
1140
1529
  var import_node_child_process2 = require("child_process");
1141
- var fs5 = __toESM(require("fs"));
1530
+ var fs6 = __toESM(require("fs"));
1142
1531
 
1143
1532
  // src/scheduled-tasks/executors/timeout.ts
1144
1533
  function resolveConfiguredTimeoutMs(timeoutSeconds, defaultTimeoutMs) {
@@ -1173,7 +1562,7 @@ function redactArgs(args) {
1173
1562
  function writeDiagnostic(message) {
1174
1563
  const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1175
1564
  if (logPath) {
1176
- fs5.appendFileSync(logPath, message);
1565
+ fs6.appendFileSync(logPath, message);
1177
1566
  return;
1178
1567
  }
1179
1568
  process.stderr.write(message);
@@ -1383,15 +1772,15 @@ ${body}`.trim()
1383
1772
 
1384
1773
  // src/scheduled-tasks/executors/ShellExecutor.ts
1385
1774
  var import_node_child_process3 = require("child_process");
1386
- var fs6 = __toESM(require("fs"));
1387
- var path5 = __toESM(require("path"));
1775
+ var fs7 = __toESM(require("fs"));
1776
+ var path6 = __toESM(require("path"));
1388
1777
  var MAX_OUTPUT_BYTES2 = 1024 * 1024;
1389
1778
  var DEFAULT_TIMEOUT_MS4 = 6e4;
1390
1779
  var POSIX_SHELL_CANDIDATES = ["bash.exe", "sh.exe"];
1391
1780
  function resolveShellExecution(script, options = {}) {
1392
1781
  const platform = options.platform ?? process.platform;
1393
1782
  const env = options.env ?? process.env;
1394
- const fileExists = options.fileExists ?? fs6.existsSync;
1783
+ const fileExists = options.fileExists ?? fs7.existsSync;
1395
1784
  if (platform === "win32") {
1396
1785
  const posixShell = usesPosixShellSyntax(script) ? findWindowsPosixShell(env, fileExists) : null;
1397
1786
  if (posixShell) {
@@ -1436,10 +1825,10 @@ function findWindowsPosixShell(env, fileExists) {
1436
1825
  if (fileExists(candidate)) return candidate;
1437
1826
  }
1438
1827
  const pathValue = env.Path ?? env.PATH ?? "";
1439
- for (const dir of pathValue.split(path5.win32.delimiter)) {
1828
+ for (const dir of pathValue.split(path6.win32.delimiter)) {
1440
1829
  if (!dir) continue;
1441
1830
  for (const executable of POSIX_SHELL_CANDIDATES) {
1442
- const candidate = path5.win32.join(dir, executable);
1831
+ const candidate = path6.win32.join(dir, executable);
1443
1832
  if (fileExists(candidate) && !isWindowsWslLauncher(candidate)) {
1444
1833
  return candidate;
1445
1834
  }
@@ -1448,13 +1837,13 @@ function findWindowsPosixShell(env, fileExists) {
1448
1837
  return null;
1449
1838
  }
1450
1839
  function isWindowsWslLauncher(candidate) {
1451
- const normalized = path5.win32.normalize(candidate).toLowerCase();
1840
+ const normalized = path6.win32.normalize(candidate).toLowerCase();
1452
1841
  return normalized.endsWith("\\windows\\system32\\bash.exe") || normalized.endsWith("\\windows\\syswow64\\bash.exe");
1453
1842
  }
1454
1843
  function writeDiagnostic2(message) {
1455
1844
  const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1456
1845
  if (logPath) {
1457
- fs6.appendFileSync(logPath, message);
1846
+ fs7.appendFileSync(logPath, message);
1458
1847
  return;
1459
1848
  }
1460
1849
  process.stderr.write(message);
@@ -1601,9 +1990,9 @@ function getExecutor(taskType) {
1601
1990
 
1602
1991
  // src/scheduled-tasks/TaskConfigManager.ts
1603
1992
  var import_node_crypto = require("crypto");
1604
- var fs7 = __toESM(require("fs"));
1605
- var path6 = __toESM(require("path"));
1606
- var import_devtools_protocol7 = require("@serviceme/devtools-protocol");
1993
+ var fs8 = __toESM(require("fs"));
1994
+ var path7 = __toESM(require("path"));
1995
+ var import_devtools_protocol8 = require("@serviceme/devtools-protocol");
1607
1996
  var CONFIG_DIR3 = ".serviceme";
1608
1997
  var CONFIG_FILE = "scheduled-tasks.json";
1609
1998
  function emptyConfig() {
@@ -1614,7 +2003,7 @@ function isRecord(value) {
1614
2003
  }
1615
2004
  function requireNonEmptyString(payload, field, taskType) {
1616
2005
  if (!isRecord(payload) || typeof payload[field] !== "string" || !payload[field].trim()) {
1617
- throw (0, import_devtools_protocol7.createServicemeError)(
2006
+ throw (0, import_devtools_protocol8.createServicemeError)(
1618
2007
  "invalid_payload",
1619
2008
  `${taskType} payload ${field} is required`
1620
2009
  );
@@ -1639,27 +2028,27 @@ function validateTaskPayload(taskType, payload) {
1639
2028
  }
1640
2029
  var TaskConfigManager = class {
1641
2030
  constructor(workspacePath) {
1642
- this.configPath = path6.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
2031
+ this.configPath = path7.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1643
2032
  }
1644
2033
  getConfigPath() {
1645
2034
  return this.configPath;
1646
2035
  }
1647
2036
  readConfig() {
1648
- if (!fs7.existsSync(this.configPath)) {
2037
+ if (!fs8.existsSync(this.configPath)) {
1649
2038
  return emptyConfig();
1650
2039
  }
1651
- const raw = fs7.readFileSync(this.configPath, "utf-8");
2040
+ const raw = fs8.readFileSync(this.configPath, "utf-8");
1652
2041
  const parsed = JSON.parse(raw);
1653
2042
  return parsed;
1654
2043
  }
1655
2044
  writeConfig(config) {
1656
- const dir = path6.dirname(this.configPath);
1657
- if (!fs7.existsSync(dir)) {
1658
- fs7.mkdirSync(dir, { recursive: true });
2045
+ const dir = path7.dirname(this.configPath);
2046
+ if (!fs8.existsSync(dir)) {
2047
+ fs8.mkdirSync(dir, { recursive: true });
1659
2048
  }
1660
2049
  const tmp = `${this.configPath}.tmp`;
1661
- fs7.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1662
- fs7.renameSync(tmp, this.configPath);
2050
+ fs8.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
2051
+ fs8.renameSync(tmp, this.configPath);
1663
2052
  }
1664
2053
  listTasks() {
1665
2054
  return this.readConfig().tasks;
@@ -1930,8 +2319,8 @@ var TaskExecutionEngine = class {
1930
2319
 
1931
2320
  // src/scheduled-tasks/TaskLogManager.ts
1932
2321
  var import_node_crypto2 = require("crypto");
1933
- var fs8 = __toESM(require("fs"));
1934
- var path7 = __toESM(require("path"));
2322
+ var fs9 = __toESM(require("fs"));
2323
+ var path8 = __toESM(require("path"));
1935
2324
  var CONFIG_DIR4 = ".serviceme";
1936
2325
  var LOG_FILE2 = "scheduled-tasks-log.json";
1937
2326
  var MAX_LOGS = 200;
@@ -1956,17 +2345,17 @@ function validateAndRepairLogFile(raw) {
1956
2345
  }
1957
2346
  var TaskLogManager = class {
1958
2347
  constructor(workspacePath) {
1959
- this.logPath = path7.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
2348
+ this.logPath = path8.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1960
2349
  }
1961
2350
  getLogPath() {
1962
2351
  return this.logPath;
1963
2352
  }
1964
2353
  readLogFile() {
1965
- if (!fs8.existsSync(this.logPath)) {
2354
+ if (!fs9.existsSync(this.logPath)) {
1966
2355
  return emptyLogFile();
1967
2356
  }
1968
2357
  try {
1969
- const raw = fs8.readFileSync(this.logPath, "utf-8");
2358
+ const raw = fs9.readFileSync(this.logPath, "utf-8");
1970
2359
  const parsed = JSON.parse(raw);
1971
2360
  const file = validateAndRepairLogFile(parsed);
1972
2361
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
@@ -1982,21 +2371,21 @@ var TaskLogManager = class {
1982
2371
  }
1983
2372
  backupCorruptedFile() {
1984
2373
  try {
1985
- if (fs8.existsSync(this.logPath)) {
2374
+ if (fs9.existsSync(this.logPath)) {
1986
2375
  const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
1987
- fs8.copyFileSync(this.logPath, backupPath);
2376
+ fs9.copyFileSync(this.logPath, backupPath);
1988
2377
  }
1989
2378
  } catch {
1990
2379
  }
1991
2380
  }
1992
2381
  writeLogFile(file) {
1993
- const dir = path7.dirname(this.logPath);
1994
- if (!fs8.existsSync(dir)) {
1995
- fs8.mkdirSync(dir, { recursive: true });
2382
+ const dir = path8.dirname(this.logPath);
2383
+ if (!fs9.existsSync(dir)) {
2384
+ fs9.mkdirSync(dir, { recursive: true });
1996
2385
  }
1997
2386
  const tmp = `${this.logPath}.tmp`;
1998
- fs8.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1999
- fs8.renameSync(tmp, this.logPath);
2387
+ fs9.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
2388
+ fs9.renameSync(tmp, this.logPath);
2000
2389
  }
2001
2390
  appendLog(input) {
2002
2391
  const file = this.readLogFile();
@@ -2107,8 +2496,8 @@ var SchedulerDaemon = class {
2107
2496
  const configPath = this.configManager.getConfigPath();
2108
2497
  const dir = configPath.substring(0, configPath.lastIndexOf("/"));
2109
2498
  try {
2110
- if (fs9.existsSync(dir)) {
2111
- this.watcher = fs9.watch(dir, (_eventType, filename) => {
2499
+ if (fs10.existsSync(dir)) {
2500
+ this.watcher = fs10.watch(dir, (_eventType, filename) => {
2112
2501
  if (filename === "scheduled-tasks.json") {
2113
2502
  this.logger.log("info", "Config file changed, reconciling...");
2114
2503
  }
@@ -2260,8 +2649,215 @@ function matchCronField(field, value) {
2260
2649
  const values = field.split(",");
2261
2650
  return values.some((v) => Number.parseInt(v, 10) === value);
2262
2651
  }
2652
+
2653
+ // src/skills/SkillCatalogClient.ts
2654
+ var import_devtools_protocol9 = require("@serviceme/devtools-protocol");
2655
+ var SkillCatalogClient = class {
2656
+ constructor(options = {}) {
2657
+ this.fetchImpl = options.fetchImpl ?? fetch;
2658
+ this.baseUrl = options.baseUrl;
2659
+ }
2660
+ async getCatalog() {
2661
+ if (!this.baseUrl) {
2662
+ return {
2663
+ skills: [],
2664
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2665
+ };
2666
+ }
2667
+ const response = await this.fetchImpl(
2668
+ `${this.baseUrl}/api/v1/marketplace/skills`
2669
+ );
2670
+ if (!response.ok) {
2671
+ throw new Error(`Failed to fetch skills catalog: ${response.status}`);
2672
+ }
2673
+ const data = await response.json();
2674
+ return {
2675
+ skills: data.skills ?? [],
2676
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2677
+ };
2678
+ }
2679
+ async downloadSkill(remoteId) {
2680
+ if (!this.baseUrl) {
2681
+ throw (0, import_devtools_protocol9.createServicemeError)(
2682
+ "workspace_not_found",
2683
+ "Skill catalog baseUrl is not configured."
2684
+ );
2685
+ }
2686
+ const response = await this.fetchImpl(
2687
+ `${this.baseUrl}/api/v1/marketplace/skills/download/${remoteId}`
2688
+ );
2689
+ if (!response.ok) {
2690
+ if (response.status === 404) {
2691
+ throw (0, import_devtools_protocol9.createServicemeError)(
2692
+ "not_found",
2693
+ `Skill '${remoteId}' not found`
2694
+ );
2695
+ }
2696
+ throw new Error(
2697
+ `Failed to download skill ${remoteId}: ${response.status}`
2698
+ );
2699
+ }
2700
+ const payload = await response.json();
2701
+ return payload.data?.files ?? [];
2702
+ }
2703
+ };
2704
+
2705
+ // src/skills/SkillReconciler.ts
2706
+ var SkillReconciler = class {
2707
+ constructor(deps) {
2708
+ this.deps = deps;
2709
+ }
2710
+ async mutate(request) {
2711
+ if (request.targetScope !== "workspace" && request.targetScope !== "user") {
2712
+ throw new Error(`Invalid target scope: ${String(request.targetScope)}`);
2713
+ }
2714
+ if (request.action === "uninstall" || request.action === "move" || request.action === "removeExternal") {
2715
+ return {
2716
+ status: "success",
2717
+ changed: true,
2718
+ message: `Skill ${request.action} completed.`
2719
+ };
2720
+ }
2721
+ if (request.action !== "install") {
2722
+ return {
2723
+ status: "blocked",
2724
+ changed: false,
2725
+ message: `Skill action is not supported by bridge reconciler: ${request.action}`
2726
+ };
2727
+ }
2728
+ const catalog = await this.deps.catalogClient.getCatalog();
2729
+ const remoteSkill = catalog.skills.find(
2730
+ (skill) => this.deps.skillStore.normalizeRemoteSkillId(skill.id) === request.skillId
2731
+ );
2732
+ if (!remoteSkill) {
2733
+ return {
2734
+ status: "blocked",
2735
+ changed: false,
2736
+ message: "Skill not found in catalog."
2737
+ };
2738
+ }
2739
+ if (!request.confirmed && (remoteSkill.hasScripts || remoteSkill.hasHooks)) {
2740
+ return {
2741
+ status: "requires_confirmation",
2742
+ changed: false,
2743
+ hasScripts: Boolean(remoteSkill.hasScripts),
2744
+ hasHooks: Boolean(remoteSkill.hasHooks),
2745
+ message: "This skill contains executable scripts that require confirmation."
2746
+ };
2747
+ }
2748
+ return {
2749
+ status: "success",
2750
+ changed: true,
2751
+ message: "Skill installed."
2752
+ };
2753
+ }
2754
+ };
2755
+
2756
+ // src/skills/SkillStore.ts
2757
+ var fs11 = __toESM(require("fs/promises"));
2758
+ var path9 = __toESM(require("path"));
2759
+ var USER_SKILL_MARKER_FILE = ".ms-devtools-skill.json";
2760
+ var WORKSPACE_SKILLS_ROOT_RELATIVE = ".github/skills";
2761
+ var WORKSPACE_SKILLS_MARKER_RELATIVE = ".github/.ms-devtools-skills.yml";
2762
+ var SAFE_LOCAL_ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
2763
+ function assertSafeLocalSkillId(skillId) {
2764
+ if (typeof skillId !== "string" || skillId.length === 0 || skillId === "." || skillId === ".." || skillId.includes("/") || skillId.includes("\\") || !SAFE_LOCAL_ID_PATTERN2.test(skillId)) {
2765
+ throw new Error(`Invalid skill id: ${skillId}`);
2766
+ }
2767
+ return skillId;
2768
+ }
2769
+ var SkillStore = class {
2770
+ constructor(options) {
2771
+ this.workspacePath = options.workspacePath;
2772
+ this.userSkillsRoot = options.userSkillsRoot;
2773
+ this.fileSystem = options.fileSystem ?? fs11;
2774
+ }
2775
+ normalizeRemoteSkillId(remoteId) {
2776
+ if (remoteId.startsWith("official/")) {
2777
+ return assertSafeLocalSkillId(remoteId.slice("official/".length));
2778
+ }
2779
+ if (remoteId.startsWith("community/")) {
2780
+ const lastSlash = remoteId.lastIndexOf("/");
2781
+ return assertSafeLocalSkillId(remoteId.slice(lastSlash + 1));
2782
+ }
2783
+ return assertSafeLocalSkillId(remoteId);
2784
+ }
2785
+ getWorkspaceSkillPath(skillId) {
2786
+ return `${WORKSPACE_SKILLS_ROOT_RELATIVE}/${skillId}`;
2787
+ }
2788
+ getWorkspaceMarkerPath() {
2789
+ return WORKSPACE_SKILLS_MARKER_RELATIVE;
2790
+ }
2791
+ getUserSkillPath(skillId) {
2792
+ return path9.join(this.userSkillsRoot, skillId);
2793
+ }
2794
+ async listWorkspaceSkillIds() {
2795
+ const skillsRootPath = path9.join(
2796
+ this.workspacePath,
2797
+ WORKSPACE_SKILLS_ROOT_RELATIVE
2798
+ );
2799
+ try {
2800
+ const entries = await this.fileSystem.readdir(skillsRootPath, {
2801
+ withFileTypes: true
2802
+ });
2803
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
2804
+ } catch {
2805
+ return [];
2806
+ }
2807
+ }
2808
+ async listUserSkillIds() {
2809
+ try {
2810
+ const entries = await this.fileSystem.readdir(this.userSkillsRoot, {
2811
+ withFileTypes: true
2812
+ });
2813
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
2814
+ } catch {
2815
+ return [];
2816
+ }
2817
+ }
2818
+ async writeManagedUserSkillMarker(skillId) {
2819
+ const targetDir = this.getUserSkillPath(skillId);
2820
+ await this.fileSystem.mkdir(targetDir, { recursive: true });
2821
+ await this.fileSystem.writeFile(
2822
+ path9.join(targetDir, USER_SKILL_MARKER_FILE),
2823
+ JSON.stringify({ skillId, installedBy: "ms-devtools" }, null, 2),
2824
+ "utf-8"
2825
+ );
2826
+ }
2827
+ async isManagedUserSkill(skillId) {
2828
+ try {
2829
+ const marker = await this.fileSystem.readFile(
2830
+ path9.join(this.getUserSkillPath(skillId), USER_SKILL_MARKER_FILE),
2831
+ "utf-8"
2832
+ );
2833
+ const parsed = JSON.parse(marker);
2834
+ return parsed.skillId === skillId;
2835
+ } catch {
2836
+ return false;
2837
+ }
2838
+ }
2839
+ async writeSkillFiles(skillId, scope, files) {
2840
+ const root = scope === "workspace" ? path9.join(this.workspacePath, WORKSPACE_SKILLS_ROOT_RELATIVE) : this.userSkillsRoot;
2841
+ const targetDir = path9.join(root, skillId);
2842
+ await this.fileSystem.mkdir(targetDir, { recursive: true });
2843
+ for (const file of files) {
2844
+ const filePath = path9.join(targetDir, file.path);
2845
+ await this.fileSystem.mkdir(path9.dirname(filePath), { recursive: true });
2846
+ await this.fileSystem.writeFile(filePath, file.content, "utf-8");
2847
+ if (file.executable) {
2848
+ try {
2849
+ await this.fileSystem.chmod(filePath, 493);
2850
+ } catch {
2851
+ }
2852
+ }
2853
+ }
2854
+ }
2855
+ };
2263
2856
  // Annotate the CommonJS export names for ESM import in node:
2264
2857
  0 && (module.exports = {
2858
+ AgentCatalogClient,
2859
+ AgentReconciler,
2860
+ AgentStore,
2265
2861
  DaemonLogger,
2266
2862
  EnvironmentInspector,
2267
2863
  GithubCopilotCliExecutor,
@@ -2271,6 +2867,10 @@ function matchCronField(field, value) {
2271
2867
  ProjectTools,
2272
2868
  SchedulerDaemon,
2273
2869
  ShellExecutor,
2870
+ SkillCatalogClient,
2871
+ SkillReconciler,
2872
+ SkillStore,
2873
+ TOOL_RISK_MAP,
2274
2874
  TaskConfigManager,
2275
2875
  TaskExecutionEngine,
2276
2876
  TaskLogManager,
@@ -2288,6 +2888,7 @@ function matchCronField(field, value) {
2288
2888
  matchesCron,
2289
2889
  moveFiles,
2290
2890
  noopLogger,
2891
+ parseAgentToolPermissions,
2291
2892
  parseIntervalMs,
2292
2893
  resolveTaskExecutionPayload,
2293
2894
  unzipFile,