@serviceme/devtools-core 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/auth-Bzss8zOv.mjs +995 -0
  2. package/dist/auth-Bzss8zOv.mjs.map +1 -0
  3. package/dist/auth-DXam3c4S.js +1078 -0
  4. package/dist/auth-DXam3c4S.js.map +1 -0
  5. package/dist/auth.d.mts +2 -2
  6. package/dist/auth.d.ts +2 -2
  7. package/dist/auth.js +14 -1029
  8. package/dist/auth.mjs +2 -991
  9. package/dist/device-CDDcwDY6.mjs +758 -0
  10. package/dist/device-CDDcwDY6.mjs.map +1 -0
  11. package/dist/device-DuAYH8bk.js +839 -0
  12. package/dist/device-DuAYH8bk.js.map +1 -0
  13. package/dist/device.d.mts +2 -455
  14. package/dist/device.d.ts +2 -455
  15. package/dist/device.js +15 -728
  16. package/dist/device.mjs +2 -679
  17. package/dist/index-2rX86yMq.d.mts +331 -0
  18. package/dist/index-BI11AfMH.d.mts +377 -0
  19. package/dist/index-BI11AfMH.d.ts +377 -0
  20. package/dist/index-CoA6y0MP.d.mts +498 -0
  21. package/dist/index-CoA6y0MP.d.ts +498 -0
  22. package/dist/index-DaW7IYA-.d.mts +185 -0
  23. package/dist/index-DaW7IYA-.d.ts +185 -0
  24. package/dist/index-Dp6FwWKh.d.ts +331 -0
  25. package/dist/index.d.mts +2340 -2101
  26. package/dist/index.d.ts +2340 -2101
  27. package/dist/index.js +8117 -10538
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +7895 -10318
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/rolldown-runtime-VH7oDXx4.js +28 -0
  32. package/dist/skill-linker.d.mts +82 -81
  33. package/dist/skill-linker.d.ts +82 -81
  34. package/dist/skill-linker.js +359 -402
  35. package/dist/skill-linker.js.map +1 -1
  36. package/dist/skill-linker.mjs +345 -357
  37. package/dist/skill-linker.mjs.map +1 -1
  38. package/dist/skill-store-Bym2yinU.mjs +388 -0
  39. package/dist/skill-store-Bym2yinU.mjs.map +1 -0
  40. package/dist/skill-store-CJdYyyxu.js +408 -0
  41. package/dist/skill-store-CJdYyyxu.js.map +1 -0
  42. package/dist/skill-store.d.mts +41 -42
  43. package/dist/skill-store.d.ts +41 -42
  44. package/dist/skill-store.js +5 -338
  45. package/dist/skill-store.mjs +2 -301
  46. package/dist/submit-Cg7wSa3x.mjs +84 -0
  47. package/dist/submit-Cg7wSa3x.mjs.map +1 -0
  48. package/dist/submit-CltE6YOZ.js +98 -0
  49. package/dist/submit-CltE6YOZ.js.map +1 -0
  50. package/dist/submit.d.mts +2 -3
  51. package/dist/submit.d.ts +2 -3
  52. package/dist/submit.js +4 -166
  53. package/dist/submit.mjs +2 -130
  54. package/dist/toolbox-CbfxpNg-.mjs +540 -0
  55. package/dist/toolbox-CbfxpNg-.mjs.map +1 -0
  56. package/dist/toolbox-CxnOr-FY.js +614 -0
  57. package/dist/toolbox-CxnOr-FY.js.map +1 -0
  58. package/dist/toolbox.d.mts +2 -244
  59. package/dist/toolbox.d.ts +2 -244
  60. package/dist/toolbox.js +14 -584
  61. package/dist/toolbox.mjs +2 -536
  62. package/dist/{types-B9gk3dXH.d.ts → types-BCV3IedE.d.mts} +25 -23
  63. package/dist/{types-B9gk3dXH.d.mts → types-BCV3IedE.d.ts} +25 -23
  64. package/dist/userHome-BquDeFVE.js +526 -0
  65. package/dist/userHome-BquDeFVE.js.map +1 -0
  66. package/dist/userHome-CcnAQPG1.mjs +212 -0
  67. package/dist/userHome-CcnAQPG1.mjs.map +1 -0
  68. package/package.json +10 -10
  69. package/dist/auth.js.map +0 -1
  70. package/dist/auth.mjs.map +0 -1
  71. package/dist/device.js.map +0 -1
  72. package/dist/device.mjs.map +0 -1
  73. package/dist/index-BMk4tqIT.d.mts +0 -328
  74. package/dist/index-CymN0x9Z.d.ts +0 -328
  75. package/dist/index-PD135hlB.d.mts +0 -626
  76. package/dist/index-PD135hlB.d.ts +0 -626
  77. package/dist/skill-store.js.map +0 -1
  78. package/dist/skill-store.mjs.map +0 -1
  79. package/dist/submit.js.map +0 -1
  80. package/dist/submit.mjs.map +0 -1
  81. package/dist/toolbox.js.map +0 -1
  82. package/dist/toolbox.mjs.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/skill-store/index.ts","../src/repo-layout/index.ts","../src/skill-store/types.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { DEFAULT_REPO_LAYOUT, loadRepoLayout, type RepoLayoutDescriptor } from \"../repo-layout\";\nimport type { SkillDetail, SkillEntry, SkillFile, SkillKind } from \"./types\";\nimport { SkillNotFoundError } from \"./types\";\n\n/**\n * Minimal frontmatter reader — same shape as the extension's\n * `WorkspaceSkillsInitializationService.extractFrontmatter`. We avoid\n * pulling a YAML dep into core; SKILL.md / AGENT.md in the wild use\n * a strict subset (top-level `key: value` pairs delimited by `---`).\n * Multi-line block scalars (`>`, `|-`, `|`) are folded into a single\n * string value — the very common `description: >` form would\n * otherwise surface the bare `>` indicator as the whole description.\n */\nexport function extractFrontmatter(\n\traw: string\n): { data: Record<string, unknown>; body: string } | null {\n\tif (typeof raw !== \"string\") return null;\n\tconst match = raw.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n\tif (!match) return null;\n\tconst yamlBody = match[1] ?? \"\";\n\tconst body = match[2] ?? \"\";\n\tconst data: Record<string, unknown> = {};\n\tconst lines = yamlBody.split(/\\r?\\n/);\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i] ?? \"\";\n\t\tif (line.trim().length === 0) continue;\n\t\tif (line.trim().startsWith(\"#\")) continue;\n\t\tconst kv = line.match(/^([a-zA-Z_][\\w-]*)\\s*:\\s*(.*)$/);\n\t\tif (!kv?.[1]) continue;\n\t\tlet value: unknown = (kv[2] ?? \"\").trim();\n\t\t// Block scalar indicators: fold the following more-indented\n\t\t// lines into one value (blank separators become newlines for\n\t\t// `|`, spaces for `>`; we keep it simple and always fold with\n\t\t// spaces, which is right for descriptions).\n\t\tif (typeof value === \"string\" && /^[>|][+-]?$/.test(value)) {\n\t\t\tconst folded: string[] = [];\n\t\t\tlet j = i + 1;\n\t\t\tfor (; j < lines.length; j++) {\n\t\t\t\tconst next = lines[j] ?? \"\";\n\t\t\t\tif (next.trim().length === 0) {\n\t\t\t\t\t// A blank line only belongs to the scalar if a later\n\t\t\t\t\t// more-indented line follows; otherwise it ends it.\n\t\t\t\t\tconst after = lines[j + 1];\n\t\t\t\t\tif (after !== undefined && /^[ \\t]/.test(after)) {\n\t\t\t\t\t\tfolded.push(\"\");\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (!/^[ \\t]/.test(next)) break; // dedent ends the scalar\n\t\t\t\tfolded.push(next.trim());\n\t\t\t}\n\t\t\ti = j - 1;\n\t\t\t// Collapse whitespace runs so blank separators don't leave\n\t\t\t// double spaces (display text, not YAML fidelity).\n\t\t\tvalue = folded.join(\" \").replace(/\\s+/g, \" \").trim();\n\t\t\tdata[kv[1]] = value;\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof value === \"string\") {\n\t\t\tif (\n\t\t\t\t(value.startsWith('\"') && value.endsWith('\"')) ||\n\t\t\t\t(value.startsWith(\"'\") && value.endsWith(\"'\"))\n\t\t\t) {\n\t\t\t\tvalue = value.slice(1, -1);\n\t\t\t}\n\t\t}\n\t\tdata[kv[1]] = value;\n\t}\n\treturn { data, body };\n}\n\nexport type { SkillDetail, SkillEntry, SkillFile, SkillKind } from \"./types\";\n/**\n * Skill & Agent v2 — SkillStore (M4)\n *\n * Scans the local `~/.serviceme/repos/<id>/` tree to produce a unified\n * list of skills + agents. Local-only — no git, no network. Spec §5.5.\n *\n * Layout normalization (spec §12):\n * 1. **Standard layout** — `skills/<name>/SKILL.md` or\n * `agents/<name>/AGENT.md` under the repo root.\n * 2. **Anthropic flat layout** — `<name>/SKILL.md` directly under\n * the repo root (the `skills/` prefix is omitted).\n * 3. **awesome-copilot style** — root contains README + subdirs\n * that are skills/agents/instructions; we ignore README and\n * any dir without a manifest file.\n * 4. **Flat agent files** — a directory (commonly `agents/` or\n * `agents/official/`) full of `<name>.agent.md` files, one per\n * agent, with NO per-agent subdirectory. This is the prevailing\n * real-world convention (VS Code custom chat agents, GitHub's\n * awesome-copilot, and our own official ms-skills repo all ship\n * agents this way) — unlike skills, agents in the wild are\n * essentially never a `<name>/AGENT.md` directory pair. Detected\n * unconditionally, no `.serviceme-repo.json` opt-in required.\n *\n * Implementation: walk every directory under the repo root, check for\n * SKILL.md or AGENT.md, register if present. We skip the repo root\n * itself (no manifest lives at the top), `.git/`, and `node_modules/`.\n * Flat `*.agent.md` files are checked alongside directories at every\n * level of the walk.\n */\n// Re-export the error class + shared types so callers can\n// `import { SkillNotFoundError, SkillFile } from \"...\"`.\nexport { SkillNotFoundError } from \"./types\";\n\nexport class SkillStore {\n\tprivate readonly repoRoots: Map<string, string>;\n\t/**\n\t * Per-instance memo of repo walks. The catalog walk is a recursive\n\t * readdir over the whole checkout — the dominant cost of every\n\t * list/get — so results (and the in-flight promise) are reused for\n\t * the store's lifetime. The handler that owns long-lived stores\n\t * bounds staleness by rebuilding the store on a short TTL; within\n\t * one store instance the tree is treated as immutable.\n\t */\n\tprivate readonly entriesMemo = new Map<string, Promise<SkillEntry[]>>();\n\n\tconstructor(opts: {\n\t\trepos: ReadonlyArray<{ id: string; rootPath: string }>;\n\t}) {\n\t\tthis.repoRoots = new Map();\n\t\tfor (const r of opts.repos) this.repoRoots.set(r.id, r.rootPath);\n\t}\n\n\t/** All skills + agents across all registered repos. Repos are walked\n\t * concurrently — one slow checkout no longer stretches the total. */\n\tasync listAll(): Promise<SkillEntry[]> {\n\t\tconst perRepo = await Promise.all(\n\t\t\t[...this.repoRoots.keys()].map((repoId) => this.listByRepo(repoId))\n\t\t);\n\t\treturn perRepo.flat();\n\t}\n\n\t/** All skills + agents under a single repo. */\n\tasync listByRepo(repoId: string): Promise<SkillEntry[]> {\n\t\tconst entries = await this.listByRepoCached(repoId);\n\t\t// Copy so memoized state can't be corrupted by caller mutations.\n\t\treturn [...entries];\n\t}\n\n\t/** Single entry detail (manifest + all files inside its dir). */\n\tasync get(repoId: string, name: string): Promise<SkillDetail> {\n\t\tconst entries = await this.listByRepoCached(repoId);\n\t\tconst found = entries.find((e) => e.name === name);\n\t\tif (!found) throw new SkillNotFoundError(repoId, name);\n\t\tconst files = await collectEntryFiles(found);\n\t\treturn { ...found, files };\n\t}\n\n\t/** All files inside a single entry's directory (manifest + extras). */\n\tasync getFiles(repoId: string, name: string): Promise<SkillFile[]> {\n\t\tconst entries = await this.listByRepoCached(repoId);\n\t\tconst found = entries.find((e) => e.name === name);\n\t\tif (!found) throw new SkillNotFoundError(repoId, name);\n\t\treturn collectEntryFiles(found);\n\t}\n\n\t/**\n\t * Memoized walk. Sharing the promise collapses concurrent list/get\n\t * calls over the same repo into a single traversal; a rejected walk\n\t * is dropped so the next call retries from disk.\n\t */\n\tprivate listByRepoCached(repoId: string): Promise<SkillEntry[]> {\n\t\tconst memoized = this.entriesMemo.get(repoId);\n\t\tif (memoized) return memoized;\n\t\tconst walk = this.walkRepo(repoId).catch((error: unknown) => {\n\t\t\tthis.entriesMemo.delete(repoId);\n\t\t\tthrow error;\n\t\t});\n\t\tthis.entriesMemo.set(repoId, walk);\n\t\treturn walk;\n\t}\n\n\tprivate async walkRepo(repoId: string): Promise<SkillEntry[]> {\n\t\tconst root = this.repoRoots.get(repoId);\n\t\tif (!root) return [];\n\t\tconst layout = (await loadRepoLayout(root)) ?? DEFAULT_REPO_LAYOUT;\n\t\tconst entries: SkillEntry[] = [];\n\t\tawait walkForEntries(root, repoId, entries, layout);\n\t\treturn entries;\n\t}\n}\n\n/** All files inside a single entry (manifest first, then alphabetical). */\nasync function collectEntryFiles(found: SkillEntry): Promise<SkillFile[]> {\n\tconst out: SkillFile[] = [];\n\t// Flat single-file entries (e.g. `agents/foo.agent.md`) have `dir`\n\t// pointing at the manifest file itself, not a directory — there's\n\t// no sibling files to collect, just the one manifest.\n\tif (found.dir === found.manifestPath) {\n\t\tconst content = await fs.readFile(found.manifestPath, \"utf8\");\n\t\treturn [{ path: path.basename(found.manifestPath), content }];\n\t}\n\tawait collectFilesRecursive(found.dir, found.dir, out);\n\t// Sort for determinism (manifest first, then alphabetical)\n\tout.sort((a, b) => {\n\t\tconst aIsManifest = a.path === \"SKILL.md\" || a.path === \"AGENT.md\";\n\t\tconst bIsManifest = b.path === \"SKILL.md\" || b.path === \"AGENT.md\";\n\t\tif (aIsManifest && !bIsManifest) return -1;\n\t\tif (bIsManifest && !aIsManifest) return 1;\n\t\treturn a.path.localeCompare(b.path);\n\t});\n\treturn out;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// Internals\n// ─────────────────────────────────────────────────────────────────────\n\nconst SKIP_DIRS = new Set([\".git\", \"node_modules\", \".vscode\", \"dist\", \"build\", \"out\"]);\n\n/**\n * Directories that are never treated as candidate skill/agent parents\n * during the top-level catalog walk. `plugins/` is a distinct concept\n * from the skill/agent catalog itself — a plugin bundle\n * (`plugins/<scope>/<plugin-name>/{skills,agents}/...`) re-packages\n * ALREADY-cataloged official skills/agents (see the plugin's own\n * `catalog.json`, which lists them by name) purely for discovery\n * grouping. Walking into it produces duplicate `SkillEntry` values\n * with the SAME `repoId`+`name` as their real, top-level counterpart\n * (e.g. `skills/official/acreadiness-assess` AND\n * `plugins/official/acreadiness-cockpit/skills/acreadiness-assess`),\n * which breaks anything keyed on `repoId/name` (React list rendering,\n * the \"installed\" lookup, `SkillStore.get()`'s `.find()`).\n *\n * Scoped separately from `SKIP_DIRS` (used by both this walk AND\n * `collectFilesRecursive`) so an actual skill that happens to ship its\n * own `plugins/` asset folder still has that folder listed in its own\n * file detail view.\n */\nconst SKIP_TOP_LEVEL_SCAN_DIRS = new Set([...SKIP_DIRS, \"plugins\"]);\n\n/** Suffix that marks a standalone file as a flat agent manifest (no wrapping dir). */\nconst FLAT_AGENT_FILE_SUFFIX = \".agent.md\";\n\n/**\n * Recursive walk that yields SkillEntry values for any directory\n * containing a SKILL.md or AGENT.md file, PLUS any standalone\n * `<name>.agent.md` file (see class doc, layout style 4). The dir\n * itself is the \"entry root\" for directory-based entries; the\n * manifest file itself is the \"entry root\" for flat agent files.\n *\n * The optional `layout` descriptor (from `.serviceme-repo.json`)\n * widens the scan: included subdirs are walked even without a\n * manifest at the top level, and `treatFilesAsSkills` surfaces\n * `<name>.md` files inside them as skills (frontmatter parsed\n * from the file body).\n */\nasync function walkForEntries(\n\trootDir: string,\n\trepoId: string,\n\tout: SkillEntry[],\n\tlayout: RepoLayoutDescriptor = DEFAULT_REPO_LAYOUT\n): Promise<void> {\n\tlet dirents: import(\"node:fs\").Dirent[];\n\ttry {\n\t\tdirents = await fs.readdir(rootDir, { withFileTypes: true });\n\t} catch {\n\t\treturn;\n\t}\n\n\tfor (const d of dirents) {\n\t\tif (d.isFile() && d.name.endsWith(FLAT_AGENT_FILE_SUFFIX)) {\n\t\t\tconst filePath = path.join(rootDir, d.name);\n\t\t\tconst stat = await fs.stat(filePath);\n\t\t\tconst content = await fs.readFile(filePath, \"utf8\");\n\t\t\tconst parsed = extractFrontmatter(content);\n\t\t\tout.push({\n\t\t\t\trepoId,\n\t\t\t\tname: d.name.slice(0, -FLAT_AGENT_FILE_SUFFIX.length),\n\t\t\t\tkind: \"agent\",\n\t\t\t\tmanifestPath: filePath,\n\t\t\t\t// Sentinel: `dir === manifestPath` marks a flat single-file\n\t\t\t\t// entry (no directory of its own — see getFiles()).\n\t\t\t\tdir: filePath,\n\t\t\t\tfrontmatter: parsed?.data ?? {},\n\t\t\t\tmodifiedAt: stat.mtime.toISOString(),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (!d.isDirectory()) continue;\n\t\tif (SKIP_TOP_LEVEL_SCAN_DIRS.has(d.name)) continue;\n\t\tif (layout.exclude.includes(d.name)) continue;\n\t\tconst childDir = path.join(rootDir, d.name);\n\t\tconst kind = await detectKind(childDir);\n\t\tif (kind) {\n\t\t\tconst manifestFilename = kind === \"skill\" ? \"SKILL.md\" : \"AGENT.md\";\n\t\t\tconst manifestPath = path.join(childDir, manifestFilename);\n\t\t\tconst stat = await fs.stat(manifestPath);\n\t\t\tconst content = await fs.readFile(manifestPath, \"utf8\");\n\t\t\tconst parsed = extractFrontmatter(content);\n\t\t\tout.push({\n\t\t\t\trepoId,\n\t\t\t\tname: d.name,\n\t\t\t\tkind,\n\t\t\t\tmanifestPath,\n\t\t\t\tdir: childDir,\n\t\t\t\tfrontmatter: parsed?.data ?? {},\n\t\t\t\tmodifiedAt: stat.mtime.toISOString(),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\t// No manifest at the top — recurse, but ALSO check whether\n\t\t// the layout descriptor wants this subdir widened.\n\t\tawait walkForEntries(childDir, repoId, out, layout);\n\t\tif (layout.treatFilesAsSkills && layout.include.includes(d.name)) {\n\t\t\tawait surfaceFilesAsSkills(childDir, d.name, repoId, out);\n\t\t}\n\t}\n}\n\n/**\n * Walk `<includedSubdir>/<file>.md` and register each as a skill.\n * The `.md` file itself is the manifest — frontmatter (if any) is\n * parsed and surfaced. This is the awesome-copilot style: a\n * `prompts/` dir full of `<name>.md` files, no SKILL.md anywhere.\n */\nasync function surfaceFilesAsSkills(\n\tabsDir: string,\n\trelName: string,\n\trepoId: string,\n\tout: SkillEntry[]\n): Promise<void> {\n\tlet dirents: import(\"node:fs\").Dirent[];\n\ttry {\n\t\tdirents = await fs.readdir(absDir, { withFileTypes: true });\n\t} catch {\n\t\treturn;\n\t}\n\tfor (const d of dirents) {\n\t\tif (!d.isFile()) continue;\n\t\tif (!d.name.endsWith(\".md\")) continue;\n\t\tconst filePath = path.join(absDir, d.name);\n\t\tconst stat = await fs.stat(filePath);\n\t\tconst content = await fs.readFile(filePath, \"utf8\");\n\t\tconst parsed = extractFrontmatter(content);\n\t\t// Entry name = \"<subdir>/<file>\" (with the .md stripped) so it\n\t\t// stays unique even when two included subdirs share filenames.\n\t\tconst entryName = `${relName}/${d.name.replace(/\\.md$/, \"\")}`;\n\t\tout.push({\n\t\t\trepoId,\n\t\t\tname: entryName,\n\t\t\tkind: \"skill\",\n\t\t\tmanifestPath: filePath,\n\t\t\tdir: absDir,\n\t\t\tfrontmatter: parsed?.data ?? {},\n\t\t\tmodifiedAt: stat.mtime.toISOString(),\n\t\t});\n\t}\n}\n\n/**\n * Returns the SkillKind of a directory if it contains a manifest file,\n * or `null` if it doesn't. Manifest precedence: SKILL.md wins if both\n * are present (defensive — the spec separates skills and agents into\n * different subdir trees, so this collision shouldn't happen in\n * well-formed repos).\n */\nasync function detectKind(dir: string): Promise<SkillKind | null> {\n\tconst [hasSkill, hasAgent] = await Promise.all([\n\t\tfs\n\t\t\t.access(path.join(dir, \"SKILL.md\"))\n\t\t\t.then(() => true)\n\t\t\t.catch(() => false),\n\t\tfs\n\t\t\t.access(path.join(dir, \"AGENT.md\"))\n\t\t\t.then(() => true)\n\t\t\t.catch(() => false),\n\t]);\n\tif (hasSkill) return \"skill\";\n\tif (hasAgent) return \"agent\";\n\treturn null;\n}\n\n/**\n * Walk a skill/agent directory recursively, pushing every file into\n * `out` with its path RELATIVE to the entry root. Skips `.git/`,\n * `node_modules/`, and the SKIP_DIRS set.\n */\nasync function collectFilesRecursive(\n\tabsDir: string,\n\tentryRoot: string,\n\tout: SkillFile[]\n): Promise<void> {\n\tlet dirents: import(\"node:fs\").Dirent[];\n\ttry {\n\t\tdirents = await fs.readdir(absDir, { withFileTypes: true });\n\t} catch {\n\t\treturn;\n\t}\n\tfor (const d of dirents) {\n\t\tif (SKIP_DIRS.has(d.name)) continue;\n\t\tconst full = path.join(absDir, d.name);\n\t\tif (d.isDirectory()) {\n\t\t\tawait collectFilesRecursive(full, entryRoot, out);\n\t\t} else if (d.isFile()) {\n\t\t\tconst content = await fs.readFile(full, \"utf8\");\n\t\t\tout.push({ path: path.relative(entryRoot, full), content });\n\t\t}\n\t}\n}\n","/**\n * Spec §12 — Repo layout descriptor.\n *\n * Some third-party repos (awesome-copilot, composio) use a\n * non-default layout that mixes prompts / instructions / agents /\n * hooks / plugins at the root. The default SkillStore scan\n * (see ../skill-store/index.ts) only picks up directories that\n * contain a `SKILL.md` or `AGENT.md` manifest, so prompts stored\n * as `<subdir>/<name>.md` (no manifest) get missed.\n *\n * This module is the loader for the optional `.serviceme-repo.json`\n * marker file a repo author can drop in at the repo root to opt\n * into a wider scan. The schema:\n *\n * {\n * \"schema\": 1,\n * \"include\": [\"prompts\", \"instructions\"], // subdirs to treat as \"skill dirs\"\n * \"exclude\": [\"hooks\", \"plugins\"], // subdirs to skip\n * \"treatFilesAsSkills\": true // inside an included subdir, each .md file is a skill\n * }\n *\n * All four fields are optional. The defaults match the v0.2\n * walkForEntries behaviour (manifest-based, no marker, no\n * extensions), so adding the file is purely additive.\n *\n * @see docs/architecture/skill-agent-v2-repo.md §12\n */\n\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\n\nconst MARKER_FILENAME = \".serviceme-repo.json\";\nconst SUPPORTED_SCHEMA = 1;\n\nexport interface RepoLayoutDescriptor {\n\t/** Schema version. Currently always 1. */\n\tschema: number;\n\t/** Subdirs (relative to repo root) to walk for entries. */\n\tinclude: string[];\n\t/** Subdirs (relative to repo root) to skip, even if a parent is included. */\n\texclude: string[];\n\t/**\n\t * When true, an included subdir's `<name>.md` files are\n\t * surfaced as skills (frontmatter is parsed from each file as\n\t * a stand-in for SKILL.md). When false (default), the\n\t * include list only widens the recursion — the manifest rule\n\t * still applies.\n\t */\n\ttreatFilesAsSkills: boolean;\n}\n\n/**\n * Default descriptor used when no `.serviceme-repo.json` is\n * present. The shape is the same as what an empty marker would\n * produce, so callers don't have to special-case the \"no marker\"\n * branch.\n */\nexport const DEFAULT_REPO_LAYOUT: RepoLayoutDescriptor = {\n\tschema: SUPPORTED_SCHEMA,\n\tinclude: [],\n\texclude: [],\n\ttreatFilesAsSkills: false,\n};\n\n/**\n * Try to load a `.serviceme-repo.json` from the given repo root.\n * Returns `DEFAULT_REPO_LAYOUT` (not undefined) when the file is\n * absent — callers branch on `include.length` / `treatFilesAsSkills`\n * to decide whether to widen the scan.\n *\n * Malformed markers (bad JSON, wrong schema) are surfaced as\n * `null` so callers can warn the user instead of silently\n * treating them as the default.\n */\nexport async function loadRepoLayout(repoRoot: string): Promise<RepoLayoutDescriptor | null> {\n\tconst markerPath = path.join(repoRoot, MARKER_FILENAME);\n\tlet raw: string;\n\ttry {\n\t\traw = await fs.readFile(markerPath, \"utf8\");\n\t} catch {\n\t\treturn DEFAULT_REPO_LAYOUT;\n\t}\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch {\n\t\treturn null;\n\t}\n\tif (!parsed || typeof parsed !== \"object\") return null;\n\tconst obj = parsed as Record<string, unknown>;\n\n\tif (obj.schema !== SUPPORTED_SCHEMA) return null;\n\tif (!Array.isArray(obj.include) || obj.include.some((s) => typeof s !== \"string\")) {\n\t\treturn null;\n\t}\n\tif (!Array.isArray(obj.exclude) || obj.exclude.some((s) => typeof s !== \"string\")) {\n\t\treturn null;\n\t}\n\tif (typeof obj.treatFilesAsSkills !== \"boolean\") return null;\n\n\treturn {\n\t\tschema: SUPPORTED_SCHEMA,\n\t\tinclude: obj.include as string[],\n\t\texclude: obj.exclude as string[],\n\t\ttreatFilesAsSkills: obj.treatFilesAsSkills,\n\t};\n}\n\n/** Subdir name → entry kind override (rarely needed; default = manifest-driven). */\nexport type KindOverride = Map<string, \"skill\" | \"agent\">;\n","/**\n * Skill & Agent v2 — SkillStore Types\n *\n * SkillStore scans the local `~/.serviceme/repos/<id>/` tree to produce\n * a unified list of skills + agents. The store is the single source of\n * truth for \"what's available right now\" — the UI reads it; SubmitClient\n * reads it; install flows read it.\n *\n * Three repo layouts are supported in v1 (per spec §12):\n * 1. **Default** (`medalsoftchina-ms-skills`): `skills/<name>/SKILL.md`\n * + `agents/<name>/AGENT.md`.\n * 2. **Anthropic-style flat** (`anthropics/skills`): each subdir of\n * the repo root IS a skill — `<name>/SKILL.md` (no `skills/`\n * intermediate).\n * 3. **awesome-copilot style** (`github/awesome-copilot`): mixed\n * prompts/instructions/agents at the root.\n *\n * Normalization rule (spec §12): for every directory under the repo\n * root, peek for a `SKILL.md` or `AGENT.md` file. If present, register\n * it as a skill or agent respectively. This is more permissive than\n * guessing from the directory name and works for all three styles.\n *\n * The store is local-only and pure — no git, no network. Tests\n * construct a fixture repo tree and pass the root in directly.\n */\n\n/** What's available at this entry: a skill or an agent. */\nexport type SkillKind = \"skill\" | \"agent\";\n\n/** Top-level summary of a discovered skill/agent entry. */\nexport interface SkillEntry {\n\t/** Repository the entry was found in. */\n\trepoId: string;\n\t/** Skill or agent name (the directory name under the repo). */\n\tname: string;\n\tkind: SkillKind;\n\t/** Absolute path to the SKILL.md / AGENT.md file. */\n\tmanifestPath: string;\n\t/** Absolute path to the directory containing the entry's files. */\n\tdir: string;\n\t/** Parsed frontmatter (best-effort; raw key/value map). */\n\tfrontmatter: Record<string, unknown>;\n\t/** ISO timestamp of the manifest file's mtime. */\n\tmodifiedAt: string;\n}\n\n/** Full detail of an entry: summary + all the files inside the dir. */\nexport interface SkillDetail extends SkillEntry {\n\tfiles: SkillFile[];\n}\n\n/** A single file inside a skill / agent directory. */\nexport interface SkillFile {\n\t/** Path relative to the entry's directory. */\n\tpath: string;\n\t/** UTF-8 content. */\n\tcontent: string;\n}\n\n/** Sentinel error for store lookups that miss. */\nexport class SkillNotFoundError extends Error {\n\tconstructor(\n\t\tpublic readonly repoId: string,\n\t\tpublic readonly skillName: string\n\t) {\n\t\tsuper(`skill/agent not found: ${repoId}/${skillName}`);\n\t\tthis.name = \"SkillNotFoundError\";\n\t}\n}\n"],"mappings":"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","names":["fs","path","stat"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/skill-store/index.ts","../src/repo-layout/index.ts","../src/skill-store/types.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { DEFAULT_REPO_LAYOUT, loadRepoLayout, type RepoLayoutDescriptor } from \"../repo-layout\";\nimport type { SkillDetail, SkillEntry, SkillFile, SkillKind } from \"./types\";\nimport { SkillNotFoundError } from \"./types\";\n\n/**\n * Minimal frontmatter reader — same shape as the extension's\n * `WorkspaceSkillsInitializationService.extractFrontmatter`. We avoid\n * pulling a YAML dep into core; SKILL.md / AGENT.md in the wild use\n * a strict subset (top-level `key: value` pairs delimited by `---`).\n * Multi-line block scalars (`>`, `|-`, `|`) are folded into a single\n * string value — the very common `description: >` form would\n * otherwise surface the bare `>` indicator as the whole description.\n */\nexport function extractFrontmatter(\n\traw: string\n): { data: Record<string, unknown>; body: string } | null {\n\tif (typeof raw !== \"string\") return null;\n\tconst match = raw.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n\tif (!match) return null;\n\tconst yamlBody = match[1] ?? \"\";\n\tconst body = match[2] ?? \"\";\n\tconst data: Record<string, unknown> = {};\n\tconst lines = yamlBody.split(/\\r?\\n/);\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i] ?? \"\";\n\t\tif (line.trim().length === 0) continue;\n\t\tif (line.trim().startsWith(\"#\")) continue;\n\t\tconst kv = line.match(/^([a-zA-Z_][\\w-]*)\\s*:\\s*(.*)$/);\n\t\tif (!kv?.[1]) continue;\n\t\tlet value: unknown = (kv[2] ?? \"\").trim();\n\t\t// Block scalar indicators: fold the following more-indented\n\t\t// lines into one value (blank separators become newlines for\n\t\t// `|`, spaces for `>`; we keep it simple and always fold with\n\t\t// spaces, which is right for descriptions).\n\t\tif (typeof value === \"string\" && /^[>|][+-]?$/.test(value)) {\n\t\t\tconst folded: string[] = [];\n\t\t\tlet j = i + 1;\n\t\t\tfor (; j < lines.length; j++) {\n\t\t\t\tconst next = lines[j] ?? \"\";\n\t\t\t\tif (next.trim().length === 0) {\n\t\t\t\t\t// A blank line only belongs to the scalar if a later\n\t\t\t\t\t// more-indented line follows; otherwise it ends it.\n\t\t\t\t\tconst after = lines[j + 1];\n\t\t\t\t\tif (after !== undefined && /^[ \\t]/.test(after)) {\n\t\t\t\t\t\tfolded.push(\"\");\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (!/^[ \\t]/.test(next)) break; // dedent ends the scalar\n\t\t\t\tfolded.push(next.trim());\n\t\t\t}\n\t\t\ti = j - 1;\n\t\t\t// Collapse whitespace runs so blank separators don't leave\n\t\t\t// double spaces (display text, not YAML fidelity).\n\t\t\tvalue = folded.join(\" \").replace(/\\s+/g, \" \").trim();\n\t\t\tdata[kv[1]] = value;\n\t\t\tcontinue;\n\t\t}\n\t\tif (typeof value === \"string\") {\n\t\t\tif (\n\t\t\t\t(value.startsWith('\"') && value.endsWith('\"')) ||\n\t\t\t\t(value.startsWith(\"'\") && value.endsWith(\"'\"))\n\t\t\t) {\n\t\t\t\tvalue = value.slice(1, -1);\n\t\t\t}\n\t\t}\n\t\tdata[kv[1]] = value;\n\t}\n\treturn { data, body };\n}\n\nexport type { SkillDetail, SkillEntry, SkillFile, SkillKind } from \"./types\";\n/**\n * Skill & Agent v2 — SkillStore (M4)\n *\n * Scans the local `~/.serviceme/repos/<id>/` tree to produce a unified\n * list of skills + agents. Local-only — no git, no network. Spec §5.5.\n *\n * Layout normalization (spec §12):\n * 1. **Standard layout** — `skills/<name>/SKILL.md` or\n * `agents/<name>/AGENT.md` under the repo root.\n * 2. **Anthropic flat layout** — `<name>/SKILL.md` directly under\n * the repo root (the `skills/` prefix is omitted).\n * 3. **awesome-copilot style** — root contains README + subdirs\n * that are skills/agents/instructions; we ignore README and\n * any dir without a manifest file.\n * 4. **Flat agent files** — a directory (commonly `agents/` or\n * `agents/official/`) full of `<name>.agent.md` files, one per\n * agent, with NO per-agent subdirectory. This is the prevailing\n * real-world convention (VS Code custom chat agents, GitHub's\n * awesome-copilot, and our own official ms-skills repo all ship\n * agents this way) — unlike skills, agents in the wild are\n * essentially never a `<name>/AGENT.md` directory pair. Detected\n * unconditionally, no `.serviceme-repo.json` opt-in required.\n *\n * Implementation: walk every directory under the repo root, check for\n * SKILL.md or AGENT.md, register if present. We skip the repo root\n * itself (no manifest lives at the top), `.git/`, and `node_modules/`.\n * Flat `*.agent.md` files are checked alongside directories at every\n * level of the walk.\n */\n// Re-export the error class + shared types so callers can\n// `import { SkillNotFoundError, SkillFile } from \"...\"`.\nexport { SkillNotFoundError } from \"./types\";\n\nexport class SkillStore {\n\tprivate readonly repoRoots: Map<string, string>;\n\t/**\n\t * Per-instance memo of repo walks. The catalog walk is a recursive\n\t * readdir over the whole checkout — the dominant cost of every\n\t * list/get — so results (and the in-flight promise) are reused for\n\t * the store's lifetime. The handler that owns long-lived stores\n\t * bounds staleness by rebuilding the store on a short TTL; within\n\t * one store instance the tree is treated as immutable.\n\t */\n\tprivate readonly entriesMemo = new Map<string, Promise<SkillEntry[]>>();\n\n\tconstructor(opts: {\n\t\trepos: ReadonlyArray<{ id: string; rootPath: string }>;\n\t}) {\n\t\tthis.repoRoots = new Map();\n\t\tfor (const r of opts.repos) this.repoRoots.set(r.id, r.rootPath);\n\t}\n\n\t/** All skills + agents across all registered repos. Repos are walked\n\t * concurrently — one slow checkout no longer stretches the total. */\n\tasync listAll(): Promise<SkillEntry[]> {\n\t\tconst perRepo = await Promise.all(\n\t\t\t[...this.repoRoots.keys()].map((repoId) => this.listByRepo(repoId))\n\t\t);\n\t\treturn perRepo.flat();\n\t}\n\n\t/** All skills + agents under a single repo. */\n\tasync listByRepo(repoId: string): Promise<SkillEntry[]> {\n\t\tconst entries = await this.listByRepoCached(repoId);\n\t\t// Copy so memoized state can't be corrupted by caller mutations.\n\t\treturn [...entries];\n\t}\n\n\t/** Single entry detail (manifest + all files inside its dir). */\n\tasync get(repoId: string, name: string): Promise<SkillDetail> {\n\t\tconst entries = await this.listByRepoCached(repoId);\n\t\tconst found = entries.find((e) => e.name === name);\n\t\tif (!found) throw new SkillNotFoundError(repoId, name);\n\t\tconst files = await collectEntryFiles(found);\n\t\treturn { ...found, files };\n\t}\n\n\t/** All files inside a single entry's directory (manifest + extras). */\n\tasync getFiles(repoId: string, name: string): Promise<SkillFile[]> {\n\t\tconst entries = await this.listByRepoCached(repoId);\n\t\tconst found = entries.find((e) => e.name === name);\n\t\tif (!found) throw new SkillNotFoundError(repoId, name);\n\t\treturn collectEntryFiles(found);\n\t}\n\n\t/**\n\t * Memoized walk. Sharing the promise collapses concurrent list/get\n\t * calls over the same repo into a single traversal; a rejected walk\n\t * is dropped so the next call retries from disk.\n\t */\n\tprivate listByRepoCached(repoId: string): Promise<SkillEntry[]> {\n\t\tconst memoized = this.entriesMemo.get(repoId);\n\t\tif (memoized) return memoized;\n\t\tconst walk = this.walkRepo(repoId).catch((error: unknown) => {\n\t\t\tthis.entriesMemo.delete(repoId);\n\t\t\tthrow error;\n\t\t});\n\t\tthis.entriesMemo.set(repoId, walk);\n\t\treturn walk;\n\t}\n\n\tprivate async walkRepo(repoId: string): Promise<SkillEntry[]> {\n\t\tconst root = this.repoRoots.get(repoId);\n\t\tif (!root) return [];\n\t\tconst layout = (await loadRepoLayout(root)) ?? DEFAULT_REPO_LAYOUT;\n\t\tconst entries: SkillEntry[] = [];\n\t\tawait walkForEntries(root, repoId, entries, layout);\n\t\treturn entries;\n\t}\n}\n\n/** All files inside a single entry (manifest first, then alphabetical). */\nasync function collectEntryFiles(found: SkillEntry): Promise<SkillFile[]> {\n\tconst out: SkillFile[] = [];\n\t// Flat single-file entries (e.g. `agents/foo.agent.md`) have `dir`\n\t// pointing at the manifest file itself, not a directory — there's\n\t// no sibling files to collect, just the one manifest.\n\tif (found.dir === found.manifestPath) {\n\t\tconst content = await fs.readFile(found.manifestPath, \"utf8\");\n\t\treturn [{ path: path.basename(found.manifestPath), content }];\n\t}\n\tawait collectFilesRecursive(found.dir, found.dir, out);\n\t// Sort for determinism (manifest first, then alphabetical)\n\tout.sort((a, b) => {\n\t\tconst aIsManifest = a.path === \"SKILL.md\" || a.path === \"AGENT.md\";\n\t\tconst bIsManifest = b.path === \"SKILL.md\" || b.path === \"AGENT.md\";\n\t\tif (aIsManifest && !bIsManifest) return -1;\n\t\tif (bIsManifest && !aIsManifest) return 1;\n\t\treturn a.path.localeCompare(b.path);\n\t});\n\treturn out;\n}\n\n// ─────────────────────────────────────────────────────────────────────\n// Internals\n// ─────────────────────────────────────────────────────────────────────\n\nconst SKIP_DIRS = new Set([\".git\", \"node_modules\", \".vscode\", \"dist\", \"build\", \"out\"]);\n\n/**\n * Directories that are never treated as candidate skill/agent parents\n * during the top-level catalog walk. `plugins/` is a distinct concept\n * from the skill/agent catalog itself — a plugin bundle\n * (`plugins/<scope>/<plugin-name>/{skills,agents}/...`) re-packages\n * ALREADY-cataloged official skills/agents (see the plugin's own\n * `catalog.json`, which lists them by name) purely for discovery\n * grouping. Walking into it produces duplicate `SkillEntry` values\n * with the SAME `repoId`+`name` as their real, top-level counterpart\n * (e.g. `skills/official/acreadiness-assess` AND\n * `plugins/official/acreadiness-cockpit/skills/acreadiness-assess`),\n * which breaks anything keyed on `repoId/name` (React list rendering,\n * the \"installed\" lookup, `SkillStore.get()`'s `.find()`).\n *\n * Scoped separately from `SKIP_DIRS` (used by both this walk AND\n * `collectFilesRecursive`) so an actual skill that happens to ship its\n * own `plugins/` asset folder still has that folder listed in its own\n * file detail view.\n */\nconst SKIP_TOP_LEVEL_SCAN_DIRS = new Set([...SKIP_DIRS, \"plugins\"]);\n\n/** Suffix that marks a standalone file as a flat agent manifest (no wrapping dir). */\nconst FLAT_AGENT_FILE_SUFFIX = \".agent.md\";\n\n/**\n * Recursive walk that yields SkillEntry values for any directory\n * containing a SKILL.md or AGENT.md file, PLUS any standalone\n * `<name>.agent.md` file (see class doc, layout style 4). The dir\n * itself is the \"entry root\" for directory-based entries; the\n * manifest file itself is the \"entry root\" for flat agent files.\n *\n * The optional `layout` descriptor (from `.serviceme-repo.json`)\n * widens the scan: included subdirs are walked even without a\n * manifest at the top level, and `treatFilesAsSkills` surfaces\n * `<name>.md` files inside them as skills (frontmatter parsed\n * from the file body).\n */\nasync function walkForEntries(\n\trootDir: string,\n\trepoId: string,\n\tout: SkillEntry[],\n\tlayout: RepoLayoutDescriptor = DEFAULT_REPO_LAYOUT\n): Promise<void> {\n\tlet dirents: import(\"node:fs\").Dirent[];\n\ttry {\n\t\tdirents = await fs.readdir(rootDir, { withFileTypes: true });\n\t} catch {\n\t\treturn;\n\t}\n\n\tfor (const d of dirents) {\n\t\tif (d.isFile() && d.name.endsWith(FLAT_AGENT_FILE_SUFFIX)) {\n\t\t\tconst filePath = path.join(rootDir, d.name);\n\t\t\tconst stat = await fs.stat(filePath);\n\t\t\tconst content = await fs.readFile(filePath, \"utf8\");\n\t\t\tconst parsed = extractFrontmatter(content);\n\t\t\tout.push({\n\t\t\t\trepoId,\n\t\t\t\tname: d.name.slice(0, -FLAT_AGENT_FILE_SUFFIX.length),\n\t\t\t\tkind: \"agent\",\n\t\t\t\tmanifestPath: filePath,\n\t\t\t\t// Sentinel: `dir === manifestPath` marks a flat single-file\n\t\t\t\t// entry (no directory of its own — see getFiles()).\n\t\t\t\tdir: filePath,\n\t\t\t\tfrontmatter: parsed?.data ?? {},\n\t\t\t\tmodifiedAt: stat.mtime.toISOString(),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (!d.isDirectory()) continue;\n\t\tif (SKIP_TOP_LEVEL_SCAN_DIRS.has(d.name)) continue;\n\t\tif (layout.exclude.includes(d.name)) continue;\n\t\tconst childDir = path.join(rootDir, d.name);\n\t\tconst kind = await detectKind(childDir);\n\t\tif (kind) {\n\t\t\tconst manifestFilename = kind === \"skill\" ? \"SKILL.md\" : \"AGENT.md\";\n\t\t\tconst manifestPath = path.join(childDir, manifestFilename);\n\t\t\tconst stat = await fs.stat(manifestPath);\n\t\t\tconst content = await fs.readFile(manifestPath, \"utf8\");\n\t\t\tconst parsed = extractFrontmatter(content);\n\t\t\tout.push({\n\t\t\t\trepoId,\n\t\t\t\tname: d.name,\n\t\t\t\tkind,\n\t\t\t\tmanifestPath,\n\t\t\t\tdir: childDir,\n\t\t\t\tfrontmatter: parsed?.data ?? {},\n\t\t\t\tmodifiedAt: stat.mtime.toISOString(),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\t// No manifest at the top — recurse, but ALSO check whether\n\t\t// the layout descriptor wants this subdir widened.\n\t\tawait walkForEntries(childDir, repoId, out, layout);\n\t\tif (layout.treatFilesAsSkills && layout.include.includes(d.name)) {\n\t\t\tawait surfaceFilesAsSkills(childDir, d.name, repoId, out);\n\t\t}\n\t}\n}\n\n/**\n * Walk `<includedSubdir>/<file>.md` and register each as a skill.\n * The `.md` file itself is the manifest — frontmatter (if any) is\n * parsed and surfaced. This is the awesome-copilot style: a\n * `prompts/` dir full of `<name>.md` files, no SKILL.md anywhere.\n */\nasync function surfaceFilesAsSkills(\n\tabsDir: string,\n\trelName: string,\n\trepoId: string,\n\tout: SkillEntry[]\n): Promise<void> {\n\tlet dirents: import(\"node:fs\").Dirent[];\n\ttry {\n\t\tdirents = await fs.readdir(absDir, { withFileTypes: true });\n\t} catch {\n\t\treturn;\n\t}\n\tfor (const d of dirents) {\n\t\tif (!d.isFile()) continue;\n\t\tif (!d.name.endsWith(\".md\")) continue;\n\t\tconst filePath = path.join(absDir, d.name);\n\t\tconst stat = await fs.stat(filePath);\n\t\tconst content = await fs.readFile(filePath, \"utf8\");\n\t\tconst parsed = extractFrontmatter(content);\n\t\t// Entry name = \"<subdir>/<file>\" (with the .md stripped) so it\n\t\t// stays unique even when two included subdirs share filenames.\n\t\tconst entryName = `${relName}/${d.name.replace(/\\.md$/, \"\")}`;\n\t\tout.push({\n\t\t\trepoId,\n\t\t\tname: entryName,\n\t\t\tkind: \"skill\",\n\t\t\tmanifestPath: filePath,\n\t\t\tdir: absDir,\n\t\t\tfrontmatter: parsed?.data ?? {},\n\t\t\tmodifiedAt: stat.mtime.toISOString(),\n\t\t});\n\t}\n}\n\n/**\n * Returns the SkillKind of a directory if it contains a manifest file,\n * or `null` if it doesn't. Manifest precedence: SKILL.md wins if both\n * are present (defensive — the spec separates skills and agents into\n * different subdir trees, so this collision shouldn't happen in\n * well-formed repos).\n */\nasync function detectKind(dir: string): Promise<SkillKind | null> {\n\tconst [hasSkill, hasAgent] = await Promise.all([\n\t\tfs\n\t\t\t.access(path.join(dir, \"SKILL.md\"))\n\t\t\t.then(() => true)\n\t\t\t.catch(() => false),\n\t\tfs\n\t\t\t.access(path.join(dir, \"AGENT.md\"))\n\t\t\t.then(() => true)\n\t\t\t.catch(() => false),\n\t]);\n\tif (hasSkill) return \"skill\";\n\tif (hasAgent) return \"agent\";\n\treturn null;\n}\n\n/**\n * Walk a skill/agent directory recursively, pushing every file into\n * `out` with its path RELATIVE to the entry root. Skips `.git/`,\n * `node_modules/`, and the SKIP_DIRS set.\n */\nasync function collectFilesRecursive(\n\tabsDir: string,\n\tentryRoot: string,\n\tout: SkillFile[]\n): Promise<void> {\n\tlet dirents: import(\"node:fs\").Dirent[];\n\ttry {\n\t\tdirents = await fs.readdir(absDir, { withFileTypes: true });\n\t} catch {\n\t\treturn;\n\t}\n\tfor (const d of dirents) {\n\t\tif (SKIP_DIRS.has(d.name)) continue;\n\t\tconst full = path.join(absDir, d.name);\n\t\tif (d.isDirectory()) {\n\t\t\tawait collectFilesRecursive(full, entryRoot, out);\n\t\t} else if (d.isFile()) {\n\t\t\tconst content = await fs.readFile(full, \"utf8\");\n\t\t\tout.push({ path: path.relative(entryRoot, full), content });\n\t\t}\n\t}\n}\n","/**\n * Spec §12 — Repo layout descriptor.\n *\n * Some third-party repos (awesome-copilot, composio) use a\n * non-default layout that mixes prompts / instructions / agents /\n * hooks / plugins at the root. The default SkillStore scan\n * (see ../skill-store/index.ts) only picks up directories that\n * contain a `SKILL.md` or `AGENT.md` manifest, so prompts stored\n * as `<subdir>/<name>.md` (no manifest) get missed.\n *\n * This module is the loader for the optional `.serviceme-repo.json`\n * marker file a repo author can drop in at the repo root to opt\n * into a wider scan. The schema:\n *\n * {\n * \"schema\": 1,\n * \"include\": [\"prompts\", \"instructions\"], // subdirs to treat as \"skill dirs\"\n * \"exclude\": [\"hooks\", \"plugins\"], // subdirs to skip\n * \"treatFilesAsSkills\": true // inside an included subdir, each .md file is a skill\n * }\n *\n * All four fields are optional. The defaults match the v0.2\n * walkForEntries behaviour (manifest-based, no marker, no\n * extensions), so adding the file is purely additive.\n *\n * @see docs/architecture/skill-agent-v2-repo.md §12\n */\n\nimport * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\n\nconst MARKER_FILENAME = \".serviceme-repo.json\";\nconst SUPPORTED_SCHEMA = 1;\n\nexport interface RepoLayoutDescriptor {\n\t/** Schema version. Currently always 1. */\n\tschema: number;\n\t/** Subdirs (relative to repo root) to walk for entries. */\n\tinclude: string[];\n\t/** Subdirs (relative to repo root) to skip, even if a parent is included. */\n\texclude: string[];\n\t/**\n\t * When true, an included subdir's `<name>.md` files are\n\t * surfaced as skills (frontmatter is parsed from each file as\n\t * a stand-in for SKILL.md). When false (default), the\n\t * include list only widens the recursion — the manifest rule\n\t * still applies.\n\t */\n\ttreatFilesAsSkills: boolean;\n}\n\n/**\n * Default descriptor used when no `.serviceme-repo.json` is\n * present. The shape is the same as what an empty marker would\n * produce, so callers don't have to special-case the \"no marker\"\n * branch.\n */\nexport const DEFAULT_REPO_LAYOUT: RepoLayoutDescriptor = {\n\tschema: SUPPORTED_SCHEMA,\n\tinclude: [],\n\texclude: [],\n\ttreatFilesAsSkills: false,\n};\n\n/**\n * Try to load a `.serviceme-repo.json` from the given repo root.\n * Returns `DEFAULT_REPO_LAYOUT` (not undefined) when the file is\n * absent — callers branch on `include.length` / `treatFilesAsSkills`\n * to decide whether to widen the scan.\n *\n * Malformed markers (bad JSON, wrong schema) are surfaced as\n * `null` so callers can warn the user instead of silently\n * treating them as the default.\n */\nexport async function loadRepoLayout(repoRoot: string): Promise<RepoLayoutDescriptor | null> {\n\tconst markerPath = path.join(repoRoot, MARKER_FILENAME);\n\tlet raw: string;\n\ttry {\n\t\traw = await fs.readFile(markerPath, \"utf8\");\n\t} catch {\n\t\treturn DEFAULT_REPO_LAYOUT;\n\t}\n\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch {\n\t\treturn null;\n\t}\n\tif (!parsed || typeof parsed !== \"object\") return null;\n\tconst obj = parsed as Record<string, unknown>;\n\n\tif (obj.schema !== SUPPORTED_SCHEMA) return null;\n\tif (!Array.isArray(obj.include) || obj.include.some((s) => typeof s !== \"string\")) {\n\t\treturn null;\n\t}\n\tif (!Array.isArray(obj.exclude) || obj.exclude.some((s) => typeof s !== \"string\")) {\n\t\treturn null;\n\t}\n\tif (typeof obj.treatFilesAsSkills !== \"boolean\") return null;\n\n\treturn {\n\t\tschema: SUPPORTED_SCHEMA,\n\t\tinclude: obj.include as string[],\n\t\texclude: obj.exclude as string[],\n\t\ttreatFilesAsSkills: obj.treatFilesAsSkills,\n\t};\n}\n\n/** Subdir name → entry kind override (rarely needed; default = manifest-driven). */\nexport type KindOverride = Map<string, \"skill\" | \"agent\">;\n","/**\n * Skill & Agent v2 — SkillStore Types\n *\n * SkillStore scans the local `~/.serviceme/repos/<id>/` tree to produce\n * a unified list of skills + agents. The store is the single source of\n * truth for \"what's available right now\" — the UI reads it; SubmitClient\n * reads it; install flows read it.\n *\n * Three repo layouts are supported in v1 (per spec §12):\n * 1. **Default** (`medalsoftchina-ms-skills`): `skills/<name>/SKILL.md`\n * + `agents/<name>/AGENT.md`.\n * 2. **Anthropic-style flat** (`anthropics/skills`): each subdir of\n * the repo root IS a skill — `<name>/SKILL.md` (no `skills/`\n * intermediate).\n * 3. **awesome-copilot style** (`github/awesome-copilot`): mixed\n * prompts/instructions/agents at the root.\n *\n * Normalization rule (spec §12): for every directory under the repo\n * root, peek for a `SKILL.md` or `AGENT.md` file. If present, register\n * it as a skill or agent respectively. This is more permissive than\n * guessing from the directory name and works for all three styles.\n *\n * The store is local-only and pure — no git, no network. Tests\n * construct a fixture repo tree and pass the root in directly.\n */\n\n/** What's available at this entry: a skill or an agent. */\nexport type SkillKind = \"skill\" | \"agent\";\n\n/** Top-level summary of a discovered skill/agent entry. */\nexport interface SkillEntry {\n\t/** Repository the entry was found in. */\n\trepoId: string;\n\t/** Skill or agent name (the directory name under the repo). */\n\tname: string;\n\tkind: SkillKind;\n\t/** Absolute path to the SKILL.md / AGENT.md file. */\n\tmanifestPath: string;\n\t/** Absolute path to the directory containing the entry's files. */\n\tdir: string;\n\t/** Parsed frontmatter (best-effort; raw key/value map). */\n\tfrontmatter: Record<string, unknown>;\n\t/** ISO timestamp of the manifest file's mtime. */\n\tmodifiedAt: string;\n}\n\n/** Full detail of an entry: summary + all the files inside the dir. */\nexport interface SkillDetail extends SkillEntry {\n\tfiles: SkillFile[];\n}\n\n/** A single file inside a skill / agent directory. */\nexport interface SkillFile {\n\t/** Path relative to the entry's directory. */\n\tpath: string;\n\t/** UTF-8 content. */\n\tcontent: string;\n}\n\n/** Sentinel error for store lookups that miss. */\nexport class SkillNotFoundError extends Error {\n\tconstructor(\n\t\tpublic readonly repoId: string,\n\t\tpublic readonly skillName: string\n\t) {\n\t\tsuper(`skill/agent not found: ${repoId}/${skillName}`);\n\t\tthis.name = \"SkillNotFoundError\";\n\t}\n}\n"],"mappings":"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","names":["fs","path","stat"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/submit/index.ts","../src/paths/userHome.ts","../src/submit/types.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport type { GitClient } from \"../git-client\";\nimport { getRepoDir } from \"../paths/userHome\";\nimport type { SkillFile } from \"../skill-store/types\";\nimport type { SubmitValidationRequest, SubmitValidationResponse } from \"./types\";\nimport { SubmitError } from \"./types\";\n\n// Re-export so consumers (bridge handlers, CLI commands) can detect\n// SubmitError via `instanceof` without reaching into the internal\n// `./types` module.\nexport { SubmitError } from \"./types\";\n\n/**\n * Skill & Agent v2 — SubmitClient (M4)\n *\n * SubmitClient orchestrates the \"validate then push\" flow described\n * in docs/architecture/skill-agent-v2-repo.md §5.7:\n *\n * 1. **validate** — POST the candidate files to the server's\n * `/api/v1/skills/validate` endpoint (5 deny reasons: repo_not_\n * writable, file_too_large, path_traversal, invalid_frontmatter,\n * name_conflict).\n * 2. **write** — materialize the files into the local repo clone at\n * `~/.serviceme/repos/<repoId>/skills/<name>/` (or `agents/`).\n * 3. **commit** — `git add . && git commit -m \"feat(skills): add <name>\"`.\n * 4. **push** — `git push origin <branch>` via the server proxy.\n *\n * The client is intentionally thin: it does NOT do its own validation\n * (the server is the gate), and it does NOT cache anything across\n * calls. The single source of truth for \"is this submission allowed?\"\n * is the server's validate endpoint.\n *\n * @see docs/architecture/skill-agent-v2-repo.md §5.7 SubmitClient\n */\n\nexport interface SubmitOptions {\n\t/** Override the server's validate URL. Defaults to `http://localhost:3000/api/v1`. */\n\tserverBaseUrl?: string;\n\t/** Override the branch to push. Defaults to the repo's `branch` field. */\n\tbranch?: string;\n\t/** Skip the actual push (for tests + dry-runs). When true, step 4 returns a synthetic PushResult. */\n\tskipPush?: boolean;\n}\n\nexport interface SubmitResult {\n\trepoId: string;\n\tskillName: string;\n\tcommitSha: string;\n\tpushedRef?: string;\n\tpushedSha?: string;\n}\n\nexport interface SubmitClientOptions {\n\tgitClient: GitClient;\n\t/** Lookup the default branch for a repo (config-driven). */\n\tgetRepoBranch?: (repoId: string) => string | undefined;\n\t/** HTTP fetch impl (defaults to the global `fetch`). */\n\tfetcher?: typeof fetch;\n\t/** Default server base URL when no override is supplied. */\n\tdefaultServerBaseUrl?: string;\n}\n\nexport class SubmitClient {\n\tprivate readonly git: GitClient;\n\tprivate readonly getRepoBranch: (repoId: string) => string | undefined;\n\tprivate readonly fetcher: typeof fetch;\n\tprivate readonly defaultServerBaseUrl: string;\n\n\tconstructor(opts: SubmitClientOptions) {\n\t\tthis.git = opts.gitClient;\n\t\tthis.getRepoBranch = opts.getRepoBranch ?? (() => undefined);\n\t\tthis.fetcher = opts.fetcher ?? (globalThis.fetch as typeof fetch);\n\t\tthis.defaultServerBaseUrl = opts.defaultServerBaseUrl ?? \"http://localhost:3000\";\n\t}\n\n\t/**\n\t * Validate-only path. Useful for the UI's \"Save Draft\" flow which\n\t * wants to surface validation errors without committing or pushing.\n\t */\n\tasync validate(req: SubmitValidationRequest): Promise<SubmitValidationResponse> {\n\t\tconst url = `${this.defaultServerBaseUrl}/api/v1/skills/validate`;\n\t\tconst res = await this.fetcher(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\tbody: JSON.stringify(req),\n\t\t});\n\t\tif (!res.ok) {\n\t\t\tthrow new SubmitError(\"network_error\", `validate request failed: HTTP ${res.status}`);\n\t\t}\n\t\treturn (await res.json()) as SubmitValidationResponse;\n\t}\n\n\t/**\n\t * Full submit pipeline. Throws `SubmitError` on:\n\t * - validate deny (reason echoed)\n\t * - network failure (network_error)\n\t * - local write failure\n\t * - commit/push failure\n\t */\n\tasync submit(\n\t\trepoId: string,\n\t\tskillName: string,\n\t\tfiles: SkillFile[],\n\t\topts: SubmitOptions = {}\n\t): Promise<SubmitResult> {\n\t\t// (1) Validate\n\t\tconst v = await this.validate({ repoId, skillName, files });\n\t\tif (!v.allow) {\n\t\t\tthrow new SubmitError(v.reason ?? \"unknown\", v.detail ?? \"validation denied\");\n\t\t}\n\n\t\t// (2) Write files into the local repo clone\n\t\tconst localRepoPath = getRepoDir(repoId);\n\t\tconst targetDir = path.join(localRepoPath, \"skills\", skillName);\n\t\tawait fs.mkdir(targetDir, { recursive: true });\n\t\tfor (const f of files) {\n\t\t\tconst full = path.join(targetDir, f.path);\n\t\t\tawait fs.mkdir(path.dirname(full), { recursive: true });\n\t\t\tconst tmp = `${full}.${process.pid}.${Date.now()}.tmp`;\n\t\t\tawait fs.writeFile(tmp, f.content, \"utf8\");\n\t\t\tawait fs.rename(tmp, full);\n\t\t}\n\n\t\t// (3) Commit (convention: `feat(skills): add <name>`)\n\t\tconst commitMessage = `feat(skills): add ${skillName}`;\n\t\tconst { commitSha } = await this.git.commit(localRepoPath, commitMessage);\n\n\t\t// (4) Push\n\t\tlet pushedRef: string | undefined;\n\t\tlet pushedSha: string | undefined;\n\t\tif (!opts.skipPush) {\n\t\t\tconst branch = opts.branch ?? this.getRepoBranch(repoId) ?? \"main\";\n\t\t\tconst pushResult = await this.git.push(repoId, localRepoPath, branch);\n\t\t\tpushedRef = pushResult.ref;\n\t\t\tpushedSha = pushResult.commitSha;\n\t\t}\n\n\t\treturn {\n\t\t\trepoId,\n\t\t\tskillName,\n\t\t\tcommitSha,\n\t\t\tpushedRef,\n\t\t\tpushedSha,\n\t\t};\n\t}\n}\n","import * as os from \"node:os\";\nimport * as path from \"node:path\";\n\n/**\n * SERVICEME user home directory helpers.\n *\n * All paths resolve under {@link getServicemeHome}, which is either the\n * `SERVICEME_HOME` environment variable (when set and non-empty) or\n * `$HOME/.serviceme` on POSIX / `%USERPROFILE%\\.serviceme` on Windows.\n *\n * Tests inject `homeDir` and `servicemeHomeEnv` overrides via\n * {@link setUserHomeOverrides} / {@link resetUserHomeOverrides} so they can\n * exercise the path logic without touching the real user environment.\n */\n\n/** Layout constants — kept in one place so other modules can reuse them. */\nexport const SERVICEME_DIR_NAME = \".serviceme\";\nexport const REPOS_SUBDIR = \"repos\";\nexport const CACHE_SUBDIR = \"cache\";\nexport const DRAFTS_SUBDIR = \"drafts\";\nexport const SKILL_DRAFTS_SUBDIR = \"skills\";\nexport const AGENT_DRAFTS_SUBDIR = \"agents\";\nexport const REPOS_CONFIG_FILENAME = \"repos.json\";\nexport const WORKSPACES_SUBDIR = \"workspaces\";\nexport const WORKSPACE_CONTENT_STATE_FILENAME = \"copilot-content-state.json\";\n\n/** rev.20 — r7 BYOM Server Proxy toggle self-state (read by ext + CLI + server). */\nexport const SERVER_PROXY_GLOBAL_FILENAME = \"server-proxy.json\";\n\n/**\n * Repo id regex — used to validate any `repoId` argument before it is joined\n * into a filesystem path. Keeps path traversal attempts out and gives us a\n * predictable on-disk shape.\n */\nexport const SAFE_REPO_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;\n\n/** Environment variable that overrides the user-home root directory. */\nexport const SERVICEME_HOME_ENV = \"SERVICEME_HOME\";\n\n/**\n * Test seam: lets unit tests inject deterministic values for `os.homedir()`\n * and the `SERVICEME_HOME` env override without actually mutating\n * `process.env` (which would leak into other tests).\n */\ninterface UserHomeOverrides {\n\thomeDir?: string | undefined;\n\tservicemeHomeEnv?: string | undefined;\n\tplatform?: NodeJS.Platform | undefined;\n}\n\nlet activeOverrides: UserHomeOverrides = {};\n\nexport function setUserHomeOverrides(overrides: UserHomeOverrides): void {\n\tactiveOverrides = { ...overrides };\n}\n\nexport function resetUserHomeOverrides(): void {\n\tactiveOverrides = {};\n}\n\nfunction resolveHomeDir(): string {\n\tconst injected = activeOverrides.homeDir;\n\tif (injected !== undefined) {\n\t\treturn injected;\n\t}\n\treturn os.homedir();\n}\n\nfunction resolveServicemeHomeEnv(): string | undefined {\n\tconst injected = activeOverrides.servicemeHomeEnv;\n\tif (injected !== undefined) {\n\t\t// Treat empty string as \"not set\" — `process.env` always returns a string\n\t\t// but tests may deliberately pass \"\" to opt out.\n\t\treturn injected.length > 0 ? injected : undefined;\n\t}\n\tconst envValue = process.env[SERVICEME_HOME_ENV];\n\treturn envValue && envValue.length > 0 ? envValue : undefined;\n}\n\nfunction resolvePlatform(): NodeJS.Platform {\n\treturn activeOverrides.platform ?? process.platform;\n}\n\nexport function assertSafeRepoId(repoId: string): string {\n\tif (typeof repoId !== \"string\" || repoId.length === 0 || !SAFE_REPO_ID_PATTERN.test(repoId)) {\n\t\tthrow new Error(\n\t\t\t`Invalid repo id: ${JSON.stringify(repoId)}. ` +\n\t\t\t\t`Must match ${SAFE_REPO_ID_PATTERN} (alphanumeric start, then ` +\n\t\t\t\t`alphanumerics / underscores / hyphens, ≤ 64 chars).`\n\t\t);\n\t}\n\treturn repoId;\n}\n\n/**\n * The raw OS home directory (`os.homedir()`), honoring test overrides\n * ({@link setUserHomeOverrides}). Exported for callers that need a\n * home-relative path *outside* of `~/.serviceme` — e.g. the\n * `~/.agents/{skills,agents}` convention used by `SkillLinker` for\n * user-scope links.\n */\nexport function getHomeDir(): string {\n\treturn resolveHomeDir();\n}\n\n/**\n * Root directory for all SERVICEME user-level state. Resolves to\n * `${SERVICEME_HOME}` when that env var is set, otherwise `${HOME}/.serviceme`\n * (POSIX) or `%USERPROFILE%\\.serviceme` (Windows via `os.homedir`).\n */\nexport function getServicemeHome(): string {\n\tconst override = resolveServicemeHomeEnv();\n\tif (override !== undefined) {\n\t\treturn path.resolve(override);\n\t}\n\treturn path.join(resolveHomeDir(), SERVICEME_DIR_NAME);\n}\n\n/** `$HOME/.serviceme/repos` (or `${SERVICEME_HOME}/repos`). */\nexport function getReposDir(): string {\n\treturn path.join(getServicemeHome(), REPOS_SUBDIR);\n}\n\n/** `$HOME/.serviceme/repos/<repoId>` — validates `repoId` first. */\nexport function getRepoDir(repoId: string): string {\n\treturn path.join(getReposDir(), assertSafeRepoId(repoId));\n}\n\n/** `$HOME/.serviceme/cache` — generic per-user cache. */\nexport function getCacheDir(): string {\n\treturn path.join(getServicemeHome(), CACHE_SUBDIR);\n}\n\n/** `$HOME/.serviceme/cache/<repoId>` — per-repo sync state lives here. */\nexport function getRepoCacheDir(repoId: string): string {\n\treturn path.join(getCacheDir(), assertSafeRepoId(repoId));\n}\n\n/** `$HOME/.serviceme/drafts` — local edits not yet pushed upstream. */\nexport function getDraftsDir(): string {\n\treturn path.join(getServicemeHome(), DRAFTS_SUBDIR);\n}\n\n/** `$HOME/.serviceme/drafts/skills` — local skill drafts. */\nexport function getSkillDraftsDir(): string {\n\treturn path.join(getDraftsDir(), SKILL_DRAFTS_SUBDIR);\n}\n\n/** `$HOME/.serviceme/drafts/agents` — local agent drafts. */\nexport function getAgentDraftsDir(): string {\n\treturn path.join(getDraftsDir(), AGENT_DRAFTS_SUBDIR);\n}\n\n/**\n * `$HOME/.serviceme/repos.json` — the single config entry point for repo\n * metadata. Always under the resolved home root (i.e. follows\n * `SERVICEME_HOME` overrides too).\n */\nexport function getReposConfigPath(): string {\n\treturn path.join(getServicemeHome(), REPOS_CONFIG_FILENAME);\n}\n\n/** `~/.serviceme/workspaces` — machine-local per-workspace state root. */\nexport function getWorkspacesDir(): string {\n\treturn path.join(getServicemeHome(), WORKSPACES_SUBDIR);\n}\n\n/**\n * Convenience helper for callers that need to switch behaviour on platform\n * (e.g. `SkillLinker` chooses symlink vs junction). Exported mostly so tests\n * can pin the value without touching `process.platform` directly.\n */\nexport function getHomePlatform(): NodeJS.Platform {\n\treturn resolvePlatform();\n}\n\n// ─── Scheduled Tasks paths (added by M1.5.3) ───────────────────────────────\n\n/** Layout constants for scheduled-tasks files. Kept here so other modules\n * can reuse them and the names stay in sync with the design doc. */\nexport const SCHEDULED_TASKS_CONFIG_FILENAME = \"scheduled-tasks.json\";\nexport const SCHEDULED_TASKS_LOG_FILENAME = \"scheduled-tasks-log.json\";\nexport const SCHEDULER_PID_FILENAME = \"scheduler.pid\";\nexport const SCHEDULER_LOCK_FILENAME = \"scheduler.lock\";\nexport const SCHEDULER_LOG_FILENAME = \"scheduler.log\";\nexport const MIGRATION_FAILURES_FILENAME = \"migration-failures.json\";\nexport const KNOWN_WORKSPACES_FILENAME = \"known-workspaces.json\";\n\n/** `~/.serviceme/scheduled-tasks.json` — the global v2 task list. */\nexport function getScheduledTasksConfigPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULED_TASKS_CONFIG_FILENAME);\n}\n\n/** `~/.serviceme/scheduled-tasks-log.json` — the global execution log (200 LRU). */\nexport function getScheduledTasksLogPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULED_TASKS_LOG_FILENAME);\n}\n\n/** `~/.serviceme/scheduler.pid` — global daemon PID file. */\nexport function getSchedulerPidPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULER_PID_FILENAME);\n}\n\n/** `~/.serviceme/scheduler.lock` — global daemon startup flock. */\nexport function getSchedulerLockPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULER_LOCK_FILENAME);\n}\n\n/** `~/.serviceme/scheduler.log` — global daemon log (daemon lifecycle, not task execution). */\nexport function getSchedulerLogPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULER_LOG_FILENAME);\n}\n\n/** `~/.serviceme/migration-failures.json` — diagnostics for failed v1→v2 imports. */\nexport function getMigrationFailuresPath(): string {\n\treturn path.join(getServicemeHome(), MIGRATION_FAILURES_FILENAME);\n}\n\n/** `~/.serviceme/known-workspaces.json` — workspace list the extension has seen. */\nexport function getKnownWorkspacesPath(): string {\n\treturn path.join(getServicemeHome(), KNOWN_WORKSPACES_FILENAME);\n}\n\n// ─── r7 BYOM Server Proxy (rev.20) ─────────────────────────────────────────\n\n/**\n * `~/.serviceme/server-proxy.json` — r7 BYOM Server Proxy toggle\n * self-state. Written by the extension's `ProxyConfigService`, readable\n * by the CLI / Server so they can decide whether to route traffic\n * through the corporate proxy without booting VS Code.\n *\n * Lives under user-level `~/.serviceme/` (not under a workspace) because\n * the toggle is a *user preference* — the same human enabling it on\n * machine A should be able to rely on it on machine B after sync, not\n * have it disappear when they switch repos.\n */\nexport function getServerProxyGlobalPath(): string {\n\treturn path.join(getServicemeHome(), SERVER_PROXY_GLOBAL_FILENAME);\n}\n\n// ─── Phase 5 (auth + device + toolbox) placeholders (Spec §11.12) ────────\n\n/** Layout constants for the Phase 5 client-side state files. The server\n * already has the corresponding routes (`/api/v1/auth/...`,\n * `/api/v1/device/...`, etc.) — the client just needs the on-disk\n * file paths + a first-run bootstrap so the auth/device/toolbox\n * services can open + read them without a per-call existence check. */\nexport const CREDENTIALS_CONFIG_FILENAME = \"credentials.json\";\nexport const DEVICE_JSON_FILENAME = \"device.json\";\nexport const TOOLBOX_JSON_FILENAME = \"toolbox.json\";\nexport const MACHINE_ID_FILENAME = \"machine-id\";\nexport const PROFILES_JSON_FILENAME = \"profiles.json\";\n\n/** `~/.serviceme/credentials.json` — better-auth session tokens + refresh state (client-side cache). */\nexport function getCredentialsConfigPath(): string {\n\treturn path.join(getServicemeHome(), CREDENTIALS_CONFIG_FILENAME);\n}\n\n/** `~/.serviceme/device.json` — device enrollment payload (id, public key fingerprint, claimed-by). */\nexport function getDeviceJsonPath(): string {\n\treturn path.join(getServicemeHome(), DEVICE_JSON_FILENAME);\n}\n\n/** `~/.serviceme/toolbox.json` — local toolbox state (installed tool refs, per-user prefs). */\nexport function getToolboxJsonPath(): string {\n\treturn path.join(getServicemeHome(), TOOLBOX_JSON_FILENAME);\n}\n\n/** `~/.serviceme/machine-id` — opaque stable per-install id (uuid v4 string, no JSON wrapper). */\nexport function getMachineIdPath(): string {\n\treturn path.join(getServicemeHome(), MACHINE_ID_FILENAME);\n}\n\n/** `~/.serviceme/profiles.json` — cached projection of server-side profile rows; refresh on demand. */\nexport function getProfilesJsonPath(): string {\n\treturn path.join(getServicemeHome(), PROFILES_JSON_FILENAME);\n}\n","/**\n * Skill & Agent v2 — SubmitClient Types (M4)\n *\n * Mirrors the server's POST /api/v1/skills/validate contract (M2\n * SubmitApi). Defined here as a separate types file so the test\n * fixtures + the client can both import without circular deps.\n */\n\n/** Reasons the server may deny a submission. */\nexport type DenyReason =\n\t| \"repo_not_writable\"\n\t| \"file_too_large\"\n\t| \"path_traversal\"\n\t| \"invalid_frontmatter\"\n\t| \"name_conflict\"\n\t/** Local-client-side errors that don't come from the server. */\n\t| \"network_error\"\n\t| \"write_error\"\n\t| \"commit_error\"\n\t| \"push_error\"\n\t| \"unknown\";\n\n/** Request payload. Identical to the server's SubmitValidationRequest. */\nexport interface SubmitValidationRequest {\n\trepoId: string;\n\tskillName: string;\n\tfiles: Array<{ path: string; content: string }>;\n}\n\n/** Response payload. Identical to the server's SubmitValidationResponse. */\nexport interface SubmitValidationResponse {\n\tallow: boolean;\n\treason?: DenyReason;\n\tdetail?: string;\n}\n\n/** Sentinel error thrown by SubmitClient.submit() / validate(). */\nexport class SubmitError extends Error {\n\treadonly reason: DenyReason;\n\treadonly detail: string;\n\treadonly status?: number;\n\n\tconstructor(reason: DenyReason, detail: string, options?: { status?: number }) {\n\t\tsuper(`submit failed (${reason}): ${detail}`);\n\t\tthis.name = \"SubmitError\";\n\t\tthis.reason = reason;\n\t\tthis.detail = detail;\n\t\tthis.status = options?.status;\n\t}\n}\n"],"mappings":"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","names":["path"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/submit/index.ts","../src/paths/userHome.ts","../src/submit/types.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport type { GitClient } from \"../git-client\";\nimport { getRepoDir } from \"../paths/userHome\";\nimport type { SkillFile } from \"../skill-store/types\";\nimport type { SubmitValidationRequest, SubmitValidationResponse } from \"./types\";\nimport { SubmitError } from \"./types\";\n\n// Re-export so consumers (bridge handlers, CLI commands) can detect\n// SubmitError via `instanceof` without reaching into the internal\n// `./types` module.\nexport { SubmitError } from \"./types\";\n\n/**\n * Skill & Agent v2 — SubmitClient (M4)\n *\n * SubmitClient orchestrates the \"validate then push\" flow described\n * in docs/architecture/skill-agent-v2-repo.md §5.7:\n *\n * 1. **validate** — POST the candidate files to the server's\n * `/api/v1/skills/validate` endpoint (5 deny reasons: repo_not_\n * writable, file_too_large, path_traversal, invalid_frontmatter,\n * name_conflict).\n * 2. **write** — materialize the files into the local repo clone at\n * `~/.serviceme/repos/<repoId>/skills/<name>/` (or `agents/`).\n * 3. **commit** — `git add . && git commit -m \"feat(skills): add <name>\"`.\n * 4. **push** — `git push origin <branch>` via the server proxy.\n *\n * The client is intentionally thin: it does NOT do its own validation\n * (the server is the gate), and it does NOT cache anything across\n * calls. The single source of truth for \"is this submission allowed?\"\n * is the server's validate endpoint.\n *\n * @see docs/architecture/skill-agent-v2-repo.md §5.7 SubmitClient\n */\n\nexport interface SubmitOptions {\n\t/** Override the server's validate URL. Defaults to `http://localhost:3000/api/v1`. */\n\tserverBaseUrl?: string;\n\t/** Override the branch to push. Defaults to the repo's `branch` field. */\n\tbranch?: string;\n\t/** Skip the actual push (for tests + dry-runs). When true, step 4 returns a synthetic PushResult. */\n\tskipPush?: boolean;\n}\n\nexport interface SubmitResult {\n\trepoId: string;\n\tskillName: string;\n\tcommitSha: string;\n\tpushedRef?: string;\n\tpushedSha?: string;\n}\n\nexport interface SubmitClientOptions {\n\tgitClient: GitClient;\n\t/** Lookup the default branch for a repo (config-driven). */\n\tgetRepoBranch?: (repoId: string) => string | undefined;\n\t/** HTTP fetch impl (defaults to the global `fetch`). */\n\tfetcher?: typeof fetch;\n\t/** Default server base URL when no override is supplied. */\n\tdefaultServerBaseUrl?: string;\n}\n\nexport class SubmitClient {\n\tprivate readonly git: GitClient;\n\tprivate readonly getRepoBranch: (repoId: string) => string | undefined;\n\tprivate readonly fetcher: typeof fetch;\n\tprivate readonly defaultServerBaseUrl: string;\n\n\tconstructor(opts: SubmitClientOptions) {\n\t\tthis.git = opts.gitClient;\n\t\tthis.getRepoBranch = opts.getRepoBranch ?? (() => undefined);\n\t\tthis.fetcher = opts.fetcher ?? (globalThis.fetch as typeof fetch);\n\t\tthis.defaultServerBaseUrl = opts.defaultServerBaseUrl ?? \"http://localhost:3000\";\n\t}\n\n\t/**\n\t * Validate-only path. Useful for the UI's \"Save Draft\" flow which\n\t * wants to surface validation errors without committing or pushing.\n\t */\n\tasync validate(req: SubmitValidationRequest): Promise<SubmitValidationResponse> {\n\t\tconst url = `${this.defaultServerBaseUrl}/api/v1/skills/validate`;\n\t\tconst res = await this.fetcher(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\tbody: JSON.stringify(req),\n\t\t});\n\t\tif (!res.ok) {\n\t\t\tthrow new SubmitError(\"network_error\", `validate request failed: HTTP ${res.status}`);\n\t\t}\n\t\treturn (await res.json()) as SubmitValidationResponse;\n\t}\n\n\t/**\n\t * Full submit pipeline. Throws `SubmitError` on:\n\t * - validate deny (reason echoed)\n\t * - network failure (network_error)\n\t * - local write failure\n\t * - commit/push failure\n\t */\n\tasync submit(\n\t\trepoId: string,\n\t\tskillName: string,\n\t\tfiles: SkillFile[],\n\t\topts: SubmitOptions = {}\n\t): Promise<SubmitResult> {\n\t\t// (1) Validate\n\t\tconst v = await this.validate({ repoId, skillName, files });\n\t\tif (!v.allow) {\n\t\t\tthrow new SubmitError(v.reason ?? \"unknown\", v.detail ?? \"validation denied\");\n\t\t}\n\n\t\t// (2) Write files into the local repo clone\n\t\tconst localRepoPath = getRepoDir(repoId);\n\t\tconst targetDir = path.join(localRepoPath, \"skills\", skillName);\n\t\tawait fs.mkdir(targetDir, { recursive: true });\n\t\tfor (const f of files) {\n\t\t\tconst full = path.join(targetDir, f.path);\n\t\t\tawait fs.mkdir(path.dirname(full), { recursive: true });\n\t\t\tconst tmp = `${full}.${process.pid}.${Date.now()}.tmp`;\n\t\t\tawait fs.writeFile(tmp, f.content, \"utf8\");\n\t\t\tawait fs.rename(tmp, full);\n\t\t}\n\n\t\t// (3) Commit (convention: `feat(skills): add <name>`)\n\t\tconst commitMessage = `feat(skills): add ${skillName}`;\n\t\tconst { commitSha } = await this.git.commit(localRepoPath, commitMessage);\n\n\t\t// (4) Push\n\t\tlet pushedRef: string | undefined;\n\t\tlet pushedSha: string | undefined;\n\t\tif (!opts.skipPush) {\n\t\t\tconst branch = opts.branch ?? this.getRepoBranch(repoId) ?? \"main\";\n\t\t\tconst pushResult = await this.git.push(repoId, localRepoPath, branch);\n\t\t\tpushedRef = pushResult.ref;\n\t\t\tpushedSha = pushResult.commitSha;\n\t\t}\n\n\t\treturn {\n\t\t\trepoId,\n\t\t\tskillName,\n\t\t\tcommitSha,\n\t\t\tpushedRef,\n\t\t\tpushedSha,\n\t\t};\n\t}\n}\n","import * as os from \"node:os\";\nimport * as path from \"node:path\";\n\n/**\n * SERVICEME user home directory helpers.\n *\n * All paths resolve under {@link getServicemeHome}, which is either the\n * `SERVICEME_HOME` environment variable (when set and non-empty) or\n * `$HOME/.serviceme` on POSIX / `%USERPROFILE%\\.serviceme` on Windows.\n *\n * Tests inject `homeDir` and `servicemeHomeEnv` overrides via\n * {@link setUserHomeOverrides} / {@link resetUserHomeOverrides} so they can\n * exercise the path logic without touching the real user environment.\n */\n\n/** Layout constants — kept in one place so other modules can reuse them. */\nexport const SERVICEME_DIR_NAME = \".serviceme\";\nexport const REPOS_SUBDIR = \"repos\";\nexport const CACHE_SUBDIR = \"cache\";\nexport const DRAFTS_SUBDIR = \"drafts\";\nexport const SKILL_DRAFTS_SUBDIR = \"skills\";\nexport const AGENT_DRAFTS_SUBDIR = \"agents\";\nexport const REPOS_CONFIG_FILENAME = \"repos.json\";\nexport const WORKSPACES_SUBDIR = \"workspaces\";\nexport const WORKSPACE_CONTENT_STATE_FILENAME = \"copilot-content-state.json\";\n\n/** rev.20 — r7 BYOM Server Proxy toggle self-state (read by ext + CLI + server). */\nexport const SERVER_PROXY_GLOBAL_FILENAME = \"server-proxy.json\";\n\n/**\n * Repo id regex — used to validate any `repoId` argument before it is joined\n * into a filesystem path. Keeps path traversal attempts out and gives us a\n * predictable on-disk shape.\n */\nexport const SAFE_REPO_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;\n\n/** Environment variable that overrides the user-home root directory. */\nexport const SERVICEME_HOME_ENV = \"SERVICEME_HOME\";\n\n/**\n * Test seam: lets unit tests inject deterministic values for `os.homedir()`\n * and the `SERVICEME_HOME` env override without actually mutating\n * `process.env` (which would leak into other tests).\n */\ninterface UserHomeOverrides {\n\thomeDir?: string | undefined;\n\tservicemeHomeEnv?: string | undefined;\n\tplatform?: NodeJS.Platform | undefined;\n}\n\nlet activeOverrides: UserHomeOverrides = {};\n\nexport function setUserHomeOverrides(overrides: UserHomeOverrides): void {\n\tactiveOverrides = { ...overrides };\n}\n\nexport function resetUserHomeOverrides(): void {\n\tactiveOverrides = {};\n}\n\nfunction resolveHomeDir(): string {\n\tconst injected = activeOverrides.homeDir;\n\tif (injected !== undefined) {\n\t\treturn injected;\n\t}\n\treturn os.homedir();\n}\n\nfunction resolveServicemeHomeEnv(): string | undefined {\n\tconst injected = activeOverrides.servicemeHomeEnv;\n\tif (injected !== undefined) {\n\t\t// Treat empty string as \"not set\" — `process.env` always returns a string\n\t\t// but tests may deliberately pass \"\" to opt out.\n\t\treturn injected.length > 0 ? injected : undefined;\n\t}\n\tconst envValue = process.env[SERVICEME_HOME_ENV];\n\treturn envValue && envValue.length > 0 ? envValue : undefined;\n}\n\nfunction resolvePlatform(): NodeJS.Platform {\n\treturn activeOverrides.platform ?? process.platform;\n}\n\nexport function assertSafeRepoId(repoId: string): string {\n\tif (typeof repoId !== \"string\" || repoId.length === 0 || !SAFE_REPO_ID_PATTERN.test(repoId)) {\n\t\tthrow new Error(\n\t\t\t`Invalid repo id: ${JSON.stringify(repoId)}. ` +\n\t\t\t\t`Must match ${SAFE_REPO_ID_PATTERN} (alphanumeric start, then ` +\n\t\t\t\t`alphanumerics / underscores / hyphens, ≤ 64 chars).`\n\t\t);\n\t}\n\treturn repoId;\n}\n\n/**\n * The raw OS home directory (`os.homedir()`), honoring test overrides\n * ({@link setUserHomeOverrides}). Exported for callers that need a\n * home-relative path *outside* of `~/.serviceme` — e.g. the\n * `~/.agents/{skills,agents}` convention used by `SkillLinker` for\n * user-scope links.\n */\nexport function getHomeDir(): string {\n\treturn resolveHomeDir();\n}\n\n/**\n * Root directory for all SERVICEME user-level state. Resolves to\n * `${SERVICEME_HOME}` when that env var is set, otherwise `${HOME}/.serviceme`\n * (POSIX) or `%USERPROFILE%\\.serviceme` (Windows via `os.homedir`).\n */\nexport function getServicemeHome(): string {\n\tconst override = resolveServicemeHomeEnv();\n\tif (override !== undefined) {\n\t\treturn path.resolve(override);\n\t}\n\treturn path.join(resolveHomeDir(), SERVICEME_DIR_NAME);\n}\n\n/** `$HOME/.serviceme/repos` (or `${SERVICEME_HOME}/repos`). */\nexport function getReposDir(): string {\n\treturn path.join(getServicemeHome(), REPOS_SUBDIR);\n}\n\n/** `$HOME/.serviceme/repos/<repoId>` — validates `repoId` first. */\nexport function getRepoDir(repoId: string): string {\n\treturn path.join(getReposDir(), assertSafeRepoId(repoId));\n}\n\n/** `$HOME/.serviceme/cache` — generic per-user cache. */\nexport function getCacheDir(): string {\n\treturn path.join(getServicemeHome(), CACHE_SUBDIR);\n}\n\n/** `$HOME/.serviceme/cache/<repoId>` — per-repo sync state lives here. */\nexport function getRepoCacheDir(repoId: string): string {\n\treturn path.join(getCacheDir(), assertSafeRepoId(repoId));\n}\n\n/** `$HOME/.serviceme/drafts` — local edits not yet pushed upstream. */\nexport function getDraftsDir(): string {\n\treturn path.join(getServicemeHome(), DRAFTS_SUBDIR);\n}\n\n/** `$HOME/.serviceme/drafts/skills` — local skill drafts. */\nexport function getSkillDraftsDir(): string {\n\treturn path.join(getDraftsDir(), SKILL_DRAFTS_SUBDIR);\n}\n\n/** `$HOME/.serviceme/drafts/agents` — local agent drafts. */\nexport function getAgentDraftsDir(): string {\n\treturn path.join(getDraftsDir(), AGENT_DRAFTS_SUBDIR);\n}\n\n/**\n * `$HOME/.serviceme/repos.json` — the single config entry point for repo\n * metadata. Always under the resolved home root (i.e. follows\n * `SERVICEME_HOME` overrides too).\n */\nexport function getReposConfigPath(): string {\n\treturn path.join(getServicemeHome(), REPOS_CONFIG_FILENAME);\n}\n\n/** `~/.serviceme/workspaces` — machine-local per-workspace state root. */\nexport function getWorkspacesDir(): string {\n\treturn path.join(getServicemeHome(), WORKSPACES_SUBDIR);\n}\n\n/**\n * Convenience helper for callers that need to switch behaviour on platform\n * (e.g. `SkillLinker` chooses symlink vs junction). Exported mostly so tests\n * can pin the value without touching `process.platform` directly.\n */\nexport function getHomePlatform(): NodeJS.Platform {\n\treturn resolvePlatform();\n}\n\n// ─── Scheduled Tasks paths (added by M1.5.3) ───────────────────────────────\n\n/** Layout constants for scheduled-tasks files. Kept here so other modules\n * can reuse them and the names stay in sync with the design doc. */\nexport const SCHEDULED_TASKS_CONFIG_FILENAME = \"scheduled-tasks.json\";\nexport const SCHEDULED_TASKS_LOG_FILENAME = \"scheduled-tasks-log.json\";\nexport const SCHEDULER_PID_FILENAME = \"scheduler.pid\";\nexport const SCHEDULER_LOCK_FILENAME = \"scheduler.lock\";\nexport const SCHEDULER_LOG_FILENAME = \"scheduler.log\";\nexport const MIGRATION_FAILURES_FILENAME = \"migration-failures.json\";\nexport const KNOWN_WORKSPACES_FILENAME = \"known-workspaces.json\";\n\n/** `~/.serviceme/scheduled-tasks.json` — the global v2 task list. */\nexport function getScheduledTasksConfigPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULED_TASKS_CONFIG_FILENAME);\n}\n\n/** `~/.serviceme/scheduled-tasks-log.json` — the global execution log (200 LRU). */\nexport function getScheduledTasksLogPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULED_TASKS_LOG_FILENAME);\n}\n\n/** `~/.serviceme/scheduler.pid` — global daemon PID file. */\nexport function getSchedulerPidPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULER_PID_FILENAME);\n}\n\n/** `~/.serviceme/scheduler.lock` — global daemon startup flock. */\nexport function getSchedulerLockPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULER_LOCK_FILENAME);\n}\n\n/** `~/.serviceme/scheduler.log` — global daemon log (daemon lifecycle, not task execution). */\nexport function getSchedulerLogPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULER_LOG_FILENAME);\n}\n\n/** `~/.serviceme/migration-failures.json` — diagnostics for failed v1→v2 imports. */\nexport function getMigrationFailuresPath(): string {\n\treturn path.join(getServicemeHome(), MIGRATION_FAILURES_FILENAME);\n}\n\n/** `~/.serviceme/known-workspaces.json` — workspace list the extension has seen. */\nexport function getKnownWorkspacesPath(): string {\n\treturn path.join(getServicemeHome(), KNOWN_WORKSPACES_FILENAME);\n}\n\n// ─── r7 BYOM Server Proxy (rev.20) ─────────────────────────────────────────\n\n/**\n * `~/.serviceme/server-proxy.json` — r7 BYOM Server Proxy toggle\n * self-state. Written by the extension's `ProxyConfigService`, readable\n * by the CLI / Server so they can decide whether to route traffic\n * through the corporate proxy without booting VS Code.\n *\n * Lives under user-level `~/.serviceme/` (not under a workspace) because\n * the toggle is a *user preference* — the same human enabling it on\n * machine A should be able to rely on it on machine B after sync, not\n * have it disappear when they switch repos.\n */\nexport function getServerProxyGlobalPath(): string {\n\treturn path.join(getServicemeHome(), SERVER_PROXY_GLOBAL_FILENAME);\n}\n\n// ─── Phase 5 (auth + device + toolbox) placeholders (Spec §11.12) ────────\n\n/** Layout constants for the Phase 5 client-side state files. The server\n * already has the corresponding routes (`/api/v1/auth/...`,\n * `/api/v1/device/...`, etc.) — the client just needs the on-disk\n * file paths + a first-run bootstrap so the auth/device/toolbox\n * services can open + read them without a per-call existence check. */\nexport const CREDENTIALS_CONFIG_FILENAME = \"credentials.json\";\nexport const DEVICE_JSON_FILENAME = \"device.json\";\nexport const TOOLBOX_JSON_FILENAME = \"toolbox.json\";\nexport const MACHINE_ID_FILENAME = \"machine-id\";\nexport const PROFILES_JSON_FILENAME = \"profiles.json\";\n\n/** `~/.serviceme/credentials.json` — better-auth session tokens + refresh state (client-side cache). */\nexport function getCredentialsConfigPath(): string {\n\treturn path.join(getServicemeHome(), CREDENTIALS_CONFIG_FILENAME);\n}\n\n/** `~/.serviceme/device.json` — device enrollment payload (id, public key fingerprint, claimed-by). */\nexport function getDeviceJsonPath(): string {\n\treturn path.join(getServicemeHome(), DEVICE_JSON_FILENAME);\n}\n\n/** `~/.serviceme/toolbox.json` — local toolbox state (installed tool refs, per-user prefs). */\nexport function getToolboxJsonPath(): string {\n\treturn path.join(getServicemeHome(), TOOLBOX_JSON_FILENAME);\n}\n\n/** `~/.serviceme/machine-id` — opaque stable per-install id (uuid v4 string, no JSON wrapper). */\nexport function getMachineIdPath(): string {\n\treturn path.join(getServicemeHome(), MACHINE_ID_FILENAME);\n}\n\n/** `~/.serviceme/profiles.json` — cached projection of server-side profile rows; refresh on demand. */\nexport function getProfilesJsonPath(): string {\n\treturn path.join(getServicemeHome(), PROFILES_JSON_FILENAME);\n}\n","/**\n * Skill & Agent v2 — SubmitClient Types (M4)\n *\n * Mirrors the server's POST /api/v1/skills/validate contract (M2\n * SubmitApi). Defined here as a separate types file so the test\n * fixtures + the client can both import without circular deps.\n */\n\n/** Reasons the server may deny a submission. */\nexport type DenyReason =\n\t| \"repo_not_writable\"\n\t| \"file_too_large\"\n\t| \"path_traversal\"\n\t| \"invalid_frontmatter\"\n\t| \"name_conflict\"\n\t/** Local-client-side errors that don't come from the server. */\n\t| \"network_error\"\n\t| \"write_error\"\n\t| \"commit_error\"\n\t| \"push_error\"\n\t| \"unknown\";\n\n/** Request payload. Identical to the server's SubmitValidationRequest. */\nexport interface SubmitValidationRequest {\n\trepoId: string;\n\tskillName: string;\n\tfiles: Array<{ path: string; content: string }>;\n}\n\n/** Response payload. Identical to the server's SubmitValidationResponse. */\nexport interface SubmitValidationResponse {\n\tallow: boolean;\n\treason?: DenyReason;\n\tdetail?: string;\n}\n\n/** Sentinel error thrown by SubmitClient.submit() / validate(). */\nexport class SubmitError extends Error {\n\treadonly reason: DenyReason;\n\treadonly detail: string;\n\treadonly status?: number;\n\n\tconstructor(reason: DenyReason, detail: string, options?: { status?: number }) {\n\t\tsuper(`submit failed (${reason}): ${detail}`);\n\t\tthis.name = \"SubmitError\";\n\t\tthis.reason = reason;\n\t\tthis.detail = detail;\n\t\tthis.status = options?.status;\n\t}\n}\n"],"mappings":"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","names":["path"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/toolbox/index.ts","../src/toolbox/sort.ts","../src/toolbox/ToolboxStore.ts","../src/paths/userHome.ts","../src/toolbox/types.ts","../src/toolbox/ToolboxCore.ts"],"sourcesContent":["/**\n * toolbox barrel — Phase 5.2 public surface.\n *\n * Consumers import from `@serviceme/devtools-core/toolbox` (subpath\n * added in `package.json` `exports` map).\n *\n * Refs:\n * - 4.功能规划.md §2.3 — `toolbox/index.ts`\n */\n\nexport {\n\tmergeWithDefaults,\n\treindexOrder,\n\tsortByRecentFirst,\n\tsortByUserOrder,\n\ttouchLastUsedAt,\n} from \"./sort\";\nexport { DefaultToolImmutableError, ToolboxCore, type ToolboxCoreOptions } from \"./ToolboxCore\";\nexport {\n\tFsToolboxFileBackend,\n\ttype ToolboxFileBackend,\n\tToolboxStore,\n\ttype ToolboxStoreHooks,\n\ttype ToolboxStoreOptions,\n\tWORKSPACE_TOOLBOX_RELATIVE_PATH,\n} from \"./ToolboxStore\";\nexport {\n\tBUILTIN_DEFAULT_TOOLS,\n\ttype DefaultToolSeed,\n\ttype PersistedToolbox,\n\ttype ResolvedToolbox,\n\tTOOLBOX_JSON_SCHEMA_VERSION,\n\ttype ToolboxPatch,\n} from \"./types\";\n","/**\n * toolbox sort + dedup — pure helpers, no I/O.\n *\n * The Extension's `ToolBoxService` keeps a stable `order` index and\n * surfaces \"recently used\" via the `lastUsedAt` ISO timestamp. These\n * helpers codify that algorithm so the CLI and Extension agree on the\n * \"最近使用置顶\" UX.\n *\n * Refs:\n * - 4.功能规划.md §2.3 — `sort.ts 排序与去重纯函数`\n */\n\nimport type { ExternalTool } from \"@serviceme/devtools-protocol\";\n\n/**\n * Parsed timestamp from `lastUsedAt`. Returns 0 when the field is\n * missing or unparseable (so the entry falls below entries with a\n * real timestamp).\n */\nfunction lastUsedTimestamp(tool: ExternalTool): number {\n\tif (!tool.lastUsedAt) return 0;\n\tconst ms = Date.parse(tool.lastUsedAt);\n\treturn Number.isFinite(ms) ? ms : 0;\n}\n\n/**\n * Sort by \"recently used\" — entries with newer `lastUsedAt` float to\n * the top, entries with no `lastUsedAt` sort to the bottom (preserving\n * their relative `order` index when both are absent).\n *\n * Ties (same `lastUsedAt` or both missing) are broken by `order`, then\n * by `id` for determinism.\n */\nexport function sortByRecentFirst(tools: readonly ExternalTool[]): ExternalTool[] {\n\treturn [...tools].sort((a, b) => {\n\t\tconst tsA = lastUsedTimestamp(a);\n\t\tconst tsB = lastUsedTimestamp(b);\n\t\tif (tsA !== tsB) return tsB - tsA;\n\t\tconst orderA = a.order ?? Number.MAX_SAFE_INTEGER;\n\t\tconst orderB = b.order ?? Number.MAX_SAFE_INTEGER;\n\t\tif (orderA !== orderB) return orderA - orderB;\n\t\treturn a.id.localeCompare(b.id);\n\t});\n}\n\n/**\n * Sort by user-defined `order` field. Entries without `order` are\n * appended in their input order (stable sort via `id` tiebreaker).\n */\nexport function sortByUserOrder(tools: readonly ExternalTool[]): ExternalTool[] {\n\treturn [...tools].sort((a, b) => {\n\t\tconst orderA = a.order ?? Number.MAX_SAFE_INTEGER;\n\t\tconst orderB = b.order ?? Number.MAX_SAFE_INTEGER;\n\t\tif (orderA !== orderB) return orderA - orderB;\n\t\treturn a.id.localeCompare(b.id);\n\t});\n}\n\n/**\n * Deduplicate by `id`. The first occurrence wins; later ones are\n * discarded. Used when merging built-in + user seeds so duplicates\n * from the on-disk file don't shadow the built-in defaults.\n */\nexport function dedupeById(tools: readonly ExternalTool[]): ExternalTool[] {\n\tconst seen = new Set<string>();\n\tconst out: ExternalTool[] = [];\n\tfor (const tool of tools) {\n\t\tif (seen.has(tool.id)) continue;\n\t\tseen.add(tool.id);\n\t\tout.push(tool);\n\t}\n\treturn out;\n}\n\n/**\n * Merge built-in defaults with user-stored tools. Defaults keep\n * `isDefault: true` and their `order`; user tools are appended with\n * their stored metadata. Output is sorted by user order.\n */\nexport function mergeWithDefaults(\n\tdefaults: readonly ExternalTool[],\n\tuserTools: readonly ExternalTool[]\n): ExternalTool[] {\n\tconst defaultIds = new Set(defaults.map((d) => d.id));\n\tconst uniqueUserTools = userTools.filter((t) => !defaultIds.has(t.id));\n\treturn sortByUserOrder([...defaults, ...uniqueUserTools]);\n}\n\n/**\n * Re-number `order` for a list of tool ids. Used by\n * `toolbox.update` when the user drags-and-drops entries in the UI.\n */\nexport function reindexOrder(\n\ttools: ExternalTool[],\n\tnewOrderIds: readonly string[]\n): ExternalTool[] {\n\tconst orderMap = new Map<string, number>();\n\tfor (const [index, id] of newOrderIds.entries()) {\n\t\torderMap.set(id, index);\n\t}\n\tfor (const tool of tools) {\n\t\tconst next = orderMap.get(tool.id);\n\t\tif (next !== undefined) tool.order = next;\n\t}\n\treturn tools;\n}\n\n/**\n * Stamp `lastUsedAt` on the targeted tool (clones the array). Returns\n * a new array; the input is left untouched.\n */\nexport function touchLastUsedAt(\n\ttools: readonly ExternalTool[],\n\tid: string,\n\twhen: Date = new Date()\n): ExternalTool[] {\n\treturn tools.map((tool) => (tool.id === id ? { ...tool, lastUsedAt: when.toISOString() } : tool));\n}\n","/**\n * ToolboxStore — JSON persistence for the toolbox entries.\n *\n * Two scopes:\n * - `user` → `~/.serviceme/toolbox.json`\n * - `workspace` → `<cwd>/.github/.serviceme-toolbox.json`\n *\n * Both files share the `PersistedToolbox` shape and the same atomic\n * write pattern (tmp + rename + fsync) as `IdentityStore`. Concurrent\n * writers are serialized via a mkdir-based file lock (Phase 6+ may\n * upgrade to `proper-lockfile`).\n *\n * Refs:\n * - 4.功能规划.md §2.3 — `ToolboxStore.ts JSON 持久化(user + workspace scope)`\n * - `3.功能拆分.md` §3 — toolbox wire shape\n */\n\nimport * as fsp from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { setTimeout as delay } from \"node:timers/promises\";\n\nimport type { ExternalTool, ToolboxScope } from \"@serviceme/devtools-protocol\";\n\nimport { getToolboxJsonPath } from \"../paths/userHome\";\n\nimport { mergeWithDefaults, sortByUserOrder } from \"./sort\";\nimport {\n\tBUILTIN_DEFAULT_TOOLS,\n\ttype DefaultToolSeed,\n\ttype PersistedToolbox,\n\ttype ResolvedToolbox,\n\tTOOLBOX_JSON_SCHEMA_VERSION,\n} from \"./types\";\n\nconst FILE_MODE = 0o600;\nconst LOCK_DIR_MODE = 0o700;\nconst DEFAULT_LOCK_TIMEOUT_MS = 5000;\nconst DEFAULT_LOCK_RETRY_MS = 25;\n// `mkdir` (lock acquisition) and writing the pid file are two separate\n// syscalls, so there's a brief window where the lock dir exists but the\n// pid file doesn't yet. A grace period keeps a concurrent acquirer from\n// mistaking that window for an abandoned lock (see `isStaleLock`).\nconst LOCK_STALE_GRACE_MS = 200;\nconst TMP_SUFFIX = \".tmp\";\nexport const WORKSPACE_TOOLBOX_RELATIVE_PATH = path.join(\".github\", \".serviceme-toolbox.json\");\nconst LEGACY_WORKSPACE_TOOLBOX_FILENAME = \".ms-devtools-toolbox.json\";\n\n/**\n * One-time migration: the workspace-scope toolbox file used to be named\n * `.ms-devtools-toolbox.json`. If the new `.serviceme-toolbox.json` doesn't\n * exist yet but the legacy file does (in the same directory), rename it\n * forward so existing toolbox entries aren't silently lost.\n */\nasync function migrateLegacyWorkspaceToolboxFile(filePath: string | null): Promise<void> {\n\tif (!filePath) return;\n\tconst legacyPath = path.join(path.dirname(filePath), LEGACY_WORKSPACE_TOOLBOX_FILENAME);\n\tif (legacyPath === filePath) return;\n\ttry {\n\t\tawait fsp.access(filePath);\n\t\treturn;\n\t} catch {\n\t\t// new file missing — check the legacy path below\n\t}\n\ttry {\n\t\tawait fsp.rename(legacyPath, filePath);\n\t} catch {\n\t\t// legacy file doesn't exist either — nothing to migrate\n\t}\n}\n\nexport interface ToolboxFileBackend {\n\tread(filePath: string): Promise<PersistedToolbox | null>;\n\twrite(filePath: string, payload: PersistedToolbox): Promise<void>;\n\texists(filePath: string): Promise<boolean>;\n\t/** Maximum number of `.corrupted.*.bak` snapshots to keep per toolbox file (default: 5). */\n\tmaxBackupCount?: number;\n}\n\nexport interface ToolboxStoreOptions {\n\t/** Override the user-scope file path (default: `getToolboxJsonPath()`). */\n\tuserFilePath?: string;\n\t/** Override the workspace-scope file path resolver (default: cwd-relative). */\n\tresolveWorkspacePath?: () => string | null;\n\t/** Injectable built-in tool seeds (default: `BUILTIN_DEFAULT_TOOLS`). */\n\tdefaultTools?: readonly DefaultToolSeed[];\n\thooks?: ToolboxStoreHooks;\n\tbackend?: ToolboxFileBackend;\n\tlockTimeoutMs?: number;\n\tlockRetryMs?: number;\n}\n\nexport interface ToolboxStoreHooks {\n\tbeforeWrite?: (scope: ToolboxScope, payload: PersistedToolbox) => void | Promise<void>;\n\tafterWrite?: (scope: ToolboxScope, payload: PersistedToolbox) => void | Promise<void>;\n}\n\nexport class FsToolboxFileBackend implements ToolboxFileBackend {\n\tmaxBackupCount: number = 5;\n\n\tasync exists(filePath: string): Promise<boolean> {\n\t\ttry {\n\t\t\tawait fsp.access(filePath);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tasync read(filePath: string): Promise<PersistedToolbox | null> {\n\t\tlet buf: string;\n\t\ttry {\n\t\t\tbuf = await fsp.readFile(filePath, \"utf8\");\n\t\t} catch (err) {\n\t\t\tif (isNodeError(err) && err.code === \"ENOENT\") return null;\n\t\t\tthrow err;\n\t\t}\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(buf) as unknown;\n\t\t\treturn coercePersistedToolbox(parsed);\n\t\t} catch {\n\t\t\t// File exists but is unparseable JSON or fails schema validation\n\t\t\t// (e.g. hand-edited, written by an older/incompatible schema, or\n\t\t\t// truncated by a crash). Quarantine it as a timestamped `.bak`\n\t\t\t// sibling and fall back to an empty toolbox (defaults still\n\t\t\t// merge in via `ToolboxStore.read()`) rather than crashing the\n\t\t\t// whole \"get external tools\" request.\n\t\t\tawait this.backupCorruptedFile(filePath);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate async backupCorruptedFile(filePath: string): Promise<void> {\n\t\ttry {\n\t\t\tconst backupPath = `${filePath}.corrupted.${Date.now()}.bak`;\n\t\t\tawait fsp.copyFile(filePath, backupPath);\n\t\t\tawait this.purgeExcessBackups(filePath);\n\t\t} catch {\n\t\t\t// Best-effort backup — never let backup failure mask the real recovery.\n\t\t}\n\t}\n\n\tprivate async purgeExcessBackups(filePath: string): Promise<void> {\n\t\tconst dir = path.dirname(filePath);\n\t\tconst base = path.basename(filePath);\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = await fsp.readdir(dir);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tconst backups = entries\n\t\t\t.filter((n) => n.startsWith(base) && n.endsWith(\".bak\"))\n\t\t\t.map((n) => ({ name: n, filePath: path.join(dir, n) }))\n\t\t\t.sort((a, b) => {\n\t\t\t\t// Sort oldest-first so we drop the earliest ones first.\n\t\t\t\treturn a.name.localeCompare(b.name);\n\t\t\t});\n\t\tconst excess = backups.length - this.maxBackupCount;\n\t\tif (excess <= 0) return;\n\t\tawait Promise.all(\n\t\t\tbackups.slice(0, excess).map((b) => fsp.rm(b.filePath).catch(() => undefined))\n\t\t);\n\t}\n\n\tasync write(filePath: string, payload: PersistedToolbox): Promise<void> {\n\t\tawait fsp.mkdir(path.dirname(filePath), { recursive: true });\n\t\tconst tmpPath = `${filePath}${TMP_SUFFIX}`;\n\t\tconst bytes = Buffer.from(JSON.stringify(payload, null, \"\\t\"), \"utf8\");\n\t\tawait fsp.rm(tmpPath, { force: true });\n\t\tconst handle = await fsp.open(tmpPath, \"w\", FILE_MODE);\n\t\ttry {\n\t\t\tawait handle.writeFile(bytes);\n\t\t\tawait handle.sync();\n\t\t} finally {\n\t\t\tawait handle.close();\n\t\t}\n\t\tawait fsp.rename(tmpPath, filePath);\n\t\tawait fsp.chmod(filePath, FILE_MODE).catch(() => undefined);\n\t}\n}\n\nfunction coercePersistedToolbox(parsed: unknown): PersistedToolbox {\n\tif (typeof parsed !== \"object\" || parsed === null) {\n\t\tthrow new Error(\"toolbox.json: top-level must be an object\");\n\t}\n\tconst obj = parsed as Record<string, unknown>;\n\tconst version = obj.version;\n\tif (version !== TOOLBOX_JSON_SCHEMA_VERSION) {\n\t\tthrow new Error(`toolbox.json: unsupported schema version ${String(version)}`);\n\t}\n\tif (!Array.isArray(obj.tools)) {\n\t\tthrow new Error(\"toolbox.json: 'tools' must be an array\");\n\t}\n\treturn { version, tools: obj.tools as ExternalTool[] };\n}\n\nfunction isNodeError(value: unknown): value is NodeJS.ErrnoException {\n\treturn value instanceof Error && typeof (value as { code?: unknown }).code === \"string\";\n}\n\nfunction isProcessAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nclass ToolboxFileLock {\n\tprivate readonly dirPath: string;\n\tprivate readonly pidFilePath: string;\n\tprivate readonly timeoutMs: number;\n\tprivate readonly retryMs: number;\n\tprivate acquired = false;\n\n\tconstructor(filePath: string, timeoutMs: number, retryMs: number) {\n\t\tthis.dirPath = `${filePath}.lock`;\n\t\tthis.pidFilePath = path.join(this.dirPath, \"pid\");\n\t\tthis.timeoutMs = timeoutMs;\n\t\tthis.retryMs = retryMs;\n\t}\n\n\tasync acquire(): Promise<void> {\n\t\tconst start = Date.now();\n\t\twhile (true) {\n\t\t\ttry {\n\t\t\t\tawait fsp.mkdir(this.dirPath, { mode: LOCK_DIR_MODE });\n\t\t\t\tawait fsp.writeFile(this.pidFilePath, String(process.pid), \"utf8\").catch(() => undefined);\n\t\t\t\tthis.acquired = true;\n\t\t\t\treturn;\n\t\t\t} catch (err) {\n\t\t\t\tif (!isNodeError(err) || err.code !== \"EEXIST\") throw err;\n\t\t\t\tconst stale = await this.isStaleLock();\n\t\t\t\tif (stale) {\n\t\t\t\t\tawait fsp.rm(this.dirPath, { recursive: true, force: true });\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (Date.now() - start >= this.timeoutMs) {\n\t\t\t\t\tthrow new Error(`ToolboxStore lock acquisition timed out for ${this.dirPath}`);\n\t\t\t\t}\n\t\t\t\tawait delay(this.retryMs);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async isStaleLock(): Promise<boolean> {\n\t\tlet pidStr: string;\n\t\ttry {\n\t\t\tpidStr = await fsp.readFile(this.pidFilePath, \"utf8\");\n\t\t} catch {\n\t\t\t// The pid file may not exist yet because another acquirer just\n\t\t\t// created the lock dir and hasn't finished writing its pid file\n\t\t\t// (mkdir + writeFile is not atomic). Give it a short grace window\n\t\t\t// before concluding the owner crashed between mkdir and writeFile.\n\t\t\ttry {\n\t\t\t\tconst stat = await fsp.stat(this.dirPath);\n\t\t\t\treturn Date.now() - stat.mtimeMs > LOCK_STALE_GRACE_MS;\n\t\t\t} catch {\n\t\t\t\t// Lock dir disappeared concurrently (e.g. released mid-check) —\n\t\t\t\t// not stale, just gone; the caller's next mkdir will succeed.\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\tconst pid = Number.parseInt(pidStr.trim(), 10);\n\t\tif (!Number.isFinite(pid) || pid <= 0) return true;\n\t\treturn !isProcessAlive(pid);\n\t}\n\n\tasync release(): Promise<void> {\n\t\tif (!this.acquired) return;\n\t\tthis.acquired = false;\n\t\tawait fsp.rm(this.dirPath, { recursive: true, force: true });\n\t}\n}\n\nfunction defaultWorkspacePath(): string | null {\n\t// The store caller (ToolboxCore) supplies a cwd, but tests want a\n\t// stable default. Return null when the env var says \"no workspace\".\n\tif (process.env.SERVICEME_NO_WORKSPACE_TOOLBOX === \"1\") return null;\n\treturn path.join(process.cwd(), WORKSPACE_TOOLBOX_RELATIVE_PATH);\n}\n\nexport class ToolboxStore {\n\tprivate readonly userFilePath: string;\n\tprivate readonly resolveWorkspacePath: () => string | null;\n\tprivate readonly defaults: readonly DefaultToolSeed[];\n\tprivate readonly hooks: ToolboxStoreHooks;\n\tprivate readonly backend: ToolboxFileBackend;\n\tprivate readonly lockTimeoutMs: number;\n\tprivate readonly lockRetryMs: number;\n\n\tconstructor(opts: ToolboxStoreOptions = {}) {\n\t\tthis.userFilePath = opts.userFilePath ?? getToolboxJsonPath();\n\t\tthis.resolveWorkspacePath = opts.resolveWorkspacePath ?? defaultWorkspacePath;\n\t\tthis.defaults = opts.defaultTools ?? BUILTIN_DEFAULT_TOOLS;\n\t\tthis.hooks = opts.hooks ?? {};\n\t\tthis.backend = opts.backend ?? new FsToolboxFileBackend();\n\t\tthis.lockTimeoutMs = opts.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;\n\t\tthis.lockRetryMs = opts.lockRetryMs ?? DEFAULT_LOCK_RETRY_MS;\n\t}\n\n\t/** Read a scope; returns the resolved toolbox (with defaults merged when empty). */\n\tasync read(scope: ToolboxScope): Promise<ResolvedToolbox> {\n\t\tconst filePath = this.filePathFor(scope);\n\t\tif (scope === \"workspace\") {\n\t\t\tawait migrateLegacyWorkspaceToolboxFile(filePath);\n\t\t}\n\t\tconst stored = filePath ? await this.backend.read(filePath) : null;\n\t\tconst storedTools = stored?.tools ?? [];\n\t\tconst defaults = this.defaults.map((seed, index) => toDefaultTool(seed, index));\n\t\tconst merged = mergeWithDefaults(defaults, storedTools);\n\t\tconst userOnly = storedTools.filter((t) => !this.defaults.some((d) => d.id === t.id));\n\t\tvoid userOnly;\n\t\treturn { scope, tools: merged };\n\t}\n\n\t/**\n\t * Atomic write under a file lock. Replaces the entire `tools` array\n\t * with the provided snapshot.\n\t */\n\tasync write(scope: ToolboxScope, tools: readonly ExternalTool[]): Promise<void> {\n\t\tconst filePath = this.filePathFor(scope);\n\t\tif (!filePath) {\n\t\t\tthrow new Error(`Cannot write toolbox scope '${scope}': file path is unavailable`);\n\t\t}\n\t\tif (scope === \"workspace\") {\n\t\t\tawait migrateLegacyWorkspaceToolboxFile(filePath);\n\t\t}\n\t\tconst payload: PersistedToolbox = {\n\t\t\tversion: TOOLBOX_JSON_SCHEMA_VERSION,\n\t\t\ttools: sortByUserOrder([...tools]),\n\t\t};\n\t\tawait this.hooks.beforeWrite?.(scope, payload);\n\t\tconst lock = new ToolboxFileLock(filePath, this.lockTimeoutMs, this.lockRetryMs);\n\t\tawait lock.acquire();\n\t\ttry {\n\t\t\tawait this.backend.write(filePath, payload);\n\t\t} finally {\n\t\t\tawait lock.release();\n\t\t}\n\t\tawait this.hooks.afterWrite?.(scope, payload);\n\t}\n\n\t/**\n\t * Read-modify-write under the file lock. The mutator receives the\n\t * current user-only list (defaults not included) and returns the\n\t * replacement list. Throwing inside the mutator aborts the write.\n\t */\n\tasync mutate(\n\t\tscope: ToolboxScope,\n\t\tmutator: (current: ExternalTool[]) => Promise<ExternalTool[]>\n\t): Promise<ExternalTool[]> {\n\t\tconst filePath = this.filePathFor(scope);\n\t\tif (!filePath) {\n\t\t\tthrow new Error(`Cannot mutate toolbox scope '${scope}': file path is unavailable`);\n\t\t}\n\t\tif (scope === \"workspace\") {\n\t\t\tawait migrateLegacyWorkspaceToolboxFile(filePath);\n\t\t}\n\t\tconst lock = new ToolboxFileLock(filePath, this.lockTimeoutMs, this.lockRetryMs);\n\t\tawait lock.acquire();\n\t\ttry {\n\t\t\tconst stored = await this.backend.read(filePath);\n\t\t\tconst storedTools = stored?.tools ?? [];\n\t\t\tconst defaultIds = new Set(this.defaults.map((d) => d.id));\n\t\t\tconst userTools = storedTools.filter((t) => !defaultIds.has(t.id));\n\t\t\tconst next = await mutator(userTools);\n\t\t\tconst payload: PersistedToolbox = {\n\t\t\t\tversion: TOOLBOX_JSON_SCHEMA_VERSION,\n\t\t\t\ttools: sortByUserOrder([...next]),\n\t\t\t};\n\t\t\tawait this.hooks.beforeWrite?.(scope, payload);\n\t\t\tawait this.backend.write(filePath, payload);\n\t\t\tawait this.hooks.afterWrite?.(scope, payload);\n\t\t\treturn next;\n\t\t} finally {\n\t\t\tawait lock.release();\n\t\t}\n\t}\n\n\t/** Wipe a scope entirely (used by `toolbox.remove --all` extensions). */\n\tasync clear(scope: ToolboxScope): Promise<void> {\n\t\tconst filePath = this.filePathFor(scope);\n\t\tif (!filePath) return;\n\t\tawait fsp.rm(filePath, { force: true });\n\t}\n\n\t/** Test seam — resolve the user-scope file path. */\n\tgetUserFilePath(): string {\n\t\treturn this.userFilePath;\n\t}\n\n\t/** Test seam — resolve the workspace-scope file path (or null when disabled). */\n\tgetWorkspaceFilePath(): string | null {\n\t\treturn this.resolveWorkspacePath();\n\t}\n\n\tprivate filePathFor(scope: ToolboxScope): string | null {\n\t\treturn scope === \"user\" ? this.userFilePath : this.resolveWorkspacePath();\n\t}\n}\n\nfunction toDefaultTool(seed: DefaultToolSeed, order: number): ExternalTool {\n\treturn {\n\t\tid: seed.id,\n\t\tname: seed.name,\n\t\tdescription: seed.description,\n\t\ticon: seed.icon,\n\t\turl: seed.url,\n\t\tisDefault: true,\n\t\torder,\n\t\tscope: \"user\",\n\t};\n}\n","import * as os from \"node:os\";\nimport * as path from \"node:path\";\n\n/**\n * SERVICEME user home directory helpers.\n *\n * All paths resolve under {@link getServicemeHome}, which is either the\n * `SERVICEME_HOME` environment variable (when set and non-empty) or\n * `$HOME/.serviceme` on POSIX / `%USERPROFILE%\\.serviceme` on Windows.\n *\n * Tests inject `homeDir` and `servicemeHomeEnv` overrides via\n * {@link setUserHomeOverrides} / {@link resetUserHomeOverrides} so they can\n * exercise the path logic without touching the real user environment.\n */\n\n/** Layout constants — kept in one place so other modules can reuse them. */\nexport const SERVICEME_DIR_NAME = \".serviceme\";\nexport const REPOS_SUBDIR = \"repos\";\nexport const CACHE_SUBDIR = \"cache\";\nexport const DRAFTS_SUBDIR = \"drafts\";\nexport const SKILL_DRAFTS_SUBDIR = \"skills\";\nexport const AGENT_DRAFTS_SUBDIR = \"agents\";\nexport const REPOS_CONFIG_FILENAME = \"repos.json\";\nexport const WORKSPACES_SUBDIR = \"workspaces\";\nexport const WORKSPACE_CONTENT_STATE_FILENAME = \"copilot-content-state.json\";\n\n/** rev.20 — r7 BYOM Server Proxy toggle self-state (read by ext + CLI + server). */\nexport const SERVER_PROXY_GLOBAL_FILENAME = \"server-proxy.json\";\n\n/**\n * Repo id regex — used to validate any `repoId` argument before it is joined\n * into a filesystem path. Keeps path traversal attempts out and gives us a\n * predictable on-disk shape.\n */\nexport const SAFE_REPO_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;\n\n/** Environment variable that overrides the user-home root directory. */\nexport const SERVICEME_HOME_ENV = \"SERVICEME_HOME\";\n\n/**\n * Test seam: lets unit tests inject deterministic values for `os.homedir()`\n * and the `SERVICEME_HOME` env override without actually mutating\n * `process.env` (which would leak into other tests).\n */\ninterface UserHomeOverrides {\n\thomeDir?: string | undefined;\n\tservicemeHomeEnv?: string | undefined;\n\tplatform?: NodeJS.Platform | undefined;\n}\n\nlet activeOverrides: UserHomeOverrides = {};\n\nexport function setUserHomeOverrides(overrides: UserHomeOverrides): void {\n\tactiveOverrides = { ...overrides };\n}\n\nexport function resetUserHomeOverrides(): void {\n\tactiveOverrides = {};\n}\n\nfunction resolveHomeDir(): string {\n\tconst injected = activeOverrides.homeDir;\n\tif (injected !== undefined) {\n\t\treturn injected;\n\t}\n\treturn os.homedir();\n}\n\nfunction resolveServicemeHomeEnv(): string | undefined {\n\tconst injected = activeOverrides.servicemeHomeEnv;\n\tif (injected !== undefined) {\n\t\t// Treat empty string as \"not set\" — `process.env` always returns a string\n\t\t// but tests may deliberately pass \"\" to opt out.\n\t\treturn injected.length > 0 ? injected : undefined;\n\t}\n\tconst envValue = process.env[SERVICEME_HOME_ENV];\n\treturn envValue && envValue.length > 0 ? envValue : undefined;\n}\n\nfunction resolvePlatform(): NodeJS.Platform {\n\treturn activeOverrides.platform ?? process.platform;\n}\n\nexport function assertSafeRepoId(repoId: string): string {\n\tif (typeof repoId !== \"string\" || repoId.length === 0 || !SAFE_REPO_ID_PATTERN.test(repoId)) {\n\t\tthrow new Error(\n\t\t\t`Invalid repo id: ${JSON.stringify(repoId)}. ` +\n\t\t\t\t`Must match ${SAFE_REPO_ID_PATTERN} (alphanumeric start, then ` +\n\t\t\t\t`alphanumerics / underscores / hyphens, ≤ 64 chars).`\n\t\t);\n\t}\n\treturn repoId;\n}\n\n/**\n * The raw OS home directory (`os.homedir()`), honoring test overrides\n * ({@link setUserHomeOverrides}). Exported for callers that need a\n * home-relative path *outside* of `~/.serviceme` — e.g. the\n * `~/.agents/{skills,agents}` convention used by `SkillLinker` for\n * user-scope links.\n */\nexport function getHomeDir(): string {\n\treturn resolveHomeDir();\n}\n\n/**\n * Root directory for all SERVICEME user-level state. Resolves to\n * `${SERVICEME_HOME}` when that env var is set, otherwise `${HOME}/.serviceme`\n * (POSIX) or `%USERPROFILE%\\.serviceme` (Windows via `os.homedir`).\n */\nexport function getServicemeHome(): string {\n\tconst override = resolveServicemeHomeEnv();\n\tif (override !== undefined) {\n\t\treturn path.resolve(override);\n\t}\n\treturn path.join(resolveHomeDir(), SERVICEME_DIR_NAME);\n}\n\n/** `$HOME/.serviceme/repos` (or `${SERVICEME_HOME}/repos`). */\nexport function getReposDir(): string {\n\treturn path.join(getServicemeHome(), REPOS_SUBDIR);\n}\n\n/** `$HOME/.serviceme/repos/<repoId>` — validates `repoId` first. */\nexport function getRepoDir(repoId: string): string {\n\treturn path.join(getReposDir(), assertSafeRepoId(repoId));\n}\n\n/** `$HOME/.serviceme/cache` — generic per-user cache. */\nexport function getCacheDir(): string {\n\treturn path.join(getServicemeHome(), CACHE_SUBDIR);\n}\n\n/** `$HOME/.serviceme/cache/<repoId>` — per-repo sync state lives here. */\nexport function getRepoCacheDir(repoId: string): string {\n\treturn path.join(getCacheDir(), assertSafeRepoId(repoId));\n}\n\n/** `$HOME/.serviceme/drafts` — local edits not yet pushed upstream. */\nexport function getDraftsDir(): string {\n\treturn path.join(getServicemeHome(), DRAFTS_SUBDIR);\n}\n\n/** `$HOME/.serviceme/drafts/skills` — local skill drafts. */\nexport function getSkillDraftsDir(): string {\n\treturn path.join(getDraftsDir(), SKILL_DRAFTS_SUBDIR);\n}\n\n/** `$HOME/.serviceme/drafts/agents` — local agent drafts. */\nexport function getAgentDraftsDir(): string {\n\treturn path.join(getDraftsDir(), AGENT_DRAFTS_SUBDIR);\n}\n\n/**\n * `$HOME/.serviceme/repos.json` — the single config entry point for repo\n * metadata. Always under the resolved home root (i.e. follows\n * `SERVICEME_HOME` overrides too).\n */\nexport function getReposConfigPath(): string {\n\treturn path.join(getServicemeHome(), REPOS_CONFIG_FILENAME);\n}\n\n/** `~/.serviceme/workspaces` — machine-local per-workspace state root. */\nexport function getWorkspacesDir(): string {\n\treturn path.join(getServicemeHome(), WORKSPACES_SUBDIR);\n}\n\n/**\n * Convenience helper for callers that need to switch behaviour on platform\n * (e.g. `SkillLinker` chooses symlink vs junction). Exported mostly so tests\n * can pin the value without touching `process.platform` directly.\n */\nexport function getHomePlatform(): NodeJS.Platform {\n\treturn resolvePlatform();\n}\n\n// ─── Scheduled Tasks paths (added by M1.5.3) ───────────────────────────────\n\n/** Layout constants for scheduled-tasks files. Kept here so other modules\n * can reuse them and the names stay in sync with the design doc. */\nexport const SCHEDULED_TASKS_CONFIG_FILENAME = \"scheduled-tasks.json\";\nexport const SCHEDULED_TASKS_LOG_FILENAME = \"scheduled-tasks-log.json\";\nexport const SCHEDULER_PID_FILENAME = \"scheduler.pid\";\nexport const SCHEDULER_LOCK_FILENAME = \"scheduler.lock\";\nexport const SCHEDULER_LOG_FILENAME = \"scheduler.log\";\nexport const MIGRATION_FAILURES_FILENAME = \"migration-failures.json\";\nexport const KNOWN_WORKSPACES_FILENAME = \"known-workspaces.json\";\n\n/** `~/.serviceme/scheduled-tasks.json` — the global v2 task list. */\nexport function getScheduledTasksConfigPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULED_TASKS_CONFIG_FILENAME);\n}\n\n/** `~/.serviceme/scheduled-tasks-log.json` — the global execution log (200 LRU). */\nexport function getScheduledTasksLogPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULED_TASKS_LOG_FILENAME);\n}\n\n/** `~/.serviceme/scheduler.pid` — global daemon PID file. */\nexport function getSchedulerPidPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULER_PID_FILENAME);\n}\n\n/** `~/.serviceme/scheduler.lock` — global daemon startup flock. */\nexport function getSchedulerLockPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULER_LOCK_FILENAME);\n}\n\n/** `~/.serviceme/scheduler.log` — global daemon log (daemon lifecycle, not task execution). */\nexport function getSchedulerLogPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULER_LOG_FILENAME);\n}\n\n/** `~/.serviceme/migration-failures.json` — diagnostics for failed v1→v2 imports. */\nexport function getMigrationFailuresPath(): string {\n\treturn path.join(getServicemeHome(), MIGRATION_FAILURES_FILENAME);\n}\n\n/** `~/.serviceme/known-workspaces.json` — workspace list the extension has seen. */\nexport function getKnownWorkspacesPath(): string {\n\treturn path.join(getServicemeHome(), KNOWN_WORKSPACES_FILENAME);\n}\n\n// ─── r7 BYOM Server Proxy (rev.20) ─────────────────────────────────────────\n\n/**\n * `~/.serviceme/server-proxy.json` — r7 BYOM Server Proxy toggle\n * self-state. Written by the extension's `ProxyConfigService`, readable\n * by the CLI / Server so they can decide whether to route traffic\n * through the corporate proxy without booting VS Code.\n *\n * Lives under user-level `~/.serviceme/` (not under a workspace) because\n * the toggle is a *user preference* — the same human enabling it on\n * machine A should be able to rely on it on machine B after sync, not\n * have it disappear when they switch repos.\n */\nexport function getServerProxyGlobalPath(): string {\n\treturn path.join(getServicemeHome(), SERVER_PROXY_GLOBAL_FILENAME);\n}\n\n// ─── Phase 5 (auth + device + toolbox) placeholders (Spec §11.12) ────────\n\n/** Layout constants for the Phase 5 client-side state files. The server\n * already has the corresponding routes (`/api/v1/auth/...`,\n * `/api/v1/device/...`, etc.) — the client just needs the on-disk\n * file paths + a first-run bootstrap so the auth/device/toolbox\n * services can open + read them without a per-call existence check. */\nexport const CREDENTIALS_CONFIG_FILENAME = \"credentials.json\";\nexport const DEVICE_JSON_FILENAME = \"device.json\";\nexport const TOOLBOX_JSON_FILENAME = \"toolbox.json\";\nexport const MACHINE_ID_FILENAME = \"machine-id\";\nexport const PROFILES_JSON_FILENAME = \"profiles.json\";\n\n/** `~/.serviceme/credentials.json` — better-auth session tokens + refresh state (client-side cache). */\nexport function getCredentialsConfigPath(): string {\n\treturn path.join(getServicemeHome(), CREDENTIALS_CONFIG_FILENAME);\n}\n\n/** `~/.serviceme/device.json` — device enrollment payload (id, public key fingerprint, claimed-by). */\nexport function getDeviceJsonPath(): string {\n\treturn path.join(getServicemeHome(), DEVICE_JSON_FILENAME);\n}\n\n/** `~/.serviceme/toolbox.json` — local toolbox state (installed tool refs, per-user prefs). */\nexport function getToolboxJsonPath(): string {\n\treturn path.join(getServicemeHome(), TOOLBOX_JSON_FILENAME);\n}\n\n/** `~/.serviceme/machine-id` — opaque stable per-install id (uuid v4 string, no JSON wrapper). */\nexport function getMachineIdPath(): string {\n\treturn path.join(getServicemeHome(), MACHINE_ID_FILENAME);\n}\n\n/** `~/.serviceme/profiles.json` — cached projection of server-side profile rows; refresh on demand. */\nexport function getProfilesJsonPath(): string {\n\treturn path.join(getServicemeHome(), PROFILES_JSON_FILENAME);\n}\n","/**\n * toolbox types — internal-only data shapes.\n *\n * The public data model lives in `@serviceme/devtools-protocol/toolbox`\n * (`ExternalTool`, `ToolboxScope`, `ToolboxList`). The types below are\n * the persisted on-disk shape and the patch helpers, scoped to the\n * toolbox domain only.\n *\n * Refs:\n * - 4.功能规划.md §2.3 — `toolbox/types.ts`\n */\n\nimport type { ExternalTool, ToolboxScope } from \"@serviceme/devtools-protocol\";\n\n/** Schema version of the on-disk toolbox JSON files. Bumped on breaking changes. */\nexport const TOOLBOX_JSON_SCHEMA_VERSION = 1;\n\nexport interface PersistedToolbox {\n\tversion: number;\n\ttools: ExternalTool[];\n}\n\n/** Patch payload accepted by `ToolboxCore.update` (mirrors `ExternalToolPatch`). */\nexport type ToolboxPatch = Partial<Omit<ExternalTool, \"id\" | \"isDefault\">>;\n\n/**\n * Built-in (default) tool seeds. Mirrors the Extension's\n * `apps/extension/src/config/external-tools.ts`. The Core stores these\n * verbatim and never mutates them — the Extension's `ToolBoxService`\n * runs the same merge on top of the Core's view.\n */\nexport interface DefaultToolSeed {\n\tid: string;\n\tname: string;\n\tdescription: string;\n\ticon: string;\n\turl: string;\n}\n\n/** Built-in tools rendered when the JSON file is missing or empty. */\nexport const BUILTIN_DEFAULT_TOOLS: readonly DefaultToolSeed[] = [\n\t{\n\t\tid: \"builtin-docs\",\n\t\tname: \"SERVICEME Docs\",\n\t\tdescription: \"Official documentation portal\",\n\t\ticon: \"book\",\n\t\turl: \"https://docs.medalsoft.com/serviceme\",\n\t},\n\t{\n\t\tid: \"builtin-issues\",\n\t\tname: \"Issue Tracker\",\n\t\tdescription: \"Report bugs and feature requests\",\n\t\ticon: \"bug\",\n\t\turl: \"https://github.com/medalsoftchina/ms-devtools-vscode/issues\",\n\t},\n\t{\n\t\tid: \"builtin-changelog\",\n\t\tname: \"Changelog\",\n\t\tdescription: \"Release notes for every published version\",\n\t\ticon: \"history\",\n\t\turl: \"https://github.com/medalsoftchina/ms-devtools-vscode/releases\",\n\t},\n];\n\n/** Resolved toolbox shape returned by `ToolboxStore.read()`. */\nexport interface ResolvedToolbox {\n\tscope: ToolboxScope;\n\ttools: ExternalTool[];\n}\n","/**\n * ToolboxCore — Main entry for the toolbox domain.\n *\n * Orchestrates the user + workspace scopes via `ToolboxStore`, applies\n * the \"最近使用置顶\" sort, and exposes a small surface that mirrors\n * the Phase 5.1 bridge methods:\n *\n * - `list(scope?)` → `toolbox.list`\n * - `add(tool, scope)` → `toolbox.add`\n * - `remove(id, scope?)` → `toolbox.remove`\n * - `update(id, patch, scope?)` → `toolbox.update`\n *\n * Default tools (built-in seeds) are never user-deletable. `remove()`\n * is a no-op for default tools and surfaces a structured error so\n * callers can map it to `TOOLBOX_DEFAULT_IMMUTABLE` (Phase 5.3 error\n * code).\n *\n * Refs:\n * - 4.功能规划.md §2.3 — `ToolboxCore.ts 主入口`\n * - `3.功能拆分.md` §3 — wire shape\n */\n\nimport type { ExternalTool, ToolboxList, ToolboxScope } from \"@serviceme/devtools-protocol\";\n\nimport { sortByRecentFirst, sortByUserOrder, touchLastUsedAt } from \"./sort\";\nimport { ToolboxStore } from \"./ToolboxStore\";\nimport { BUILTIN_DEFAULT_TOOLS, type DefaultToolSeed, type ToolboxPatch } from \"./types\";\n\n/** Sentinel — caller tried to remove a built-in (immutable) tool. */\nexport class DefaultToolImmutableError extends Error {\n\tconstructor(public readonly toolId: string) {\n\t\tsuper(`Cannot remove default toolbox entry: ${toolId}`);\n\t\tthis.name = \"DefaultToolImmutableError\";\n\t}\n}\n\nexport interface ToolboxCoreOptions {\n\tstore?: ToolboxStore;\n\tdefaultTools?: readonly DefaultToolSeed[];\n\t/** Sort applied to the merged list returned by `list()`. Default: `sortByUserOrder`. */\n\tlistSort?: (tools: readonly ExternalTool[]) => ExternalTool[];\n}\n\nexport class ToolboxCore {\n\tprivate readonly store: ToolboxStore;\n\tprivate readonly defaults: readonly DefaultToolSeed[];\n\tprivate readonly listSort: (tools: readonly ExternalTool[]) => ExternalTool[];\n\n\tconstructor(opts: ToolboxCoreOptions = {}) {\n\t\tthis.store = opts.store ?? new ToolboxStore();\n\t\tthis.defaults = opts.defaultTools ?? BUILTIN_DEFAULT_TOOLS;\n\t\tthis.listSort = opts.listSort ?? sortByUserOrder;\n\t}\n\n\t/** List all tools in a scope. Built-in defaults are merged in. */\n\tasync list(scope: ToolboxScope = \"user\"): Promise<ToolboxList> {\n\t\tconst resolved = await this.store.read(scope);\n\t\treturn {\n\t\t\tscope,\n\t\t\ttools: this.listSort(resolved.tools),\n\t\t};\n\t}\n\n\t/**\n\t * Append a new tool to the requested scope. Default tools are\n\t * rejected (they are seeds, not user entries).\n\t */\n\tasync add(tool: ExternalTool, scope: ToolboxScope = \"user\"): Promise<ToolboxList> {\n\t\tif (this.isDefaultId(tool.id)) {\n\t\t\tthrow new DefaultToolImmutableError(tool.id);\n\t\t}\n\t\tconst next = await this.store.mutate(scope, async (current) => {\n\t\t\tconst filtered = current.filter((t) => t.id !== tool.id);\n\t\t\treturn [\n\t\t\t\t...filtered,\n\t\t\t\t{\n\t\t\t\t\t...tool,\n\t\t\t\t\tscope,\n\t\t\t\t\tisDefault: false,\n\t\t\t\t\torder: tool.order ?? filtered.length,\n\t\t\t\t},\n\t\t\t];\n\t\t});\n\t\treturn { scope, tools: next };\n\t}\n\n\t/**\n\t * Remove a tool by id. Returns `success: false` when the id is a\n\t * built-in default (idempotent, never throws on missing entries).\n\t */\n\tasync remove(\n\t\tid: string,\n\t\tscope: ToolboxScope = \"user\"\n\t): Promise<{ scope: ToolboxScope; toolId: string; success: boolean }> {\n\t\tif (this.isDefaultId(id)) {\n\t\t\tthrow new DefaultToolImmutableError(id);\n\t\t}\n\t\tconst removed = await this.store.mutate(scope, async (current) =>\n\t\t\tcurrent.filter((t) => t.id !== id)\n\t\t);\n\t\treturn { scope, toolId: id, success: !removed.some((t) => t.id === id) };\n\t}\n\n\t/**\n\t * Patch a tool by id. Default tools can only have their `order`\n\t * updated; other patches are silently ignored for default entries\n\t * (callers can compare before/after to detect the ignore).\n\t */\n\tasync update(\n\t\tid: string,\n\t\tpatch: ToolboxPatch,\n\t\tscope: ToolboxScope = \"user\"\n\t): Promise<ToolboxList> {\n\t\tconst isDefault = this.isDefaultId(id);\n\t\tconst next = await this.store.mutate(scope, async (current) => {\n\t\t\tif (isDefault) {\n\t\t\t\t// Default tools are not stored on disk — silently ignore\n\t\t\t\t// non-order patches. The order field is also not persisted\n\t\t\t\t// (default order is set in memory at merge time), so the\n\t\t\t\t// caller can compare before/after to detect the ignore.\n\t\t\t\treturn current;\n\t\t\t}\n\t\t\treturn current.map((tool) =>\n\t\t\t\ttool.id === id ? { ...tool, ...patch, scope, isDefault: false } : tool\n\t\t\t);\n\t\t});\n\t\treturn { scope, tools: next };\n\t}\n\n\t/**\n\t * Stamp `lastUsedAt` on the targeted tool. This is the \"recently\n\t * used\" hook the Extension's webview uses when a user clicks a\n\t * toolbox entry.\n\t */\n\tasync recordUsage(\n\t\tid: string,\n\t\tscope: ToolboxScope = \"user\",\n\t\twhen: Date = new Date()\n\t): Promise<ExternalTool | null> {\n\t\tif (this.isDefaultId(id)) {\n\t\t\t// Defaults live in memory only — return a synthetic record so\n\t\t\t// callers can render the click without persisting anything.\n\t\t\tconst seed = this.defaults.find((d) => d.id === id);\n\t\t\tif (!seed) return null;\n\t\t\treturn { ...seed, scope, isDefault: true, order: 0, lastUsedAt: when.toISOString() };\n\t\t}\n\t\tlet updated: ExternalTool | null = null;\n\t\tawait this.store.mutate(scope, async (current) => {\n\t\t\tconst next = touchLastUsedAt(current, id, when);\n\t\t\tupdated = next.find((t) => t.id === id) ?? null;\n\t\t\treturn next;\n\t\t});\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Combined view: user + workspace scopes merged, sorted by recent\n\t * usage. Workspace tools overlay user tools (workspace entries win\n\t * on `id` collision).\n\t */\n\tasync listMerged(): Promise<ToolboxList> {\n\t\tconst [user, workspace] = await Promise.all([\n\t\t\tthis.store.read(\"user\"),\n\t\t\tthis.store.read(\"workspace\"),\n\t\t]);\n\t\tconst seen = new Set<string>();\n\t\tconst merged: ExternalTool[] = [];\n\t\tfor (const tool of workspace.tools) {\n\t\t\tseen.add(tool.id);\n\t\t\tmerged.push({ ...tool, scope: \"workspace\" });\n\t\t}\n\t\tfor (const tool of user.tools) {\n\t\t\tif (seen.has(tool.id)) continue;\n\t\t\tmerged.push({ ...tool, scope: \"user\" });\n\t\t}\n\t\treturn { scope: \"user\", tools: sortByRecentFirst(merged) };\n\t}\n\n\t/** Expose the underlying store (CLI / Bridge use it for path-level access). */\n\tgetStore(): ToolboxStore {\n\t\treturn this.store;\n\t}\n\n\tprivate isDefaultId(id: string): boolean {\n\t\treturn this.defaults.some((d) => d.id === id);\n\t}\n}\n"],"mappings":"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","names":["path","delay","stat"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/toolbox/sort.ts","../src/toolbox/ToolboxStore.ts","../src/paths/userHome.ts","../src/toolbox/types.ts","../src/toolbox/ToolboxCore.ts"],"sourcesContent":["/**\n * toolbox sort + dedup — pure helpers, no I/O.\n *\n * The Extension's `ToolBoxService` keeps a stable `order` index and\n * surfaces \"recently used\" via the `lastUsedAt` ISO timestamp. These\n * helpers codify that algorithm so the CLI and Extension agree on the\n * \"最近使用置顶\" UX.\n *\n * Refs:\n * - 4.功能规划.md §2.3 — `sort.ts 排序与去重纯函数`\n */\n\nimport type { ExternalTool } from \"@serviceme/devtools-protocol\";\n\n/**\n * Parsed timestamp from `lastUsedAt`. Returns 0 when the field is\n * missing or unparseable (so the entry falls below entries with a\n * real timestamp).\n */\nfunction lastUsedTimestamp(tool: ExternalTool): number {\n\tif (!tool.lastUsedAt) return 0;\n\tconst ms = Date.parse(tool.lastUsedAt);\n\treturn Number.isFinite(ms) ? ms : 0;\n}\n\n/**\n * Sort by \"recently used\" — entries with newer `lastUsedAt` float to\n * the top, entries with no `lastUsedAt` sort to the bottom (preserving\n * their relative `order` index when both are absent).\n *\n * Ties (same `lastUsedAt` or both missing) are broken by `order`, then\n * by `id` for determinism.\n */\nexport function sortByRecentFirst(tools: readonly ExternalTool[]): ExternalTool[] {\n\treturn [...tools].sort((a, b) => {\n\t\tconst tsA = lastUsedTimestamp(a);\n\t\tconst tsB = lastUsedTimestamp(b);\n\t\tif (tsA !== tsB) return tsB - tsA;\n\t\tconst orderA = a.order ?? Number.MAX_SAFE_INTEGER;\n\t\tconst orderB = b.order ?? Number.MAX_SAFE_INTEGER;\n\t\tif (orderA !== orderB) return orderA - orderB;\n\t\treturn a.id.localeCompare(b.id);\n\t});\n}\n\n/**\n * Sort by user-defined `order` field. Entries without `order` are\n * appended in their input order (stable sort via `id` tiebreaker).\n */\nexport function sortByUserOrder(tools: readonly ExternalTool[]): ExternalTool[] {\n\treturn [...tools].sort((a, b) => {\n\t\tconst orderA = a.order ?? Number.MAX_SAFE_INTEGER;\n\t\tconst orderB = b.order ?? Number.MAX_SAFE_INTEGER;\n\t\tif (orderA !== orderB) return orderA - orderB;\n\t\treturn a.id.localeCompare(b.id);\n\t});\n}\n\n/**\n * Deduplicate by `id`. The first occurrence wins; later ones are\n * discarded. Used when merging built-in + user seeds so duplicates\n * from the on-disk file don't shadow the built-in defaults.\n */\nexport function dedupeById(tools: readonly ExternalTool[]): ExternalTool[] {\n\tconst seen = new Set<string>();\n\tconst out: ExternalTool[] = [];\n\tfor (const tool of tools) {\n\t\tif (seen.has(tool.id)) continue;\n\t\tseen.add(tool.id);\n\t\tout.push(tool);\n\t}\n\treturn out;\n}\n\n/**\n * Merge built-in defaults with user-stored tools. Defaults keep\n * `isDefault: true` and their `order`; user tools are appended with\n * their stored metadata. Output is sorted by user order.\n */\nexport function mergeWithDefaults(\n\tdefaults: readonly ExternalTool[],\n\tuserTools: readonly ExternalTool[]\n): ExternalTool[] {\n\tconst defaultIds = new Set(defaults.map((d) => d.id));\n\tconst uniqueUserTools = userTools.filter((t) => !defaultIds.has(t.id));\n\treturn sortByUserOrder([...defaults, ...uniqueUserTools]);\n}\n\n/**\n * Re-number `order` for a list of tool ids. Used by\n * `toolbox.update` when the user drags-and-drops entries in the UI.\n */\nexport function reindexOrder(\n\ttools: ExternalTool[],\n\tnewOrderIds: readonly string[]\n): ExternalTool[] {\n\tconst orderMap = new Map<string, number>();\n\tfor (const [index, id] of newOrderIds.entries()) {\n\t\torderMap.set(id, index);\n\t}\n\tfor (const tool of tools) {\n\t\tconst next = orderMap.get(tool.id);\n\t\tif (next !== undefined) tool.order = next;\n\t}\n\treturn tools;\n}\n\n/**\n * Stamp `lastUsedAt` on the targeted tool (clones the array). Returns\n * a new array; the input is left untouched.\n */\nexport function touchLastUsedAt(\n\ttools: readonly ExternalTool[],\n\tid: string,\n\twhen: Date = new Date()\n): ExternalTool[] {\n\treturn tools.map((tool) => (tool.id === id ? { ...tool, lastUsedAt: when.toISOString() } : tool));\n}\n","/**\n * ToolboxStore — JSON persistence for the toolbox entries.\n *\n * Two scopes:\n * - `user` → `~/.serviceme/toolbox.json`\n * - `workspace` → `<cwd>/.github/.serviceme-toolbox.json`\n *\n * Both files share the `PersistedToolbox` shape and the same atomic\n * write pattern (tmp + rename + fsync) as `IdentityStore`. Concurrent\n * writers are serialized via a mkdir-based file lock (Phase 6+ may\n * upgrade to `proper-lockfile`).\n *\n * Refs:\n * - 4.功能规划.md §2.3 — `ToolboxStore.ts JSON 持久化(user + workspace scope)`\n * - `3.功能拆分.md` §3 — toolbox wire shape\n */\n\nimport * as fsp from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { setTimeout as delay } from \"node:timers/promises\";\n\nimport type { ExternalTool, ToolboxScope } from \"@serviceme/devtools-protocol\";\n\nimport { getToolboxJsonPath } from \"../paths/userHome\";\n\nimport { mergeWithDefaults, sortByUserOrder } from \"./sort\";\nimport {\n\tBUILTIN_DEFAULT_TOOLS,\n\ttype DefaultToolSeed,\n\ttype PersistedToolbox,\n\ttype ResolvedToolbox,\n\tTOOLBOX_JSON_SCHEMA_VERSION,\n} from \"./types\";\n\nconst FILE_MODE = 0o600;\nconst LOCK_DIR_MODE = 0o700;\nconst DEFAULT_LOCK_TIMEOUT_MS = 5000;\nconst DEFAULT_LOCK_RETRY_MS = 25;\n// `mkdir` (lock acquisition) and writing the pid file are two separate\n// syscalls, so there's a brief window where the lock dir exists but the\n// pid file doesn't yet. A grace period keeps a concurrent acquirer from\n// mistaking that window for an abandoned lock (see `isStaleLock`).\nconst LOCK_STALE_GRACE_MS = 200;\nconst TMP_SUFFIX = \".tmp\";\nexport const WORKSPACE_TOOLBOX_RELATIVE_PATH = path.join(\".github\", \".serviceme-toolbox.json\");\nconst LEGACY_WORKSPACE_TOOLBOX_FILENAME = \".ms-devtools-toolbox.json\";\n\n/**\n * One-time migration: the workspace-scope toolbox file used to be named\n * `.ms-devtools-toolbox.json`. If the new `.serviceme-toolbox.json` doesn't\n * exist yet but the legacy file does (in the same directory), rename it\n * forward so existing toolbox entries aren't silently lost.\n */\nasync function migrateLegacyWorkspaceToolboxFile(filePath: string | null): Promise<void> {\n\tif (!filePath) return;\n\tconst legacyPath = path.join(path.dirname(filePath), LEGACY_WORKSPACE_TOOLBOX_FILENAME);\n\tif (legacyPath === filePath) return;\n\ttry {\n\t\tawait fsp.access(filePath);\n\t\treturn;\n\t} catch {\n\t\t// new file missing — check the legacy path below\n\t}\n\ttry {\n\t\tawait fsp.rename(legacyPath, filePath);\n\t} catch {\n\t\t// legacy file doesn't exist either — nothing to migrate\n\t}\n}\n\nexport interface ToolboxFileBackend {\n\tread(filePath: string): Promise<PersistedToolbox | null>;\n\twrite(filePath: string, payload: PersistedToolbox): Promise<void>;\n\texists(filePath: string): Promise<boolean>;\n\t/** Maximum number of `.corrupted.*.bak` snapshots to keep per toolbox file (default: 5). */\n\tmaxBackupCount?: number;\n}\n\nexport interface ToolboxStoreOptions {\n\t/** Override the user-scope file path (default: `getToolboxJsonPath()`). */\n\tuserFilePath?: string;\n\t/** Override the workspace-scope file path resolver (default: cwd-relative). */\n\tresolveWorkspacePath?: () => string | null;\n\t/** Injectable built-in tool seeds (default: `BUILTIN_DEFAULT_TOOLS`). */\n\tdefaultTools?: readonly DefaultToolSeed[];\n\thooks?: ToolboxStoreHooks;\n\tbackend?: ToolboxFileBackend;\n\tlockTimeoutMs?: number;\n\tlockRetryMs?: number;\n}\n\nexport interface ToolboxStoreHooks {\n\tbeforeWrite?: (scope: ToolboxScope, payload: PersistedToolbox) => void | Promise<void>;\n\tafterWrite?: (scope: ToolboxScope, payload: PersistedToolbox) => void | Promise<void>;\n}\n\nexport class FsToolboxFileBackend implements ToolboxFileBackend {\n\tmaxBackupCount: number = 5;\n\n\tasync exists(filePath: string): Promise<boolean> {\n\t\ttry {\n\t\t\tawait fsp.access(filePath);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tasync read(filePath: string): Promise<PersistedToolbox | null> {\n\t\tlet buf: string;\n\t\ttry {\n\t\t\tbuf = await fsp.readFile(filePath, \"utf8\");\n\t\t} catch (err) {\n\t\t\tif (isNodeError(err) && err.code === \"ENOENT\") return null;\n\t\t\tthrow err;\n\t\t}\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(buf) as unknown;\n\t\t\treturn coercePersistedToolbox(parsed);\n\t\t} catch {\n\t\t\t// File exists but is unparseable JSON or fails schema validation\n\t\t\t// (e.g. hand-edited, written by an older/incompatible schema, or\n\t\t\t// truncated by a crash). Quarantine it as a timestamped `.bak`\n\t\t\t// sibling and fall back to an empty toolbox (defaults still\n\t\t\t// merge in via `ToolboxStore.read()`) rather than crashing the\n\t\t\t// whole \"get external tools\" request.\n\t\t\tawait this.backupCorruptedFile(filePath);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate async backupCorruptedFile(filePath: string): Promise<void> {\n\t\ttry {\n\t\t\tconst backupPath = `${filePath}.corrupted.${Date.now()}.bak`;\n\t\t\tawait fsp.copyFile(filePath, backupPath);\n\t\t\tawait this.purgeExcessBackups(filePath);\n\t\t} catch {\n\t\t\t// Best-effort backup — never let backup failure mask the real recovery.\n\t\t}\n\t}\n\n\tprivate async purgeExcessBackups(filePath: string): Promise<void> {\n\t\tconst dir = path.dirname(filePath);\n\t\tconst base = path.basename(filePath);\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = await fsp.readdir(dir);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tconst backups = entries\n\t\t\t.filter((n) => n.startsWith(base) && n.endsWith(\".bak\"))\n\t\t\t.map((n) => ({ name: n, filePath: path.join(dir, n) }))\n\t\t\t.sort((a, b) => {\n\t\t\t\t// Sort oldest-first so we drop the earliest ones first.\n\t\t\t\treturn a.name.localeCompare(b.name);\n\t\t\t});\n\t\tconst excess = backups.length - this.maxBackupCount;\n\t\tif (excess <= 0) return;\n\t\tawait Promise.all(\n\t\t\tbackups.slice(0, excess).map((b) => fsp.rm(b.filePath).catch(() => undefined))\n\t\t);\n\t}\n\n\tasync write(filePath: string, payload: PersistedToolbox): Promise<void> {\n\t\tawait fsp.mkdir(path.dirname(filePath), { recursive: true });\n\t\tconst tmpPath = `${filePath}${TMP_SUFFIX}`;\n\t\tconst bytes = Buffer.from(JSON.stringify(payload, null, \"\\t\"), \"utf8\");\n\t\tawait fsp.rm(tmpPath, { force: true });\n\t\tconst handle = await fsp.open(tmpPath, \"w\", FILE_MODE);\n\t\ttry {\n\t\t\tawait handle.writeFile(bytes);\n\t\t\tawait handle.sync();\n\t\t} finally {\n\t\t\tawait handle.close();\n\t\t}\n\t\tawait fsp.rename(tmpPath, filePath);\n\t\tawait fsp.chmod(filePath, FILE_MODE).catch(() => undefined);\n\t}\n}\n\nfunction coercePersistedToolbox(parsed: unknown): PersistedToolbox {\n\tif (typeof parsed !== \"object\" || parsed === null) {\n\t\tthrow new Error(\"toolbox.json: top-level must be an object\");\n\t}\n\tconst obj = parsed as Record<string, unknown>;\n\tconst version = obj.version;\n\tif (version !== TOOLBOX_JSON_SCHEMA_VERSION) {\n\t\tthrow new Error(`toolbox.json: unsupported schema version ${String(version)}`);\n\t}\n\tif (!Array.isArray(obj.tools)) {\n\t\tthrow new Error(\"toolbox.json: 'tools' must be an array\");\n\t}\n\treturn { version, tools: obj.tools as ExternalTool[] };\n}\n\nfunction isNodeError(value: unknown): value is NodeJS.ErrnoException {\n\treturn value instanceof Error && typeof (value as { code?: unknown }).code === \"string\";\n}\n\nfunction isProcessAlive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nclass ToolboxFileLock {\n\tprivate readonly dirPath: string;\n\tprivate readonly pidFilePath: string;\n\tprivate readonly timeoutMs: number;\n\tprivate readonly retryMs: number;\n\tprivate acquired = false;\n\n\tconstructor(filePath: string, timeoutMs: number, retryMs: number) {\n\t\tthis.dirPath = `${filePath}.lock`;\n\t\tthis.pidFilePath = path.join(this.dirPath, \"pid\");\n\t\tthis.timeoutMs = timeoutMs;\n\t\tthis.retryMs = retryMs;\n\t}\n\n\tasync acquire(): Promise<void> {\n\t\tconst start = Date.now();\n\t\twhile (true) {\n\t\t\ttry {\n\t\t\t\tawait fsp.mkdir(this.dirPath, { mode: LOCK_DIR_MODE });\n\t\t\t\tawait fsp.writeFile(this.pidFilePath, String(process.pid), \"utf8\").catch(() => undefined);\n\t\t\t\tthis.acquired = true;\n\t\t\t\treturn;\n\t\t\t} catch (err) {\n\t\t\t\tif (!isNodeError(err) || err.code !== \"EEXIST\") throw err;\n\t\t\t\tconst stale = await this.isStaleLock();\n\t\t\t\tif (stale) {\n\t\t\t\t\tawait fsp.rm(this.dirPath, { recursive: true, force: true });\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (Date.now() - start >= this.timeoutMs) {\n\t\t\t\t\tthrow new Error(`ToolboxStore lock acquisition timed out for ${this.dirPath}`);\n\t\t\t\t}\n\t\t\t\tawait delay(this.retryMs);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async isStaleLock(): Promise<boolean> {\n\t\tlet pidStr: string;\n\t\ttry {\n\t\t\tpidStr = await fsp.readFile(this.pidFilePath, \"utf8\");\n\t\t} catch {\n\t\t\t// The pid file may not exist yet because another acquirer just\n\t\t\t// created the lock dir and hasn't finished writing its pid file\n\t\t\t// (mkdir + writeFile is not atomic). Give it a short grace window\n\t\t\t// before concluding the owner crashed between mkdir and writeFile.\n\t\t\ttry {\n\t\t\t\tconst stat = await fsp.stat(this.dirPath);\n\t\t\t\treturn Date.now() - stat.mtimeMs > LOCK_STALE_GRACE_MS;\n\t\t\t} catch {\n\t\t\t\t// Lock dir disappeared concurrently (e.g. released mid-check) —\n\t\t\t\t// not stale, just gone; the caller's next mkdir will succeed.\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\tconst pid = Number.parseInt(pidStr.trim(), 10);\n\t\tif (!Number.isFinite(pid) || pid <= 0) return true;\n\t\treturn !isProcessAlive(pid);\n\t}\n\n\tasync release(): Promise<void> {\n\t\tif (!this.acquired) return;\n\t\tthis.acquired = false;\n\t\tawait fsp.rm(this.dirPath, { recursive: true, force: true });\n\t}\n}\n\nfunction defaultWorkspacePath(): string | null {\n\t// The store caller (ToolboxCore) supplies a cwd, but tests want a\n\t// stable default. Return null when the env var says \"no workspace\".\n\tif (process.env.SERVICEME_NO_WORKSPACE_TOOLBOX === \"1\") return null;\n\treturn path.join(process.cwd(), WORKSPACE_TOOLBOX_RELATIVE_PATH);\n}\n\nexport class ToolboxStore {\n\tprivate readonly userFilePath: string;\n\tprivate readonly resolveWorkspacePath: () => string | null;\n\tprivate readonly defaults: readonly DefaultToolSeed[];\n\tprivate readonly hooks: ToolboxStoreHooks;\n\tprivate readonly backend: ToolboxFileBackend;\n\tprivate readonly lockTimeoutMs: number;\n\tprivate readonly lockRetryMs: number;\n\n\tconstructor(opts: ToolboxStoreOptions = {}) {\n\t\tthis.userFilePath = opts.userFilePath ?? getToolboxJsonPath();\n\t\tthis.resolveWorkspacePath = opts.resolveWorkspacePath ?? defaultWorkspacePath;\n\t\tthis.defaults = opts.defaultTools ?? BUILTIN_DEFAULT_TOOLS;\n\t\tthis.hooks = opts.hooks ?? {};\n\t\tthis.backend = opts.backend ?? new FsToolboxFileBackend();\n\t\tthis.lockTimeoutMs = opts.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;\n\t\tthis.lockRetryMs = opts.lockRetryMs ?? DEFAULT_LOCK_RETRY_MS;\n\t}\n\n\t/** Read a scope; returns the resolved toolbox (with defaults merged when empty). */\n\tasync read(scope: ToolboxScope): Promise<ResolvedToolbox> {\n\t\tconst filePath = this.filePathFor(scope);\n\t\tif (scope === \"workspace\") {\n\t\t\tawait migrateLegacyWorkspaceToolboxFile(filePath);\n\t\t}\n\t\tconst stored = filePath ? await this.backend.read(filePath) : null;\n\t\tconst storedTools = stored?.tools ?? [];\n\t\tconst defaults = this.defaults.map((seed, index) => toDefaultTool(seed, index));\n\t\tconst merged = mergeWithDefaults(defaults, storedTools);\n\t\tconst userOnly = storedTools.filter((t) => !this.defaults.some((d) => d.id === t.id));\n\t\tvoid userOnly;\n\t\treturn { scope, tools: merged };\n\t}\n\n\t/**\n\t * Atomic write under a file lock. Replaces the entire `tools` array\n\t * with the provided snapshot.\n\t */\n\tasync write(scope: ToolboxScope, tools: readonly ExternalTool[]): Promise<void> {\n\t\tconst filePath = this.filePathFor(scope);\n\t\tif (!filePath) {\n\t\t\tthrow new Error(`Cannot write toolbox scope '${scope}': file path is unavailable`);\n\t\t}\n\t\tif (scope === \"workspace\") {\n\t\t\tawait migrateLegacyWorkspaceToolboxFile(filePath);\n\t\t}\n\t\tconst payload: PersistedToolbox = {\n\t\t\tversion: TOOLBOX_JSON_SCHEMA_VERSION,\n\t\t\ttools: sortByUserOrder([...tools]),\n\t\t};\n\t\tawait this.hooks.beforeWrite?.(scope, payload);\n\t\tconst lock = new ToolboxFileLock(filePath, this.lockTimeoutMs, this.lockRetryMs);\n\t\tawait lock.acquire();\n\t\ttry {\n\t\t\tawait this.backend.write(filePath, payload);\n\t\t} finally {\n\t\t\tawait lock.release();\n\t\t}\n\t\tawait this.hooks.afterWrite?.(scope, payload);\n\t}\n\n\t/**\n\t * Read-modify-write under the file lock. The mutator receives the\n\t * current user-only list (defaults not included) and returns the\n\t * replacement list. Throwing inside the mutator aborts the write.\n\t */\n\tasync mutate(\n\t\tscope: ToolboxScope,\n\t\tmutator: (current: ExternalTool[]) => Promise<ExternalTool[]>\n\t): Promise<ExternalTool[]> {\n\t\tconst filePath = this.filePathFor(scope);\n\t\tif (!filePath) {\n\t\t\tthrow new Error(`Cannot mutate toolbox scope '${scope}': file path is unavailable`);\n\t\t}\n\t\tif (scope === \"workspace\") {\n\t\t\tawait migrateLegacyWorkspaceToolboxFile(filePath);\n\t\t}\n\t\tconst lock = new ToolboxFileLock(filePath, this.lockTimeoutMs, this.lockRetryMs);\n\t\tawait lock.acquire();\n\t\ttry {\n\t\t\tconst stored = await this.backend.read(filePath);\n\t\t\tconst storedTools = stored?.tools ?? [];\n\t\t\tconst defaultIds = new Set(this.defaults.map((d) => d.id));\n\t\t\tconst userTools = storedTools.filter((t) => !defaultIds.has(t.id));\n\t\t\tconst next = await mutator(userTools);\n\t\t\tconst payload: PersistedToolbox = {\n\t\t\t\tversion: TOOLBOX_JSON_SCHEMA_VERSION,\n\t\t\t\ttools: sortByUserOrder([...next]),\n\t\t\t};\n\t\t\tawait this.hooks.beforeWrite?.(scope, payload);\n\t\t\tawait this.backend.write(filePath, payload);\n\t\t\tawait this.hooks.afterWrite?.(scope, payload);\n\t\t\treturn next;\n\t\t} finally {\n\t\t\tawait lock.release();\n\t\t}\n\t}\n\n\t/** Wipe a scope entirely (used by `toolbox.remove --all` extensions). */\n\tasync clear(scope: ToolboxScope): Promise<void> {\n\t\tconst filePath = this.filePathFor(scope);\n\t\tif (!filePath) return;\n\t\tawait fsp.rm(filePath, { force: true });\n\t}\n\n\t/** Test seam — resolve the user-scope file path. */\n\tgetUserFilePath(): string {\n\t\treturn this.userFilePath;\n\t}\n\n\t/** Test seam — resolve the workspace-scope file path (or null when disabled). */\n\tgetWorkspaceFilePath(): string | null {\n\t\treturn this.resolveWorkspacePath();\n\t}\n\n\tprivate filePathFor(scope: ToolboxScope): string | null {\n\t\treturn scope === \"user\" ? this.userFilePath : this.resolveWorkspacePath();\n\t}\n}\n\nfunction toDefaultTool(seed: DefaultToolSeed, order: number): ExternalTool {\n\treturn {\n\t\tid: seed.id,\n\t\tname: seed.name,\n\t\tdescription: seed.description,\n\t\ticon: seed.icon,\n\t\turl: seed.url,\n\t\tisDefault: true,\n\t\torder,\n\t\tscope: \"user\",\n\t};\n}\n","import * as os from \"node:os\";\nimport * as path from \"node:path\";\n\n/**\n * SERVICEME user home directory helpers.\n *\n * All paths resolve under {@link getServicemeHome}, which is either the\n * `SERVICEME_HOME` environment variable (when set and non-empty) or\n * `$HOME/.serviceme` on POSIX / `%USERPROFILE%\\.serviceme` on Windows.\n *\n * Tests inject `homeDir` and `servicemeHomeEnv` overrides via\n * {@link setUserHomeOverrides} / {@link resetUserHomeOverrides} so they can\n * exercise the path logic without touching the real user environment.\n */\n\n/** Layout constants — kept in one place so other modules can reuse them. */\nexport const SERVICEME_DIR_NAME = \".serviceme\";\nexport const REPOS_SUBDIR = \"repos\";\nexport const CACHE_SUBDIR = \"cache\";\nexport const DRAFTS_SUBDIR = \"drafts\";\nexport const SKILL_DRAFTS_SUBDIR = \"skills\";\nexport const AGENT_DRAFTS_SUBDIR = \"agents\";\nexport const REPOS_CONFIG_FILENAME = \"repos.json\";\nexport const WORKSPACES_SUBDIR = \"workspaces\";\nexport const WORKSPACE_CONTENT_STATE_FILENAME = \"copilot-content-state.json\";\n\n/** rev.20 — r7 BYOM Server Proxy toggle self-state (read by ext + CLI + server). */\nexport const SERVER_PROXY_GLOBAL_FILENAME = \"server-proxy.json\";\n\n/**\n * Repo id regex — used to validate any `repoId` argument before it is joined\n * into a filesystem path. Keeps path traversal attempts out and gives us a\n * predictable on-disk shape.\n */\nexport const SAFE_REPO_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;\n\n/** Environment variable that overrides the user-home root directory. */\nexport const SERVICEME_HOME_ENV = \"SERVICEME_HOME\";\n\n/**\n * Test seam: lets unit tests inject deterministic values for `os.homedir()`\n * and the `SERVICEME_HOME` env override without actually mutating\n * `process.env` (which would leak into other tests).\n */\ninterface UserHomeOverrides {\n\thomeDir?: string | undefined;\n\tservicemeHomeEnv?: string | undefined;\n\tplatform?: NodeJS.Platform | undefined;\n}\n\nlet activeOverrides: UserHomeOverrides = {};\n\nexport function setUserHomeOverrides(overrides: UserHomeOverrides): void {\n\tactiveOverrides = { ...overrides };\n}\n\nexport function resetUserHomeOverrides(): void {\n\tactiveOverrides = {};\n}\n\nfunction resolveHomeDir(): string {\n\tconst injected = activeOverrides.homeDir;\n\tif (injected !== undefined) {\n\t\treturn injected;\n\t}\n\treturn os.homedir();\n}\n\nfunction resolveServicemeHomeEnv(): string | undefined {\n\tconst injected = activeOverrides.servicemeHomeEnv;\n\tif (injected !== undefined) {\n\t\t// Treat empty string as \"not set\" — `process.env` always returns a string\n\t\t// but tests may deliberately pass \"\" to opt out.\n\t\treturn injected.length > 0 ? injected : undefined;\n\t}\n\tconst envValue = process.env[SERVICEME_HOME_ENV];\n\treturn envValue && envValue.length > 0 ? envValue : undefined;\n}\n\nfunction resolvePlatform(): NodeJS.Platform {\n\treturn activeOverrides.platform ?? process.platform;\n}\n\nexport function assertSafeRepoId(repoId: string): string {\n\tif (typeof repoId !== \"string\" || repoId.length === 0 || !SAFE_REPO_ID_PATTERN.test(repoId)) {\n\t\tthrow new Error(\n\t\t\t`Invalid repo id: ${JSON.stringify(repoId)}. ` +\n\t\t\t\t`Must match ${SAFE_REPO_ID_PATTERN} (alphanumeric start, then ` +\n\t\t\t\t`alphanumerics / underscores / hyphens, ≤ 64 chars).`\n\t\t);\n\t}\n\treturn repoId;\n}\n\n/**\n * The raw OS home directory (`os.homedir()`), honoring test overrides\n * ({@link setUserHomeOverrides}). Exported for callers that need a\n * home-relative path *outside* of `~/.serviceme` — e.g. the\n * `~/.agents/{skills,agents}` convention used by `SkillLinker` for\n * user-scope links.\n */\nexport function getHomeDir(): string {\n\treturn resolveHomeDir();\n}\n\n/**\n * Root directory for all SERVICEME user-level state. Resolves to\n * `${SERVICEME_HOME}` when that env var is set, otherwise `${HOME}/.serviceme`\n * (POSIX) or `%USERPROFILE%\\.serviceme` (Windows via `os.homedir`).\n */\nexport function getServicemeHome(): string {\n\tconst override = resolveServicemeHomeEnv();\n\tif (override !== undefined) {\n\t\treturn path.resolve(override);\n\t}\n\treturn path.join(resolveHomeDir(), SERVICEME_DIR_NAME);\n}\n\n/** `$HOME/.serviceme/repos` (or `${SERVICEME_HOME}/repos`). */\nexport function getReposDir(): string {\n\treturn path.join(getServicemeHome(), REPOS_SUBDIR);\n}\n\n/** `$HOME/.serviceme/repos/<repoId>` — validates `repoId` first. */\nexport function getRepoDir(repoId: string): string {\n\treturn path.join(getReposDir(), assertSafeRepoId(repoId));\n}\n\n/** `$HOME/.serviceme/cache` — generic per-user cache. */\nexport function getCacheDir(): string {\n\treturn path.join(getServicemeHome(), CACHE_SUBDIR);\n}\n\n/** `$HOME/.serviceme/cache/<repoId>` — per-repo sync state lives here. */\nexport function getRepoCacheDir(repoId: string): string {\n\treturn path.join(getCacheDir(), assertSafeRepoId(repoId));\n}\n\n/** `$HOME/.serviceme/drafts` — local edits not yet pushed upstream. */\nexport function getDraftsDir(): string {\n\treturn path.join(getServicemeHome(), DRAFTS_SUBDIR);\n}\n\n/** `$HOME/.serviceme/drafts/skills` — local skill drafts. */\nexport function getSkillDraftsDir(): string {\n\treturn path.join(getDraftsDir(), SKILL_DRAFTS_SUBDIR);\n}\n\n/** `$HOME/.serviceme/drafts/agents` — local agent drafts. */\nexport function getAgentDraftsDir(): string {\n\treturn path.join(getDraftsDir(), AGENT_DRAFTS_SUBDIR);\n}\n\n/**\n * `$HOME/.serviceme/repos.json` — the single config entry point for repo\n * metadata. Always under the resolved home root (i.e. follows\n * `SERVICEME_HOME` overrides too).\n */\nexport function getReposConfigPath(): string {\n\treturn path.join(getServicemeHome(), REPOS_CONFIG_FILENAME);\n}\n\n/** `~/.serviceme/workspaces` — machine-local per-workspace state root. */\nexport function getWorkspacesDir(): string {\n\treturn path.join(getServicemeHome(), WORKSPACES_SUBDIR);\n}\n\n/**\n * Convenience helper for callers that need to switch behaviour on platform\n * (e.g. `SkillLinker` chooses symlink vs junction). Exported mostly so tests\n * can pin the value without touching `process.platform` directly.\n */\nexport function getHomePlatform(): NodeJS.Platform {\n\treturn resolvePlatform();\n}\n\n// ─── Scheduled Tasks paths (added by M1.5.3) ───────────────────────────────\n\n/** Layout constants for scheduled-tasks files. Kept here so other modules\n * can reuse them and the names stay in sync with the design doc. */\nexport const SCHEDULED_TASKS_CONFIG_FILENAME = \"scheduled-tasks.json\";\nexport const SCHEDULED_TASKS_LOG_FILENAME = \"scheduled-tasks-log.json\";\nexport const SCHEDULER_PID_FILENAME = \"scheduler.pid\";\nexport const SCHEDULER_LOCK_FILENAME = \"scheduler.lock\";\nexport const SCHEDULER_LOG_FILENAME = \"scheduler.log\";\nexport const MIGRATION_FAILURES_FILENAME = \"migration-failures.json\";\nexport const KNOWN_WORKSPACES_FILENAME = \"known-workspaces.json\";\n\n/** `~/.serviceme/scheduled-tasks.json` — the global v2 task list. */\nexport function getScheduledTasksConfigPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULED_TASKS_CONFIG_FILENAME);\n}\n\n/** `~/.serviceme/scheduled-tasks-log.json` — the global execution log (200 LRU). */\nexport function getScheduledTasksLogPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULED_TASKS_LOG_FILENAME);\n}\n\n/** `~/.serviceme/scheduler.pid` — global daemon PID file. */\nexport function getSchedulerPidPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULER_PID_FILENAME);\n}\n\n/** `~/.serviceme/scheduler.lock` — global daemon startup flock. */\nexport function getSchedulerLockPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULER_LOCK_FILENAME);\n}\n\n/** `~/.serviceme/scheduler.log` — global daemon log (daemon lifecycle, not task execution). */\nexport function getSchedulerLogPath(): string {\n\treturn path.join(getServicemeHome(), SCHEDULER_LOG_FILENAME);\n}\n\n/** `~/.serviceme/migration-failures.json` — diagnostics for failed v1→v2 imports. */\nexport function getMigrationFailuresPath(): string {\n\treturn path.join(getServicemeHome(), MIGRATION_FAILURES_FILENAME);\n}\n\n/** `~/.serviceme/known-workspaces.json` — workspace list the extension has seen. */\nexport function getKnownWorkspacesPath(): string {\n\treturn path.join(getServicemeHome(), KNOWN_WORKSPACES_FILENAME);\n}\n\n// ─── r7 BYOM Server Proxy (rev.20) ─────────────────────────────────────────\n\n/**\n * `~/.serviceme/server-proxy.json` — r7 BYOM Server Proxy toggle\n * self-state. Written by the extension's `ProxyConfigService`, readable\n * by the CLI / Server so they can decide whether to route traffic\n * through the corporate proxy without booting VS Code.\n *\n * Lives under user-level `~/.serviceme/` (not under a workspace) because\n * the toggle is a *user preference* — the same human enabling it on\n * machine A should be able to rely on it on machine B after sync, not\n * have it disappear when they switch repos.\n */\nexport function getServerProxyGlobalPath(): string {\n\treturn path.join(getServicemeHome(), SERVER_PROXY_GLOBAL_FILENAME);\n}\n\n// ─── Phase 5 (auth + device + toolbox) placeholders (Spec §11.12) ────────\n\n/** Layout constants for the Phase 5 client-side state files. The server\n * already has the corresponding routes (`/api/v1/auth/...`,\n * `/api/v1/device/...`, etc.) — the client just needs the on-disk\n * file paths + a first-run bootstrap so the auth/device/toolbox\n * services can open + read them without a per-call existence check. */\nexport const CREDENTIALS_CONFIG_FILENAME = \"credentials.json\";\nexport const DEVICE_JSON_FILENAME = \"device.json\";\nexport const TOOLBOX_JSON_FILENAME = \"toolbox.json\";\nexport const MACHINE_ID_FILENAME = \"machine-id\";\nexport const PROFILES_JSON_FILENAME = \"profiles.json\";\n\n/** `~/.serviceme/credentials.json` — better-auth session tokens + refresh state (client-side cache). */\nexport function getCredentialsConfigPath(): string {\n\treturn path.join(getServicemeHome(), CREDENTIALS_CONFIG_FILENAME);\n}\n\n/** `~/.serviceme/device.json` — device enrollment payload (id, public key fingerprint, claimed-by). */\nexport function getDeviceJsonPath(): string {\n\treturn path.join(getServicemeHome(), DEVICE_JSON_FILENAME);\n}\n\n/** `~/.serviceme/toolbox.json` — local toolbox state (installed tool refs, per-user prefs). */\nexport function getToolboxJsonPath(): string {\n\treturn path.join(getServicemeHome(), TOOLBOX_JSON_FILENAME);\n}\n\n/** `~/.serviceme/machine-id` — opaque stable per-install id (uuid v4 string, no JSON wrapper). */\nexport function getMachineIdPath(): string {\n\treturn path.join(getServicemeHome(), MACHINE_ID_FILENAME);\n}\n\n/** `~/.serviceme/profiles.json` — cached projection of server-side profile rows; refresh on demand. */\nexport function getProfilesJsonPath(): string {\n\treturn path.join(getServicemeHome(), PROFILES_JSON_FILENAME);\n}\n","/**\n * toolbox types — internal-only data shapes.\n *\n * The public data model lives in `@serviceme/devtools-protocol/toolbox`\n * (`ExternalTool`, `ToolboxScope`, `ToolboxList`). The types below are\n * the persisted on-disk shape and the patch helpers, scoped to the\n * toolbox domain only.\n *\n * Refs:\n * - 4.功能规划.md §2.3 — `toolbox/types.ts`\n */\n\nimport type { ExternalTool, ToolboxScope } from \"@serviceme/devtools-protocol\";\n\n/** Schema version of the on-disk toolbox JSON files. Bumped on breaking changes. */\nexport const TOOLBOX_JSON_SCHEMA_VERSION = 1;\n\nexport interface PersistedToolbox {\n\tversion: number;\n\ttools: ExternalTool[];\n}\n\n/** Patch payload accepted by `ToolboxCore.update` (mirrors `ExternalToolPatch`). */\nexport type ToolboxPatch = Partial<Omit<ExternalTool, \"id\" | \"isDefault\">>;\n\n/**\n * Built-in (default) tool seeds. Mirrors the Extension's\n * `apps/extension/src/config/external-tools.ts`. The Core stores these\n * verbatim and never mutates them — the Extension's `ToolBoxService`\n * runs the same merge on top of the Core's view.\n */\nexport interface DefaultToolSeed {\n\tid: string;\n\tname: string;\n\tdescription: string;\n\ticon: string;\n\turl: string;\n}\n\n/** Built-in tools rendered when the JSON file is missing or empty. */\nexport const BUILTIN_DEFAULT_TOOLS: readonly DefaultToolSeed[] = [\n\t{\n\t\tid: \"builtin-docs\",\n\t\tname: \"SERVICEME Docs\",\n\t\tdescription: \"Official documentation portal\",\n\t\ticon: \"book\",\n\t\turl: \"https://docs.medalsoft.com/serviceme\",\n\t},\n\t{\n\t\tid: \"builtin-issues\",\n\t\tname: \"Issue Tracker\",\n\t\tdescription: \"Report bugs and feature requests\",\n\t\ticon: \"bug\",\n\t\turl: \"https://github.com/medalsoftchina/ms-devtools-vscode/issues\",\n\t},\n\t{\n\t\tid: \"builtin-changelog\",\n\t\tname: \"Changelog\",\n\t\tdescription: \"Release notes for every published version\",\n\t\ticon: \"history\",\n\t\turl: \"https://github.com/medalsoftchina/ms-devtools-vscode/releases\",\n\t},\n];\n\n/** Resolved toolbox shape returned by `ToolboxStore.read()`. */\nexport interface ResolvedToolbox {\n\tscope: ToolboxScope;\n\ttools: ExternalTool[];\n}\n","/**\n * ToolboxCore — Main entry for the toolbox domain.\n *\n * Orchestrates the user + workspace scopes via `ToolboxStore`, applies\n * the \"最近使用置顶\" sort, and exposes a small surface that mirrors\n * the Phase 5.1 bridge methods:\n *\n * - `list(scope?)` → `toolbox.list`\n * - `add(tool, scope)` → `toolbox.add`\n * - `remove(id, scope?)` → `toolbox.remove`\n * - `update(id, patch, scope?)` → `toolbox.update`\n *\n * Default tools (built-in seeds) are never user-deletable. `remove()`\n * is a no-op for default tools and surfaces a structured error so\n * callers can map it to `TOOLBOX_DEFAULT_IMMUTABLE` (Phase 5.3 error\n * code).\n *\n * Refs:\n * - 4.功能规划.md §2.3 — `ToolboxCore.ts 主入口`\n * - `3.功能拆分.md` §3 — wire shape\n */\n\nimport type { ExternalTool, ToolboxList, ToolboxScope } from \"@serviceme/devtools-protocol\";\n\nimport { sortByRecentFirst, sortByUserOrder, touchLastUsedAt } from \"./sort\";\nimport { ToolboxStore } from \"./ToolboxStore\";\nimport { BUILTIN_DEFAULT_TOOLS, type DefaultToolSeed, type ToolboxPatch } from \"./types\";\n\n/** Sentinel — caller tried to remove a built-in (immutable) tool. */\nexport class DefaultToolImmutableError extends Error {\n\tconstructor(public readonly toolId: string) {\n\t\tsuper(`Cannot remove default toolbox entry: ${toolId}`);\n\t\tthis.name = \"DefaultToolImmutableError\";\n\t}\n}\n\nexport interface ToolboxCoreOptions {\n\tstore?: ToolboxStore;\n\tdefaultTools?: readonly DefaultToolSeed[];\n\t/** Sort applied to the merged list returned by `list()`. Default: `sortByUserOrder`. */\n\tlistSort?: (tools: readonly ExternalTool[]) => ExternalTool[];\n}\n\nexport class ToolboxCore {\n\tprivate readonly store: ToolboxStore;\n\tprivate readonly defaults: readonly DefaultToolSeed[];\n\tprivate readonly listSort: (tools: readonly ExternalTool[]) => ExternalTool[];\n\n\tconstructor(opts: ToolboxCoreOptions = {}) {\n\t\tthis.store = opts.store ?? new ToolboxStore();\n\t\tthis.defaults = opts.defaultTools ?? BUILTIN_DEFAULT_TOOLS;\n\t\tthis.listSort = opts.listSort ?? sortByUserOrder;\n\t}\n\n\t/** List all tools in a scope. Built-in defaults are merged in. */\n\tasync list(scope: ToolboxScope = \"user\"): Promise<ToolboxList> {\n\t\tconst resolved = await this.store.read(scope);\n\t\treturn {\n\t\t\tscope,\n\t\t\ttools: this.listSort(resolved.tools),\n\t\t};\n\t}\n\n\t/**\n\t * Append a new tool to the requested scope. Default tools are\n\t * rejected (they are seeds, not user entries).\n\t */\n\tasync add(tool: ExternalTool, scope: ToolboxScope = \"user\"): Promise<ToolboxList> {\n\t\tif (this.isDefaultId(tool.id)) {\n\t\t\tthrow new DefaultToolImmutableError(tool.id);\n\t\t}\n\t\tconst next = await this.store.mutate(scope, async (current) => {\n\t\t\tconst filtered = current.filter((t) => t.id !== tool.id);\n\t\t\treturn [\n\t\t\t\t...filtered,\n\t\t\t\t{\n\t\t\t\t\t...tool,\n\t\t\t\t\tscope,\n\t\t\t\t\tisDefault: false,\n\t\t\t\t\torder: tool.order ?? filtered.length,\n\t\t\t\t},\n\t\t\t];\n\t\t});\n\t\treturn { scope, tools: next };\n\t}\n\n\t/**\n\t * Remove a tool by id. Returns `success: false` when the id is a\n\t * built-in default (idempotent, never throws on missing entries).\n\t */\n\tasync remove(\n\t\tid: string,\n\t\tscope: ToolboxScope = \"user\"\n\t): Promise<{ scope: ToolboxScope; toolId: string; success: boolean }> {\n\t\tif (this.isDefaultId(id)) {\n\t\t\tthrow new DefaultToolImmutableError(id);\n\t\t}\n\t\tconst removed = await this.store.mutate(scope, async (current) =>\n\t\t\tcurrent.filter((t) => t.id !== id)\n\t\t);\n\t\treturn { scope, toolId: id, success: !removed.some((t) => t.id === id) };\n\t}\n\n\t/**\n\t * Patch a tool by id. Default tools can only have their `order`\n\t * updated; other patches are silently ignored for default entries\n\t * (callers can compare before/after to detect the ignore).\n\t */\n\tasync update(\n\t\tid: string,\n\t\tpatch: ToolboxPatch,\n\t\tscope: ToolboxScope = \"user\"\n\t): Promise<ToolboxList> {\n\t\tconst isDefault = this.isDefaultId(id);\n\t\tconst next = await this.store.mutate(scope, async (current) => {\n\t\t\tif (isDefault) {\n\t\t\t\t// Default tools are not stored on disk — silently ignore\n\t\t\t\t// non-order patches. The order field is also not persisted\n\t\t\t\t// (default order is set in memory at merge time), so the\n\t\t\t\t// caller can compare before/after to detect the ignore.\n\t\t\t\treturn current;\n\t\t\t}\n\t\t\treturn current.map((tool) =>\n\t\t\t\ttool.id === id ? { ...tool, ...patch, scope, isDefault: false } : tool\n\t\t\t);\n\t\t});\n\t\treturn { scope, tools: next };\n\t}\n\n\t/**\n\t * Stamp `lastUsedAt` on the targeted tool. This is the \"recently\n\t * used\" hook the Extension's webview uses when a user clicks a\n\t * toolbox entry.\n\t */\n\tasync recordUsage(\n\t\tid: string,\n\t\tscope: ToolboxScope = \"user\",\n\t\twhen: Date = new Date()\n\t): Promise<ExternalTool | null> {\n\t\tif (this.isDefaultId(id)) {\n\t\t\t// Defaults live in memory only — return a synthetic record so\n\t\t\t// callers can render the click without persisting anything.\n\t\t\tconst seed = this.defaults.find((d) => d.id === id);\n\t\t\tif (!seed) return null;\n\t\t\treturn { ...seed, scope, isDefault: true, order: 0, lastUsedAt: when.toISOString() };\n\t\t}\n\t\tlet updated: ExternalTool | null = null;\n\t\tawait this.store.mutate(scope, async (current) => {\n\t\t\tconst next = touchLastUsedAt(current, id, when);\n\t\t\tupdated = next.find((t) => t.id === id) ?? null;\n\t\t\treturn next;\n\t\t});\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Combined view: user + workspace scopes merged, sorted by recent\n\t * usage. Workspace tools overlay user tools (workspace entries win\n\t * on `id` collision).\n\t */\n\tasync listMerged(): Promise<ToolboxList> {\n\t\tconst [user, workspace] = await Promise.all([\n\t\t\tthis.store.read(\"user\"),\n\t\t\tthis.store.read(\"workspace\"),\n\t\t]);\n\t\tconst seen = new Set<string>();\n\t\tconst merged: ExternalTool[] = [];\n\t\tfor (const tool of workspace.tools) {\n\t\t\tseen.add(tool.id);\n\t\t\tmerged.push({ ...tool, scope: \"workspace\" });\n\t\t}\n\t\tfor (const tool of user.tools) {\n\t\t\tif (seen.has(tool.id)) continue;\n\t\t\tmerged.push({ ...tool, scope: \"user\" });\n\t\t}\n\t\treturn { scope: \"user\", tools: sortByRecentFirst(merged) };\n\t}\n\n\t/** Expose the underlying store (CLI / Bridge use it for path-level access). */\n\tgetStore(): ToolboxStore {\n\t\treturn this.store;\n\t}\n\n\tprivate isDefaultId(id: string): boolean {\n\t\treturn this.defaults.some((d) => d.id === id);\n\t}\n}\n"],"mappings":"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","names":["path","stat"]}