@serviceme/devtools-core 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -5,6 +5,287 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
+ // src/agents/AgentCatalogClient.ts
9
+ var AgentCatalogClient = class {
10
+ constructor(options = {}) {
11
+ this.fetchImpl = options.fetchImpl ?? fetch;
12
+ this.baseUrl = options.baseUrl;
13
+ }
14
+ async getCatalog() {
15
+ if (!this.baseUrl) {
16
+ return {
17
+ agents: [],
18
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
19
+ };
20
+ }
21
+ const response = await this.fetchImpl(
22
+ `${this.baseUrl}/api/v1/marketplace/agents`
23
+ );
24
+ if (!response.ok) {
25
+ throw new Error(`Failed to fetch agents catalog: ${response.status}`);
26
+ }
27
+ const data = await response.json();
28
+ return {
29
+ agents: data.agents ?? [],
30
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
31
+ };
32
+ }
33
+ };
34
+
35
+ // src/permissions/agent-permissions.ts
36
+ var TOOL_RISK_MAP = {
37
+ shell: "high",
38
+ terminal: "high",
39
+ run_in_terminal: "high",
40
+ execution_subagent: "high",
41
+ filesystem: "medium",
42
+ fetch: "medium",
43
+ fetch_webpage: "medium",
44
+ create_file: "medium",
45
+ replace_string_in_file: "medium",
46
+ multi_replace_string_in_file: "medium",
47
+ read_file: "low",
48
+ search: "low",
49
+ grep_search: "low",
50
+ file_search: "low",
51
+ semantic_search: "low",
52
+ list_dir: "low"
53
+ };
54
+ var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---/;
55
+ var TOOLS_LINE_REGEX = /^tools:\s*$/m;
56
+ var TOOLS_INLINE_REGEX = /^tools:\s*\[([^\]]*)\]/m;
57
+ var LIST_ITEM_REGEX = /^\s*-\s+(.+)$/;
58
+ function parseAgentToolPermissions(content) {
59
+ const fmMatch = content.match(FRONTMATTER_REGEX);
60
+ if (!fmMatch?.[1]) return [];
61
+ const frontmatter = fmMatch[1];
62
+ const inlineMatch = frontmatter.match(TOOLS_INLINE_REGEX);
63
+ if (inlineMatch?.[1] != null) {
64
+ const raw = inlineMatch[1];
65
+ return raw.split(",").map((t) => t.trim()).filter(Boolean).map((tool) => ({
66
+ tool,
67
+ riskLevel: TOOL_RISK_MAP[tool] ?? "medium"
68
+ }));
69
+ }
70
+ const blockMatch = frontmatter.match(TOOLS_LINE_REGEX);
71
+ if (!blockMatch?.[0]) return [];
72
+ const toolsStartIndex = frontmatter.indexOf(blockMatch[0]) + blockMatch[0].length;
73
+ const remaining = frontmatter.slice(toolsStartIndex);
74
+ const lines = remaining.split(/\r?\n/);
75
+ const tools = [];
76
+ for (const line of lines) {
77
+ const itemMatch = line.match(LIST_ITEM_REGEX);
78
+ if (itemMatch?.[1]) {
79
+ const tool = itemMatch[1].trim();
80
+ tools.push({ tool, riskLevel: TOOL_RISK_MAP[tool] ?? "medium" });
81
+ } else if (line.trim() !== "" && !line.startsWith(" ") && !line.startsWith(" ")) {
82
+ break;
83
+ }
84
+ }
85
+ return tools;
86
+ }
87
+
88
+ // src/agents/AgentReconciler.ts
89
+ var AgentReconciler = class {
90
+ constructor(deps) {
91
+ this.deps = deps;
92
+ }
93
+ async mutate(request) {
94
+ if (request.targetScope !== "workspace" && request.targetScope !== "user") {
95
+ throw new Error(`Invalid target scope: ${String(request.targetScope)}`);
96
+ }
97
+ if (request.action === "uninstall" || request.action === "move" || request.action === "removeExternal") {
98
+ return {
99
+ status: "success",
100
+ changed: true,
101
+ message: `Agent ${request.action} completed.`
102
+ };
103
+ }
104
+ if (request.action !== "install") {
105
+ return {
106
+ status: "blocked",
107
+ changed: false,
108
+ message: `Agent action is not supported by bridge reconciler: ${request.action}`
109
+ };
110
+ }
111
+ const catalog = await this.deps.catalogClient.getCatalog();
112
+ const remoteAgent = catalog.agents.find(
113
+ (agent) => this.deps.agentStore.normalizeRemoteAgentId(agent.id) === request.agentId
114
+ );
115
+ if (!remoteAgent) {
116
+ return {
117
+ status: "blocked",
118
+ changed: false,
119
+ message: "Agent not found in catalog."
120
+ };
121
+ }
122
+ if (!request.confirmed && this.hasHighRiskTool(remoteAgent.tools)) {
123
+ return {
124
+ status: "requires_confirmation",
125
+ changed: false,
126
+ message: "This agent uses high-risk tools that require confirmation.",
127
+ tools: remoteAgent.tools
128
+ };
129
+ }
130
+ return {
131
+ status: "success",
132
+ changed: true,
133
+ message: "Agent installed."
134
+ };
135
+ }
136
+ getPermissionSummary(agentId, agentName, content) {
137
+ const tools = parseAgentToolPermissions(content);
138
+ return {
139
+ agentId,
140
+ agentName,
141
+ tools,
142
+ highRiskCount: tools.filter((tool) => tool.riskLevel === "high").length,
143
+ mediumRiskCount: tools.filter((tool) => tool.riskLevel === "medium").length,
144
+ lowRiskCount: tools.filter((tool) => tool.riskLevel === "low").length
145
+ };
146
+ }
147
+ hasHighRiskTool(tools) {
148
+ return tools.some((tool) => TOOL_RISK_MAP[tool] === "high");
149
+ }
150
+ };
151
+
152
+ // src/agents/AgentStore.ts
153
+ import * as fs from "fs/promises";
154
+ import * as path from "path";
155
+ var WORKSPACE_AGENTS_ROOT_RELATIVE = ".github/agents";
156
+ var WORKSPACE_AGENTS_STATE_RELATIVE = ".github/.ms-devtools-agents.yml";
157
+ var SAFE_LOCAL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
158
+ function assertSafeLocalAgentId(agentId) {
159
+ if (typeof agentId !== "string" || agentId.length === 0 || agentId === "." || agentId === ".." || agentId.includes("/") || agentId.includes("\\") || !SAFE_LOCAL_ID_PATTERN.test(agentId)) {
160
+ throw new Error(`Invalid agent id: ${agentId}`);
161
+ }
162
+ return agentId;
163
+ }
164
+ var AgentStore = class {
165
+ constructor(options) {
166
+ this.workspacePath = options.workspacePath;
167
+ this.userAgentsRoot = options.userAgentsRoot;
168
+ this.fileSystem = options.fileSystem ?? fs;
169
+ this.schemaVersion = options.schemaVersion ?? 1;
170
+ }
171
+ normalizeRemoteAgentId(remoteId) {
172
+ if (remoteId.startsWith("official/")) {
173
+ return assertSafeLocalAgentId(remoteId.slice("official/".length));
174
+ }
175
+ if (remoteId.startsWith("community/")) {
176
+ const lastSlash = remoteId.lastIndexOf("/");
177
+ return assertSafeLocalAgentId(remoteId.slice(lastSlash + 1));
178
+ }
179
+ return assertSafeLocalAgentId(remoteId);
180
+ }
181
+ getWorkspaceAgentsRootPath() {
182
+ return WORKSPACE_AGENTS_ROOT_RELATIVE;
183
+ }
184
+ getWorkspaceStateFilePath() {
185
+ return WORKSPACE_AGENTS_STATE_RELATIVE;
186
+ }
187
+ getUserAgentsRootPath() {
188
+ return this.userAgentsRoot;
189
+ }
190
+ async listWorkspaceAgentIds() {
191
+ return this.listAgentIds(
192
+ path.join(this.workspacePath, WORKSPACE_AGENTS_ROOT_RELATIVE)
193
+ );
194
+ }
195
+ async listUserAgentIds() {
196
+ return this.listAgentIds(this.userAgentsRoot);
197
+ }
198
+ async readState() {
199
+ try {
200
+ const raw = await this.fileSystem.readFile(
201
+ path.join(this.workspacePath, WORKSPACE_AGENTS_STATE_RELATIVE),
202
+ "utf-8"
203
+ );
204
+ const parsed = JSON.parse(raw);
205
+ if (typeof parsed.schemaVersion !== "number" || !Array.isArray(parsed.installedAgents)) {
206
+ return null;
207
+ }
208
+ return parsed;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ async writeState(state) {
214
+ const statePath = path.join(
215
+ this.workspacePath,
216
+ WORKSPACE_AGENTS_STATE_RELATIVE
217
+ );
218
+ await this.fileSystem.mkdir(path.dirname(statePath), { recursive: true });
219
+ await this.fileSystem.writeFile(
220
+ statePath,
221
+ JSON.stringify(state, null, 2),
222
+ "utf-8"
223
+ );
224
+ }
225
+ async addInstalledAgent(entry) {
226
+ const state = await this.readState() ?? {
227
+ schemaVersion: this.schemaVersion,
228
+ installedAgents: []
229
+ };
230
+ state.installedAgents = state.installedAgents.filter(
231
+ (agent) => agent.id !== entry.id
232
+ );
233
+ state.installedAgents.push(entry);
234
+ await this.writeState(state);
235
+ }
236
+ async removeInstalledAgent(agentId) {
237
+ const state = await this.readState();
238
+ if (!state) {
239
+ return;
240
+ }
241
+ state.installedAgents = state.installedAgents.filter(
242
+ (agent) => agent.id !== agentId
243
+ );
244
+ await this.writeState(state);
245
+ }
246
+ async writeAgentFiles(agentId, scope, files) {
247
+ const root = scope === "workspace" ? path.join(this.workspacePath, WORKSPACE_AGENTS_ROOT_RELATIVE) : this.userAgentsRoot;
248
+ const firstFile = files[0];
249
+ const isSingleFlatFile = files.length === 1 && firstFile !== void 0 && firstFile.path === `${agentId}.agent.md` && !firstFile.path.includes("/");
250
+ const targetDir = isSingleFlatFile ? root : path.join(root, agentId);
251
+ await this.fileSystem.mkdir(targetDir, { recursive: true });
252
+ for (const file of files) {
253
+ const filePath = path.join(targetDir, file.path);
254
+ await this.fileSystem.mkdir(path.dirname(filePath), { recursive: true });
255
+ await this.fileSystem.writeFile(filePath, file.content, "utf-8");
256
+ if (file.executable) {
257
+ try {
258
+ await this.fileSystem.chmod(filePath, 493);
259
+ } catch {
260
+ }
261
+ }
262
+ }
263
+ }
264
+ async listAgentIds(dir) {
265
+ try {
266
+ const entries = await this.fileSystem.readdir(dir, {
267
+ withFileTypes: true
268
+ });
269
+ const ids = [];
270
+ for (const entry of entries) {
271
+ if (entry.name.startsWith(".")) {
272
+ continue;
273
+ }
274
+ if (entry.isDirectory()) {
275
+ ids.push(entry.name);
276
+ continue;
277
+ }
278
+ if (entry.isFile() && entry.name.endsWith(".agent.md")) {
279
+ ids.push(entry.name.replace(/\.agent\.md$/, ""));
280
+ }
281
+ }
282
+ return ids.sort();
283
+ } catch {
284
+ return [];
285
+ }
286
+ }
287
+ };
288
+
8
289
  // src/copilot/doctor.ts
9
290
  import {
10
291
  COPILOT_ERROR_CODES,
@@ -248,7 +529,13 @@ var DEFAULT_TOOL_CHECK_TIMEOUT_MS = 5e3;
248
529
  var TOOL_CHECK_TIMEOUT_MS = {
249
530
  nuget: 12e3,
250
531
  nvm: 8e3,
251
- dotnet: 8e3
532
+ dotnet: 8e3,
533
+ // nvm4w/npm shims and globally-installed .cmd tools (pnpm, nrm) are slow to
534
+ // resolve on cold PATHs. Give them extra headroom so the version probe
535
+ // doesn't fall through to the generic 5s default.
536
+ npm: 15e3,
537
+ pnpm: 15e3,
538
+ nrm: 15e3
252
539
  };
253
540
  var ERROR_CODE_NOT_FOUND = 127;
254
541
  var ERROR_CODE_TIMEOUT = "ETIMEDOUT";
@@ -295,8 +582,29 @@ var EnvironmentInspector = class {
295
582
  return void 0;
296
583
  }
297
584
  }
585
+ /**
586
+ * On Windows, prefer the `.cmd` shim for npm/pnpm/nrm. nvm4w registers
587
+ * BOTH an extensionless entry (pointing to node.exe) and a `<tool>.cmd`
588
+ * wrapper; `where` returns them in that order, and the extensionless one
589
+ * would just print node's own version.
590
+ */
591
+ async getToolShimPath(toolName) {
592
+ try {
593
+ const result = await runCommand("where", {
594
+ args: [toolName],
595
+ timeoutMs: this.getToolTimeout(toolName)
596
+ });
597
+ const candidates = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
598
+ const cmdShim = candidates.find(
599
+ (line) => line.toLowerCase().endsWith(".cmd")
600
+ );
601
+ return cmdShim ?? candidates[0];
602
+ } catch {
603
+ return void 0;
604
+ }
605
+ }
298
606
  async getToolVersion(toolName) {
299
- const invocation = this.getVersionInvocation(toolName);
607
+ const invocation = await this.getVersionInvocation(toolName);
300
608
  const result = await runCommand(invocation.command, {
301
609
  args: invocation.args,
302
610
  timeoutMs: this.getToolTimeout(toolName)
@@ -371,12 +679,19 @@ var EnvironmentInspector = class {
371
679
  return this.handleToolCheckError(error);
372
680
  }
373
681
  }
374
- getVersionInvocation(toolName) {
682
+ async getVersionInvocation(toolName) {
375
683
  const isWindows = process.platform === "win32";
376
684
  if (isWindows && ["npm", "pnpm", "nrm"].includes(toolName)) {
685
+ const shim = await this.getToolShimPath(toolName);
686
+ if (shim) {
687
+ return {
688
+ command: "cmd.exe",
689
+ args: ["/c", shim, "--version"]
690
+ };
691
+ }
377
692
  return {
378
693
  command: "cmd.exe",
379
- args: ["/d", "/s", "/c", `${toolName} --version`]
694
+ args: ["/c", toolName, "--version"]
380
695
  };
381
696
  }
382
697
  const commands = {
@@ -429,8 +744,8 @@ var EnvironmentInspector = class {
429
744
  };
430
745
 
431
746
  // src/image/imageTools.ts
432
- import * as fs from "fs/promises";
433
- import * as path from "path";
747
+ import * as fs2 from "fs/promises";
748
+ import * as path2 from "path";
434
749
  import {
435
750
  createServicemeError as createServicemeError4
436
751
  } from "@serviceme/devtools-protocol";
@@ -453,7 +768,7 @@ var ImageTools = class {
453
768
  if (!await this.pathExists(filePath)) {
454
769
  return { valid: false };
455
770
  }
456
- if (!SUPPORTED_FORMATS.includes(path.extname(filePath).toLowerCase())) {
771
+ if (!SUPPORTED_FORMATS.includes(path2.extname(filePath).toLowerCase())) {
457
772
  return { valid: false };
458
773
  }
459
774
  await this.getInfo(filePath, sharpModulePath);
@@ -465,7 +780,7 @@ var ImageTools = class {
465
780
  async getInfo(imagePath, sharpModulePath) {
466
781
  const sharp = this.loadSharp(sharpModulePath);
467
782
  const metadata = await sharp(imagePath).metadata();
468
- const stats = await fs.stat(imagePath);
783
+ const stats = await fs2.stat(imagePath);
469
784
  return {
470
785
  width: metadata.width ?? 0,
471
786
  height: metadata.height ?? 0,
@@ -482,7 +797,7 @@ var ImageTools = class {
482
797
  `Invalid image file: ${imagePath}`
483
798
  );
484
799
  }
485
- const originalStats = await fs.stat(imagePath);
800
+ const originalStats = await fs2.stat(imagePath);
486
801
  const originalSize = originalStats.size;
487
802
  const outputPath = this.getOutputPath(imagePath, options);
488
803
  const compressedBuffer = await this.compressWithSharp(imagePath, options);
@@ -497,7 +812,7 @@ var ImageTools = class {
497
812
  outputPath: imagePath
498
813
  };
499
814
  }
500
- await fs.writeFile(outputPath, compressedBuffer);
815
+ await fs2.writeFile(outputPath, compressedBuffer);
501
816
  return {
502
817
  originalSize,
503
818
  compressedSize,
@@ -508,7 +823,7 @@ var ImageTools = class {
508
823
  async compressWithSharp(imagePath, options) {
509
824
  const sharp = this.loadSharp(options.sharpModulePath);
510
825
  let pipeline = sharp(imagePath);
511
- switch (options.format ?? path.extname(imagePath).toLowerCase().slice(1)) {
826
+ switch (options.format ?? path2.extname(imagePath).toLowerCase().slice(1)) {
512
827
  case "jpg":
513
828
  case "jpeg":
514
829
  pipeline = pipeline.jpeg({ quality: options.quality });
@@ -534,14 +849,14 @@ var ImageTools = class {
534
849
  if (options.replaceOriginImage) {
535
850
  return inputPath;
536
851
  }
537
- const dir = path.dirname(inputPath);
538
- const ext = path.extname(inputPath);
539
- const name = path.basename(inputPath, ext);
540
- return path.join(dir, `${name}_compressed${ext}`);
852
+ const dir = path2.dirname(inputPath);
853
+ const ext = path2.extname(inputPath);
854
+ const name = path2.basename(inputPath, ext);
855
+ return path2.join(dir, `${name}_compressed${ext}`);
541
856
  }
542
857
  async pathExists(targetPath) {
543
858
  try {
544
- await fs.access(targetPath);
859
+ await fs2.access(targetPath);
545
860
  return true;
546
861
  } catch {
547
862
  return false;
@@ -700,8 +1015,8 @@ function createConsoleLogger(prefix = "serviceme") {
700
1015
  }
701
1016
 
702
1017
  // src/project/projectTools.ts
703
- import * as fs2 from "fs/promises";
704
- import * as path2 from "path";
1018
+ import * as fs3 from "fs/promises";
1019
+ import * as path3 from "path";
705
1020
  import {
706
1021
  createServicemeError as createServicemeError6
707
1022
  } from "@serviceme/devtools-protocol";
@@ -717,7 +1032,7 @@ import {
717
1032
  rename,
718
1033
  rm
719
1034
  } from "fs/promises";
720
- import { dirname as dirname2, join as join2 } from "path";
1035
+ import { dirname as dirname3, join as join3 } from "path";
721
1036
  import { open } from "yauzl";
722
1037
  var unzipFile = (zipPath, dest) => {
723
1038
  return new Promise((resolve, reject) => {
@@ -730,12 +1045,12 @@ var unzipFile = (zipPath, dest) => {
730
1045
  zipfile.readEntry();
731
1046
  zipfile.on("entry", (entry) => {
732
1047
  if (/\/$/.test(entry.fileName)) {
733
- void mkdir(join2(dest, entry.fileName), { recursive: true }).then(() => {
1048
+ void mkdir(join3(dest, entry.fileName), { recursive: true }).then(() => {
734
1049
  zipfile.readEntry();
735
1050
  }).catch(reject);
736
1051
  } else {
737
- const outputPath = join2(dest, entry.fileName);
738
- void mkdir(dirname2(outputPath), { recursive: true }).then(() => {
1052
+ const outputPath = join3(dest, entry.fileName);
1053
+ void mkdir(dirname3(outputPath), { recursive: true }).then(() => {
739
1054
  zipfile.openReadStream(
740
1055
  entry,
741
1056
  (streamError, readStream) => {
@@ -788,8 +1103,8 @@ var mergeEntry = async (sourcePath, destPath, overwrite) => {
788
1103
  const children = await readdir(sourcePath);
789
1104
  for (const child of children) {
790
1105
  await mergeEntry(
791
- join2(sourcePath, child),
792
- join2(destPath, child),
1106
+ join3(sourcePath, child),
1107
+ join3(destPath, child),
793
1108
  overwrite
794
1109
  );
795
1110
  }
@@ -814,8 +1129,8 @@ var moveFiles = async (sourceDir, destDir, overwrite = false) => {
814
1129
  await mkdir(destDir, { recursive: true });
815
1130
  const files = await readdir(sourceDir);
816
1131
  for (const file of files) {
817
- const sourceFile = join2(sourceDir, file);
818
- const destFile = join2(destDir, file);
1132
+ const sourceFile = join3(sourceDir, file);
1133
+ const destFile = join3(destDir, file);
819
1134
  if (!overwrite) {
820
1135
  try {
821
1136
  await access2(destFile, constants.F_OK);
@@ -832,10 +1147,10 @@ var moveFiles = async (sourceDir, destDir, overwrite = false) => {
832
1147
  var ProjectTools = class {
833
1148
  async extractTemplate(zipPath, workspacePath, tempExtractDir, input) {
834
1149
  await unzipFile(zipPath, tempExtractDir);
835
- let sourceDir = path2.join(tempExtractDir, input.extractedDirName);
1150
+ let sourceDir = path3.join(tempExtractDir, input.extractedDirName);
836
1151
  let actualDirName = input.extractedDirName;
837
1152
  if (!await this.pathExists(sourceDir)) {
838
- const entries = await fs2.readdir(tempExtractDir, { withFileTypes: true });
1153
+ const entries = await fs3.readdir(tempExtractDir, { withFileTypes: true });
839
1154
  const directories = entries.filter(
840
1155
  (entry) => entry.isDirectory() && !entry.name.startsWith(".")
841
1156
  );
@@ -847,7 +1162,7 @@ var ProjectTools = class {
847
1162
  );
848
1163
  if (selectedDirectory) {
849
1164
  actualDirName = selectedDirectory;
850
- sourceDir = path2.join(tempExtractDir, actualDirName);
1165
+ sourceDir = path3.join(tempExtractDir, actualDirName);
851
1166
  } else if (directories.length === 0) {
852
1167
  throw new Error(
853
1168
  `No directory found after extraction. Expected directory: ${input.extractedDirName}`
@@ -905,7 +1220,7 @@ var ProjectTools = class {
905
1220
  } else {
906
1221
  for (const scriptPath of scripts) {
907
1222
  try {
908
- await fs2.chmod(scriptPath, 493);
1223
+ await fs3.chmod(scriptPath, 493);
909
1224
  updatedCount += 1;
910
1225
  } catch {
911
1226
  }
@@ -953,12 +1268,12 @@ var ProjectTools = class {
953
1268
  const results = [];
954
1269
  let entries;
955
1270
  try {
956
- entries = await fs2.readdir(dir, { withFileTypes: true });
1271
+ entries = await fs3.readdir(dir, { withFileTypes: true });
957
1272
  } catch {
958
1273
  return results;
959
1274
  }
960
1275
  for (const entry of entries) {
961
- const fullPath = path2.join(dir, entry.name);
1276
+ const fullPath = path3.join(dir, entry.name);
962
1277
  if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
963
1278
  results.push(...await this.findScripts(fullPath, extensions));
964
1279
  } else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
@@ -969,7 +1284,7 @@ var ProjectTools = class {
969
1284
  }
970
1285
  async pathExists(targetPath) {
971
1286
  try {
972
- await fs2.access(targetPath);
1287
+ await fs3.access(targetPath);
973
1288
  return true;
974
1289
  } catch {
975
1290
  return false;
@@ -988,7 +1303,7 @@ var ProjectTools = class {
988
1303
  const matches = [];
989
1304
  for (const directoryName of directoryNames) {
990
1305
  if (await this.directoryMatchesProjectPattern(
991
- path2.join(tempExtractDir, directoryName),
1306
+ path3.join(tempExtractDir, directoryName),
992
1307
  projectFilePattern
993
1308
  )) {
994
1309
  matches.push(directoryName);
@@ -1000,7 +1315,7 @@ var ProjectTools = class {
1000
1315
  return null;
1001
1316
  }
1002
1317
  async directoryMatchesProjectPattern(directoryPath, projectFilePattern) {
1003
- const entries = await fs2.readdir(directoryPath);
1318
+ const entries = await fs3.readdir(directoryPath);
1004
1319
  if (projectFilePattern.includes("*")) {
1005
1320
  const regex = new RegExp(`^${projectFilePattern.replace("*", ".*")}$`);
1006
1321
  return entries.some((entry) => regex.test(entry));
@@ -1013,17 +1328,17 @@ function createProjectTools() {
1013
1328
  }
1014
1329
 
1015
1330
  // src/scheduled-tasks/daemon/DaemonLogger.ts
1016
- import * as fs3 from "fs";
1017
- import * as path3 from "path";
1331
+ import * as fs4 from "fs";
1332
+ import * as path4 from "path";
1018
1333
  var CONFIG_DIR = ".serviceme";
1019
1334
  var LOG_FILE = "scheduler.log";
1020
1335
  var MAX_LOG_SIZE = 1024 * 1024;
1021
1336
  var DaemonLogger = class {
1022
1337
  constructor(workspacePath) {
1023
- this.logPath = path3.join(workspacePath, CONFIG_DIR, LOG_FILE);
1024
- const dir = path3.dirname(this.logPath);
1025
- if (!fs3.existsSync(dir)) {
1026
- fs3.mkdirSync(dir, { recursive: true });
1338
+ this.logPath = path4.join(workspacePath, CONFIG_DIR, LOG_FILE);
1339
+ const dir = path4.dirname(this.logPath);
1340
+ if (!fs4.existsSync(dir)) {
1341
+ fs4.mkdirSync(dir, { recursive: true });
1027
1342
  }
1028
1343
  }
1029
1344
  getLogPath() {
@@ -1034,16 +1349,16 @@ var DaemonLogger = class {
1034
1349
  const line = `[${ts}] [${level.toUpperCase()}] ${message}
1035
1350
  `;
1036
1351
  this.rotateIfNeeded();
1037
- fs3.appendFileSync(this.logPath, line, "utf-8");
1352
+ fs4.appendFileSync(this.logPath, line, "utf-8");
1038
1353
  }
1039
1354
  rotateIfNeeded() {
1040
1355
  try {
1041
- const stats = fs3.statSync(this.logPath);
1356
+ const stats = fs4.statSync(this.logPath);
1042
1357
  if (stats.size > MAX_LOG_SIZE) {
1043
- const content = fs3.readFileSync(this.logPath, "utf-8");
1358
+ const content = fs4.readFileSync(this.logPath, "utf-8");
1044
1359
  const halfIdx = content.indexOf("\n", Math.floor(content.length / 2));
1045
1360
  if (halfIdx > 0) {
1046
- fs3.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
1361
+ fs4.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
1047
1362
  }
1048
1363
  }
1049
1364
  } catch {
@@ -1052,33 +1367,33 @@ var DaemonLogger = class {
1052
1367
  };
1053
1368
 
1054
1369
  // src/scheduled-tasks/daemon/PidManager.ts
1055
- import * as fs4 from "fs";
1056
- import * as path4 from "path";
1370
+ import * as fs5 from "fs";
1371
+ import * as path5 from "path";
1057
1372
  var CONFIG_DIR2 = ".serviceme";
1058
1373
  var PID_FILE = "scheduler.pid";
1059
1374
  var PidManager = class {
1060
1375
  constructor(workspacePath) {
1061
- this.pidPath = path4.join(workspacePath, CONFIG_DIR2, PID_FILE);
1376
+ this.pidPath = path5.join(workspacePath, CONFIG_DIR2, PID_FILE);
1062
1377
  }
1063
1378
  getPidPath() {
1064
1379
  return this.pidPath;
1065
1380
  }
1066
1381
  writePid(pid) {
1067
- const dir = path4.dirname(this.pidPath);
1068
- if (!fs4.existsSync(dir)) {
1069
- fs4.mkdirSync(dir, { recursive: true });
1382
+ const dir = path5.dirname(this.pidPath);
1383
+ if (!fs5.existsSync(dir)) {
1384
+ fs5.mkdirSync(dir, { recursive: true });
1070
1385
  }
1071
- fs4.writeFileSync(this.pidPath, String(pid), "utf-8");
1386
+ fs5.writeFileSync(this.pidPath, String(pid), "utf-8");
1072
1387
  }
1073
1388
  readPid() {
1074
- if (!fs4.existsSync(this.pidPath)) return null;
1075
- const raw = fs4.readFileSync(this.pidPath, "utf-8").trim();
1389
+ if (!fs5.existsSync(this.pidPath)) return null;
1390
+ const raw = fs5.readFileSync(this.pidPath, "utf-8").trim();
1076
1391
  const pid = Number.parseInt(raw, 10);
1077
1392
  return Number.isNaN(pid) ? null : pid;
1078
1393
  }
1079
1394
  removePid() {
1080
- if (fs4.existsSync(this.pidPath)) {
1081
- fs4.unlinkSync(this.pidPath);
1395
+ if (fs5.existsSync(this.pidPath)) {
1396
+ fs5.unlinkSync(this.pidPath);
1082
1397
  }
1083
1398
  }
1084
1399
  isProcessRunning(pid) {
@@ -1099,12 +1414,12 @@ var PidManager = class {
1099
1414
  };
1100
1415
 
1101
1416
  // src/scheduled-tasks/daemon/SchedulerDaemon.ts
1102
- import * as fs9 from "fs";
1417
+ import * as fs10 from "fs";
1103
1418
  import * as os from "os";
1104
1419
 
1105
1420
  // src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
1106
1421
  import { spawn as spawn2 } from "child_process";
1107
- import * as fs5 from "fs";
1422
+ import * as fs6 from "fs";
1108
1423
 
1109
1424
  // src/scheduled-tasks/executors/timeout.ts
1110
1425
  function resolveConfiguredTimeoutMs(timeoutSeconds, defaultTimeoutMs) {
@@ -1139,7 +1454,7 @@ function redactArgs(args) {
1139
1454
  function writeDiagnostic(message) {
1140
1455
  const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1141
1456
  if (logPath) {
1142
- fs5.appendFileSync(logPath, message);
1457
+ fs6.appendFileSync(logPath, message);
1143
1458
  return;
1144
1459
  }
1145
1460
  process.stderr.write(message);
@@ -1349,15 +1664,15 @@ ${body}`.trim()
1349
1664
 
1350
1665
  // src/scheduled-tasks/executors/ShellExecutor.ts
1351
1666
  import { spawn as spawn3 } from "child_process";
1352
- import * as fs6 from "fs";
1353
- import * as path5 from "path";
1667
+ import * as fs7 from "fs";
1668
+ import * as path6 from "path";
1354
1669
  var MAX_OUTPUT_BYTES2 = 1024 * 1024;
1355
1670
  var DEFAULT_TIMEOUT_MS4 = 6e4;
1356
1671
  var POSIX_SHELL_CANDIDATES = ["bash.exe", "sh.exe"];
1357
1672
  function resolveShellExecution(script, options = {}) {
1358
1673
  const platform = options.platform ?? process.platform;
1359
1674
  const env = options.env ?? process.env;
1360
- const fileExists = options.fileExists ?? fs6.existsSync;
1675
+ const fileExists = options.fileExists ?? fs7.existsSync;
1361
1676
  if (platform === "win32") {
1362
1677
  const posixShell = usesPosixShellSyntax(script) ? findWindowsPosixShell(env, fileExists) : null;
1363
1678
  if (posixShell) {
@@ -1402,10 +1717,10 @@ function findWindowsPosixShell(env, fileExists) {
1402
1717
  if (fileExists(candidate)) return candidate;
1403
1718
  }
1404
1719
  const pathValue = env.Path ?? env.PATH ?? "";
1405
- for (const dir of pathValue.split(path5.win32.delimiter)) {
1720
+ for (const dir of pathValue.split(path6.win32.delimiter)) {
1406
1721
  if (!dir) continue;
1407
1722
  for (const executable of POSIX_SHELL_CANDIDATES) {
1408
- const candidate = path5.win32.join(dir, executable);
1723
+ const candidate = path6.win32.join(dir, executable);
1409
1724
  if (fileExists(candidate) && !isWindowsWslLauncher(candidate)) {
1410
1725
  return candidate;
1411
1726
  }
@@ -1414,13 +1729,13 @@ function findWindowsPosixShell(env, fileExists) {
1414
1729
  return null;
1415
1730
  }
1416
1731
  function isWindowsWslLauncher(candidate) {
1417
- const normalized = path5.win32.normalize(candidate).toLowerCase();
1732
+ const normalized = path6.win32.normalize(candidate).toLowerCase();
1418
1733
  return normalized.endsWith("\\windows\\system32\\bash.exe") || normalized.endsWith("\\windows\\syswow64\\bash.exe");
1419
1734
  }
1420
1735
  function writeDiagnostic2(message) {
1421
1736
  const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1422
1737
  if (logPath) {
1423
- fs6.appendFileSync(logPath, message);
1738
+ fs7.appendFileSync(logPath, message);
1424
1739
  return;
1425
1740
  }
1426
1741
  process.stderr.write(message);
@@ -1567,8 +1882,8 @@ function getExecutor(taskType) {
1567
1882
 
1568
1883
  // src/scheduled-tasks/TaskConfigManager.ts
1569
1884
  import { randomUUID } from "crypto";
1570
- import * as fs7 from "fs";
1571
- import * as path6 from "path";
1885
+ import * as fs8 from "fs";
1886
+ import * as path7 from "path";
1572
1887
  import { createServicemeError as createServicemeError7 } from "@serviceme/devtools-protocol";
1573
1888
  var CONFIG_DIR3 = ".serviceme";
1574
1889
  var CONFIG_FILE = "scheduled-tasks.json";
@@ -1605,27 +1920,27 @@ function validateTaskPayload(taskType, payload) {
1605
1920
  }
1606
1921
  var TaskConfigManager = class {
1607
1922
  constructor(workspacePath) {
1608
- this.configPath = path6.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1923
+ this.configPath = path7.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1609
1924
  }
1610
1925
  getConfigPath() {
1611
1926
  return this.configPath;
1612
1927
  }
1613
1928
  readConfig() {
1614
- if (!fs7.existsSync(this.configPath)) {
1929
+ if (!fs8.existsSync(this.configPath)) {
1615
1930
  return emptyConfig();
1616
1931
  }
1617
- const raw = fs7.readFileSync(this.configPath, "utf-8");
1932
+ const raw = fs8.readFileSync(this.configPath, "utf-8");
1618
1933
  const parsed = JSON.parse(raw);
1619
1934
  return parsed;
1620
1935
  }
1621
1936
  writeConfig(config) {
1622
- const dir = path6.dirname(this.configPath);
1623
- if (!fs7.existsSync(dir)) {
1624
- fs7.mkdirSync(dir, { recursive: true });
1937
+ const dir = path7.dirname(this.configPath);
1938
+ if (!fs8.existsSync(dir)) {
1939
+ fs8.mkdirSync(dir, { recursive: true });
1625
1940
  }
1626
1941
  const tmp = `${this.configPath}.tmp`;
1627
- fs7.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1628
- fs7.renameSync(tmp, this.configPath);
1942
+ fs8.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1943
+ fs8.renameSync(tmp, this.configPath);
1629
1944
  }
1630
1945
  listTasks() {
1631
1946
  return this.readConfig().tasks;
@@ -1896,8 +2211,8 @@ var TaskExecutionEngine = class {
1896
2211
 
1897
2212
  // src/scheduled-tasks/TaskLogManager.ts
1898
2213
  import { randomUUID as randomUUID2 } from "crypto";
1899
- import * as fs8 from "fs";
1900
- import * as path7 from "path";
2214
+ import * as fs9 from "fs";
2215
+ import * as path8 from "path";
1901
2216
  var CONFIG_DIR4 = ".serviceme";
1902
2217
  var LOG_FILE2 = "scheduled-tasks-log.json";
1903
2218
  var MAX_LOGS = 200;
@@ -1922,17 +2237,17 @@ function validateAndRepairLogFile(raw) {
1922
2237
  }
1923
2238
  var TaskLogManager = class {
1924
2239
  constructor(workspacePath) {
1925
- this.logPath = path7.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
2240
+ this.logPath = path8.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1926
2241
  }
1927
2242
  getLogPath() {
1928
2243
  return this.logPath;
1929
2244
  }
1930
2245
  readLogFile() {
1931
- if (!fs8.existsSync(this.logPath)) {
2246
+ if (!fs9.existsSync(this.logPath)) {
1932
2247
  return emptyLogFile();
1933
2248
  }
1934
2249
  try {
1935
- const raw = fs8.readFileSync(this.logPath, "utf-8");
2250
+ const raw = fs9.readFileSync(this.logPath, "utf-8");
1936
2251
  const parsed = JSON.parse(raw);
1937
2252
  const file = validateAndRepairLogFile(parsed);
1938
2253
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
@@ -1948,21 +2263,21 @@ var TaskLogManager = class {
1948
2263
  }
1949
2264
  backupCorruptedFile() {
1950
2265
  try {
1951
- if (fs8.existsSync(this.logPath)) {
2266
+ if (fs9.existsSync(this.logPath)) {
1952
2267
  const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
1953
- fs8.copyFileSync(this.logPath, backupPath);
2268
+ fs9.copyFileSync(this.logPath, backupPath);
1954
2269
  }
1955
2270
  } catch {
1956
2271
  }
1957
2272
  }
1958
2273
  writeLogFile(file) {
1959
- const dir = path7.dirname(this.logPath);
1960
- if (!fs8.existsSync(dir)) {
1961
- fs8.mkdirSync(dir, { recursive: true });
2274
+ const dir = path8.dirname(this.logPath);
2275
+ if (!fs9.existsSync(dir)) {
2276
+ fs9.mkdirSync(dir, { recursive: true });
1962
2277
  }
1963
2278
  const tmp = `${this.logPath}.tmp`;
1964
- fs8.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1965
- fs8.renameSync(tmp, this.logPath);
2279
+ fs9.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
2280
+ fs9.renameSync(tmp, this.logPath);
1966
2281
  }
1967
2282
  appendLog(input) {
1968
2283
  const file = this.readLogFile();
@@ -2073,8 +2388,8 @@ var SchedulerDaemon = class {
2073
2388
  const configPath = this.configManager.getConfigPath();
2074
2389
  const dir = configPath.substring(0, configPath.lastIndexOf("/"));
2075
2390
  try {
2076
- if (fs9.existsSync(dir)) {
2077
- this.watcher = fs9.watch(dir, (_eventType, filename) => {
2391
+ if (fs10.existsSync(dir)) {
2392
+ this.watcher = fs10.watch(dir, (_eventType, filename) => {
2078
2393
  if (filename === "scheduled-tasks.json") {
2079
2394
  this.logger.log("info", "Config file changed, reconciling...");
2080
2395
  }
@@ -2226,7 +2541,173 @@ function matchCronField(field, value) {
2226
2541
  const values = field.split(",");
2227
2542
  return values.some((v) => Number.parseInt(v, 10) === value);
2228
2543
  }
2544
+
2545
+ // src/skills/SkillCatalogClient.ts
2546
+ var SkillCatalogClient = class {
2547
+ constructor(options = {}) {
2548
+ this.fetchImpl = options.fetchImpl ?? fetch;
2549
+ this.baseUrl = options.baseUrl;
2550
+ }
2551
+ async getCatalog() {
2552
+ if (!this.baseUrl) {
2553
+ return {
2554
+ skills: [],
2555
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2556
+ };
2557
+ }
2558
+ const response = await this.fetchImpl(
2559
+ `${this.baseUrl}/api/v1/marketplace/skills`
2560
+ );
2561
+ if (!response.ok) {
2562
+ throw new Error(`Failed to fetch skills catalog: ${response.status}`);
2563
+ }
2564
+ const data = await response.json();
2565
+ return {
2566
+ skills: data.skills ?? [],
2567
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2568
+ };
2569
+ }
2570
+ };
2571
+
2572
+ // src/skills/SkillReconciler.ts
2573
+ var SkillReconciler = class {
2574
+ constructor(deps) {
2575
+ this.deps = deps;
2576
+ }
2577
+ async mutate(request) {
2578
+ if (request.targetScope !== "workspace" && request.targetScope !== "user") {
2579
+ throw new Error(`Invalid target scope: ${String(request.targetScope)}`);
2580
+ }
2581
+ if (request.action === "uninstall" || request.action === "move" || request.action === "removeExternal") {
2582
+ return {
2583
+ status: "success",
2584
+ changed: true,
2585
+ message: `Skill ${request.action} completed.`
2586
+ };
2587
+ }
2588
+ if (request.action !== "install") {
2589
+ return {
2590
+ status: "blocked",
2591
+ changed: false,
2592
+ message: `Skill action is not supported by bridge reconciler: ${request.action}`
2593
+ };
2594
+ }
2595
+ const catalog = await this.deps.catalogClient.getCatalog();
2596
+ const remoteSkill = catalog.skills.find(
2597
+ (skill) => this.deps.skillStore.normalizeRemoteSkillId(skill.id) === request.skillId
2598
+ );
2599
+ if (!remoteSkill) {
2600
+ return {
2601
+ status: "blocked",
2602
+ changed: false,
2603
+ message: "Skill not found in catalog."
2604
+ };
2605
+ }
2606
+ if (!request.confirmed && (remoteSkill.hasScripts || remoteSkill.hasHooks)) {
2607
+ return {
2608
+ status: "requires_confirmation",
2609
+ changed: false,
2610
+ hasScripts: Boolean(remoteSkill.hasScripts),
2611
+ hasHooks: Boolean(remoteSkill.hasHooks),
2612
+ message: "This skill contains executable scripts that require confirmation."
2613
+ };
2614
+ }
2615
+ return {
2616
+ status: "success",
2617
+ changed: true,
2618
+ message: "Skill installed."
2619
+ };
2620
+ }
2621
+ };
2622
+
2623
+ // src/skills/SkillStore.ts
2624
+ import * as fs11 from "fs/promises";
2625
+ import * as path9 from "path";
2626
+ var USER_SKILL_MARKER_FILE = ".ms-devtools-skill.json";
2627
+ var WORKSPACE_SKILLS_ROOT_RELATIVE = ".github/skills";
2628
+ var WORKSPACE_SKILLS_MARKER_RELATIVE = ".github/.ms-devtools-skills.yml";
2629
+ var SAFE_LOCAL_ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
2630
+ function assertSafeLocalSkillId(skillId) {
2631
+ if (typeof skillId !== "string" || skillId.length === 0 || skillId === "." || skillId === ".." || skillId.includes("/") || skillId.includes("\\") || !SAFE_LOCAL_ID_PATTERN2.test(skillId)) {
2632
+ throw new Error(`Invalid skill id: ${skillId}`);
2633
+ }
2634
+ return skillId;
2635
+ }
2636
+ var SkillStore = class {
2637
+ constructor(options) {
2638
+ this.workspacePath = options.workspacePath;
2639
+ this.userSkillsRoot = options.userSkillsRoot;
2640
+ this.fileSystem = options.fileSystem ?? fs11;
2641
+ }
2642
+ normalizeRemoteSkillId(remoteId) {
2643
+ if (remoteId.startsWith("official/")) {
2644
+ return assertSafeLocalSkillId(remoteId.slice("official/".length));
2645
+ }
2646
+ if (remoteId.startsWith("community/")) {
2647
+ const lastSlash = remoteId.lastIndexOf("/");
2648
+ return assertSafeLocalSkillId(remoteId.slice(lastSlash + 1));
2649
+ }
2650
+ return assertSafeLocalSkillId(remoteId);
2651
+ }
2652
+ getWorkspaceSkillPath(skillId) {
2653
+ return `${WORKSPACE_SKILLS_ROOT_RELATIVE}/${skillId}`;
2654
+ }
2655
+ getWorkspaceMarkerPath() {
2656
+ return WORKSPACE_SKILLS_MARKER_RELATIVE;
2657
+ }
2658
+ getUserSkillPath(skillId) {
2659
+ return path9.join(this.userSkillsRoot, skillId);
2660
+ }
2661
+ async listWorkspaceSkillIds() {
2662
+ const skillsRootPath = path9.join(
2663
+ this.workspacePath,
2664
+ WORKSPACE_SKILLS_ROOT_RELATIVE
2665
+ );
2666
+ try {
2667
+ const entries = await this.fileSystem.readdir(skillsRootPath, {
2668
+ withFileTypes: true
2669
+ });
2670
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
2671
+ } catch {
2672
+ return [];
2673
+ }
2674
+ }
2675
+ async listUserSkillIds() {
2676
+ try {
2677
+ const entries = await this.fileSystem.readdir(this.userSkillsRoot, {
2678
+ withFileTypes: true
2679
+ });
2680
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
2681
+ } catch {
2682
+ return [];
2683
+ }
2684
+ }
2685
+ async writeManagedUserSkillMarker(skillId) {
2686
+ const targetDir = this.getUserSkillPath(skillId);
2687
+ await this.fileSystem.mkdir(targetDir, { recursive: true });
2688
+ await this.fileSystem.writeFile(
2689
+ path9.join(targetDir, USER_SKILL_MARKER_FILE),
2690
+ JSON.stringify({ skillId, installedBy: "ms-devtools" }, null, 2),
2691
+ "utf-8"
2692
+ );
2693
+ }
2694
+ async isManagedUserSkill(skillId) {
2695
+ try {
2696
+ const marker = await this.fileSystem.readFile(
2697
+ path9.join(this.getUserSkillPath(skillId), USER_SKILL_MARKER_FILE),
2698
+ "utf-8"
2699
+ );
2700
+ const parsed = JSON.parse(marker);
2701
+ return parsed.skillId === skillId;
2702
+ } catch {
2703
+ return false;
2704
+ }
2705
+ }
2706
+ };
2229
2707
  export {
2708
+ AgentCatalogClient,
2709
+ AgentReconciler,
2710
+ AgentStore,
2230
2711
  DaemonLogger,
2231
2712
  EnvironmentInspector,
2232
2713
  GithubCopilotCliExecutor,
@@ -2236,6 +2717,10 @@ export {
2236
2717
  ProjectTools,
2237
2718
  SchedulerDaemon,
2238
2719
  ShellExecutor,
2720
+ SkillCatalogClient,
2721
+ SkillReconciler,
2722
+ SkillStore,
2723
+ TOOL_RISK_MAP,
2239
2724
  TaskConfigManager,
2240
2725
  TaskExecutionEngine,
2241
2726
  TaskLogManager,
@@ -2253,6 +2738,7 @@ export {
2253
2738
  matchesCron,
2254
2739
  moveFiles,
2255
2740
  noopLogger,
2741
+ parseAgentToolPermissions,
2256
2742
  parseIntervalMs,
2257
2743
  resolveTaskExecutionPayload,
2258
2744
  unzipFile,