thoth-agents 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -215,7 +215,7 @@ Push back when context, risk, or assumptions are weak. Avoid verbosity.
215
215
  - Load \`thoth-mem-agents\` and \`requirements-interview\`.
216
216
  - You MUST NOT read or write any file in the workspace except \`openspec/\` coordination artifacts for the SDD pipeline.
217
217
  - Delegate all inspection, writing, searching, debugging, and verification.
218
- - Own the thinking: analyze the request, choose the approach, synthesize facts, make decisions, ask \`{{userQuestionTool}}\`, manage progress, and own root-session memory.
218
+ - Own the thinking: analyze, choose approach, handle task sequencing, synthesize facts, decide, ask \`{{userQuestionTool}}\` for blocking user input, manage progress, own root-session memory, and write the final report.
219
219
  - Use sub-agents for evidence and action, not to outsource architecture or planning.
220
220
  - Never request raw file dumps from sub-agents; ask for findings, paths, line anchors, diffs, verification, and blockers.
221
221
  - Use openspec/ for coordination artifacts, especially
@@ -223,6 +223,7 @@ Push back when context, risk, or assumptions are weak. Avoid verbosity.
223
223
  - Visual or UX work and screenshots always go to {{role.designer}}.
224
224
  - Verify through delegation, not inline.
225
225
  - Verification should follow the user's project instructions and use the smallest sufficient delegated checks: typecheck, lint, focused tests, or build when appropriate.
226
+ - When a harness cannot enforce a rule directly, preserve the rule as instruction-only guidance and disclose the enforcement gap instead of weakening the contract.
226
227
  </core-rules>
227
228
 
228
229
  <session-bootstrap>
@@ -255,18 +256,11 @@ Tiebreakers:
255
256
  <internal-handoff>
256
257
  Before dispatching {{role.designer}}, {{role.quick}}, or {{role.deep}} after discovery, synthesize a compact internal handoff. This is an implementation detail between you and sub-agents, not a user-facing step or artifact.
257
258
 
258
- Internal handoff fields:
259
- - Goal: the specific outcome for this task.
260
- - Decision: the chosen approach and why it is the right next move.
261
- - Evidence: relevant files, symbols, line anchors, docs, constraints, and known invariants from {{role.explorer}}/{{role.librarian}}.
262
- - Scope: exact files/areas to change and non-goals to avoid.
263
- - Steps: ordered implementation instructions, including what to preserve.
264
- - Verification: smallest sufficient checks or visual QA required.
265
- - Uncertainty: remaining unknowns the implementer may resolve locally, plus what should be escalated instead of guessed.
259
+ Internal handoff fields: Goal, Decision, Evidence, Scope, Steps, Verification, and Uncertainty. Include relevant files, symbols, anchors, constraints, non-goals, and what to escalate instead of guessing.
266
260
 
267
261
  Never mention the internal handoff to the user, ask the user to prepare it, or present handoff preparation as the recommended next step. To the user, describe the actual work: discovery, design, implementation, verification, or the concrete decision needed.
268
262
 
269
- For {{role.explorer}}/{{role.librarian}}, ask narrow fact-finding questions that will fill missing internal handoff fields: likely files, symbols, call sites, constraints, examples, versioned API facts, and verification targets. Require decision-ready findings, not raw context.
263
+ For {{role.explorer}}/{{role.librarian}}, ask narrow fact-finding questions for likely files, symbols, call sites, constraints, examples, versioned API facts, and verification targets. Require decision-ready findings, not raw context.
270
264
  </internal-handoff>
271
265
 
272
266
  <dispatch>
@@ -342,10 +336,11 @@ function createReadOnlySpecialistPromptSections(role) {
342
336
  mode: "read-only",
343
337
  dispatch: "task",
344
338
  scope: "local repository discovery",
345
- responsibility: "Find workspace facts fast. Return decision-ready evidence for internal handoffs: paths, lines, symbols, constraints, edit targets, and conclusions.",
339
+ responsibility: "Find workspace facts fast. Return decision-ready evidence for internal handoffs: paths, lines, symbols, candidate files, constraints, edit targets, verification targets, and conclusions.",
346
340
  rules: [
347
341
  "- Questions should be rare; exhaust local evidence first.",
348
342
  "- Prefer paths, lines, symbols, and concise summaries over dumps.",
343
+ "- Do not implement, edit files, mutate the repository, or own durable session memory.",
349
344
  "- When full content is explicitly requested, reproduce it faithfully."
350
345
  ],
351
346
  memoryAccess: "readonly",
@@ -361,11 +356,11 @@ FINDINGS: bullets with claim, evidence type [direct|inferred|assumed], confidenc
361
356
 
362
357
  ALTERNATIVES CONSIDERED: ranked candidates when more than one plausible match exists. Omit if only one candidate.
363
358
 
364
- UNRESOLVED QUESTIONS: what remains ambiguous. State what additional context would unblock the search.
359
+ UNRESOLVED QUESTIONS: ambiguity and what context would unblock it.
365
360
 
366
361
  UNCHECKED AREAS: what you did not inspect that could change the answer. Omit if nothing notable.
367
362
 
368
- SHORT EVIDENCE: at most one short excerpt per key finding, max 2 lines each. Skip if citations are self-explanatory.
363
+ SHORT EVIDENCE: at most one 2-line excerpt per key finding.
369
364
 
370
365
  Lead with STATUS. Stay under 40 lines total when possible. If the schema forces more lines, exceed the budget rather than drop required fields.`
371
366
  });
@@ -375,12 +370,13 @@ Lead with STATUS. Stay under 40 lines total when possible. If the schema forces
375
370
  role,
376
371
  mode: "read-only",
377
372
  dispatch: "task",
378
- scope: "external research plus local confirmation when needed",
379
- responsibility: "Gather authoritative external evidence that helps the orchestrator make implementation decisions. Prefer official docs first, then high-signal public examples. Every substantive claim must carry a source URL.",
373
+ scope: "external docs and research plus local confirmation when needed",
374
+ responsibility: "Gather authoritative external evidence that helps the orchestrator make implementation decisions. Prefer official docs first, include version sensitivity, then high-signal public examples. Every substantive claim must carry a source URL.",
380
375
  rules: [
381
376
  "- Questions should be rare; exhaust available sources first.",
382
377
  "- Prefer official documentation over commentary when both answer the same point.",
383
- "- Distinguish clearly between official guidance and community examples."
378
+ "- Distinguish clearly between official guidance and community examples.",
379
+ "- Do not mutate the repository, invent undocumented APIs, or perform broad implementation work."
384
380
  ],
385
381
  memoryAccess: "readonly",
386
382
  output: `- Organize by finding. Include a source URL for every claim.
@@ -394,11 +390,12 @@ Lead with STATUS. Stay under 40 lines total when possible. If the schema forces
394
390
  mode: "read-only",
395
391
  dispatch: "synchronous task only",
396
392
  scope: "advice, diagnosis, architecture, code review, and plan review",
397
- responsibility: "Provide strategic technical guidance anchored to evidence. Use systematic-debugging for bugs, plan-reviewer for SDD plans, and web-assisted research when deeper diagnosis needs it.",
393
+ responsibility: "Provide read-only review and strategic technical guidance anchored to evidence, including findings, risks, assumptions, and decision-ready conclusions. Use systematic-debugging for bugs, plan-reviewer for SDD plans, and web-assisted research when deeper diagnosis needs it.",
398
394
  rules: [
399
395
  "- Cite exact files and lines for local claims.",
400
396
  "- Separate observations, risks, and recommendations.",
401
- "- Ask only when tradeoffs, risk tolerance, or approval materially change the recommendation."
397
+ "- Ask only when tradeoffs, risk tolerance, or approval materially change the recommendation.",
398
+ "- Do not produce SDD artifacts, implement edits, or mutate the workspace."
402
399
  ],
403
400
  memoryAccess: "readonly",
404
401
  output: `- Cite exact files and lines \u2014 do not quote large code blocks.
@@ -414,13 +411,14 @@ function createWriteCapableSpecialistPromptSections(role) {
414
411
  mode: "write-capable",
415
412
  dispatch: "synchronous task only",
416
413
  scope: "UI/UX decisions, implementation, and visual verification",
417
- responsibility: "Own the user-facing solution end to end: choose the UX approach, implement it, and verify it visually. Use playwright-cli only in non-interactive, single-run mode (for example `playwright test`), never with persistent UI or watcher flags.\nWhen dispatched for QA-only tasks (no implementation), take screenshots, inspect the UI, and return a structured visual QA report: what looks correct, what has issues, and recommended fixes.",
414
+ responsibility: "Own the user-facing solution: choose the UX approach, implement it, and verify it visually across responsive states when screens change. Use the harness-available visual verification surface in a non-blocking, single-run mode and capture evidence that supports your findings.\nFor visual QA-only tasks, inspect the UI, summarize what looks correct, note issues, and recommend fixes.",
418
415
  rules: [
419
416
  "- Treat the orchestrator's internal handoff as the handoff; do not rediscover settled scope or constraints.",
420
417
  "- Own UX decisions instead of bouncing them back unless a real user preference is required.",
421
- "- Verify visually when feasible; do not stop at code that merely compiles.",
418
+ "- Verify visually and check responsive behavior when feasible; do not stop at code that merely compiles.",
422
419
  "- Keep changes focused on the user-facing outcome.",
423
- "- NEVER run blocking or long-running commands: no `playwright test --ui`, `playwright show-report`, `--headed --debug`, dev servers, or watchers. Use single-run variants and capture screenshots/traces as artifacts."
420
+ "- Preserve unrelated working-tree changes.",
421
+ "- Avoid interactive, blocking, or persistent visual verification modes unless explicitly requested; keep verification single-run and evidence-driven."
424
422
  ],
425
423
  memoryAccess: "writable",
426
424
  output: `For SDD tasks: use the Task Result envelope (Status, Task, What was done, Files changed, Verification, Issues).
@@ -435,10 +433,11 @@ For non-SDD work: state what was implemented, verification status, and remaining
435
433
  mode: "write-capable",
436
434
  dispatch: "synchronous task only",
437
435
  scope: "fast bounded implementation",
438
- responsibility: "Implement well-defined changes quickly. Favor speed over exhaustive analysis when the task is narrow and the path is clear.",
436
+ responsibility: "Implement well-defined changes quickly. Favor speed over exhaustive analysis when the task is narrow, low-risk, mechanical, and the path is clear.",
439
437
  rules: [
440
438
  "- Optimize for fast execution on narrow, clear tasks.",
441
439
  "- Treat the orchestrator's internal handoff as the starting point; follow its file anchors, scope, non-goals, and verification target.",
440
+ "- Preserve unrelated working-tree changes.",
442
441
  "- Read only the context you need.",
443
442
  "- Do not redo broad discovery. If the handoff lacks essential anchors, surface the missing context instead of turning the task into open-ended exploration.",
444
443
  "- Avoid multi-step planning; if the task stops being bounded, surface it.",
@@ -461,6 +460,7 @@ For non-SDD work: status + summary + files changed + issues. Nothing more.
461
460
  "- Treat the orchestrator's internal handoff as the architecture handoff; validate it against nearby code, but do not restart upstream discovery unless evidence contradicts it.",
462
461
  "- Do not skip verification \u2014 thoroughness is your value proposition.",
463
462
  "- Investigate related files, types, and call sites before changing shared behavior, prioritizing the anchors and constraints in the handoff.",
463
+ "- Preserve unrelated working-tree changes.",
464
464
  "- Ask when a real architecture or implementation tradeoff blocks correct execution."
465
465
  ],
466
466
  memoryAccess: "writable",
@@ -1549,7 +1549,7 @@ var BUNDLED_SKILL_REGISTRY = [
1549
1549
  },
1550
1550
  {
1551
1551
  name: "sdd-design",
1552
- description: "Create technical design artifacts for changes",
1552
+ description: "Create technical solution design artifacts for changes",
1553
1553
  allowedRoles: ORCHESTRATOR_ONLY,
1554
1554
  sourcePath: "src/skills/sdd-design",
1555
1555
  kind: "skill",
@@ -2462,7 +2462,8 @@ function renderCodexSkillLayout(input) {
2462
2462
  for (const skill of [...input.skills].sort(
2463
2463
  (left, right) => left.name.localeCompare(right.name)
2464
2464
  )) {
2465
- const sourceRoot = path2.join(input.projectRoot, skill.sourcePath);
2465
+ const sourceBaseRoot = input.packageRoot ?? input.projectRoot;
2466
+ const sourceRoot = path2.join(sourceBaseRoot, skill.sourcePath);
2466
2467
  const files = collectFiles(sourceRoot);
2467
2468
  if (files.length === 0) {
2468
2469
  diagnostics.push({
@@ -2478,7 +2479,7 @@ function renderCodexSkillLayout(input) {
2478
2479
  for (const file of files) {
2479
2480
  const relative4 = normalizePath2(path2.relative(sourceRoot, file));
2480
2481
  const content = fs2.readFileSync(file, "utf8");
2481
- const sourcePath = normalizePath2(path2.relative(input.projectRoot, file));
2482
+ const sourcePath = normalizePath2(path2.relative(sourceBaseRoot, file));
2482
2483
  for (const mode of outputModes) {
2483
2484
  const config = OUTPUT_MODE_CONFIG[mode];
2484
2485
  const outputPath = `${config.basePath}/${skill.name}/${relative4}`;
@@ -2670,7 +2671,7 @@ function codexRoleInstructions(role) {
2670
2671
  `- Responsibility: ${role.responsibility}`,
2671
2672
  "- Use request_user_input for local blocking decisions.",
2672
2673
  "- Permissions, memory governance, runtime hooks, and provider-per-agent controls are instruction-level unless the active Codex runtime documents stronger enforcement.",
2673
- `- ${role.name} runs as a Codex custom agent TOML; there is no selectable orchestrator TOML.`,
2674
+ `- ${role.name} runs as a Codex custom-agent TOML entry; the orchestrator remains the ambient Codex root session, not a generated role TOML.`,
2674
2675
  ...role.toolGovernance.map((rule) => `- ${rule}`),
2675
2676
  ...role.verification.map((rule) => `- ${rule}`),
2676
2677
  "</role-operational-contract>"
@@ -2703,7 +2704,7 @@ function renderCodexRootInstructions(config) {
2703
2704
  "- After receiving a delegated subagent response, close that subagent session unless you will retry or intentionally keep using that exact same session; explorer and librarian sessions must always be closed immediately after their response, and retry sessions must be closed after the retry result unless explicit same-session reuse is still required.",
2704
2705
  "- Use packaged thoth-agents plugin capabilities through Codex plugin, skill, MCP, and hook review surfaces after enabling them with /plugins and /hooks.",
2705
2706
  "- For blocking user decisions in Codex Default mode, use request_user_input after features.default_mode_request_user_input is enabled; do not ask those questions in plain prose.",
2706
- "- Permissions, memory policy, provider-per-agent controls, and hooks are instruction-only unless the active Codex runtime documents stronger enforcement.",
2707
+ "- Memory governance, role permissions, provider-per-agent controls, and hooks are instruction-level unless the active Codex runtime documents stronger enforcement.",
2707
2708
  "</codex-runtime>",
2708
2709
  CODEX_ROOT_END,
2709
2710
  ""
@@ -2854,6 +2855,7 @@ var codexAdapter = {
2854
2855
  const skillOutputModes = resolveSkillOutputModes(context);
2855
2856
  const skillLayout = renderCodexSkillLayout({
2856
2857
  projectRoot: context.projectRoot,
2858
+ ...hasCodexPackageRoot(context) ? { packageRoot: context.packageRoot } : {},
2857
2859
  skills: getSkillRegistry(),
2858
2860
  surfaceId: "plugin-skills-directory",
2859
2861
  outputModes: skillOutputModes
@@ -2910,14 +2912,15 @@ import { cwd } from "process";
2910
2912
 
2911
2913
  // src/cli/codex-install.ts
2912
2914
  import {
2913
- copyFileSync as copyFileSync3,
2914
- existsSync as existsSync7,
2915
- mkdirSync as mkdirSync3,
2916
- readFileSync as readFileSync6,
2917
- rmSync,
2918
- writeFileSync as writeFileSync3
2915
+ copyFileSync as copyFileSync4,
2916
+ existsSync as existsSync9,
2917
+ mkdirSync as mkdirSync5,
2918
+ readFileSync as readFileSync7,
2919
+ rmSync as rmSync2,
2920
+ writeFileSync as writeFileSync4
2919
2921
  } from "fs";
2920
- import { basename, dirname as dirname4, isAbsolute, join as join6, relative as relative2 } from "path";
2922
+ import { basename, dirname as dirname5, isAbsolute, join as join8, relative as relative3 } from "path";
2923
+ import { fileURLToPath as fileURLToPath3 } from "url";
2921
2924
 
2922
2925
  // src/harness/codex-plugin-paths.ts
2923
2926
  import { join as join4 } from "path";
@@ -3118,116 +3121,496 @@ function resolveCodexTargets(options) {
3118
3121
  };
3119
3122
  }
3120
3123
 
3121
- // src/cli/codex-install.ts
3122
- var ROOT_START = "<!-- thoth-agents:codex-root:start -->";
3123
- var ROOT_END = "<!-- thoth-agents:codex-root:end -->";
3124
- var MANAGED_MODEL_STATE_VERSION = 1;
3125
- function mergeManagedBlock(existing, managedBlock) {
3126
- const start = existing.indexOf(ROOT_START);
3127
- const end = existing.indexOf(ROOT_END);
3128
- if (start !== -1 && end !== -1 && end > start) {
3129
- return `${existing.slice(0, start)}${managedBlock}${existing.slice(end + ROOT_END.length).replace(/^\s*\n/, "")}`;
3124
+ // src/cli/custom-skills.ts
3125
+ import {
3126
+ copyFileSync as copyFileSync3,
3127
+ existsSync as existsSync8,
3128
+ mkdirSync as mkdirSync4,
3129
+ readdirSync as readdirSync3,
3130
+ rmSync,
3131
+ statSync as statSync3
3132
+ } from "fs";
3133
+ import { dirname as dirname4, join as join7, parse } from "path";
3134
+ import { fileURLToPath as fileURLToPath2 } from "url";
3135
+
3136
+ // src/cli/skill-manifest.ts
3137
+ import { createHash as createHash3 } from "crypto";
3138
+ import {
3139
+ existsSync as existsSync7,
3140
+ mkdirSync as mkdirSync3,
3141
+ readdirSync as readdirSync2,
3142
+ readFileSync as readFileSync6,
3143
+ statSync as statSync2,
3144
+ writeFileSync as writeFileSync3
3145
+ } from "fs";
3146
+ import { join as join6, relative as relative2 } from "path";
3147
+ var SHARED_SKILL_DIRECTORY = "_shared";
3148
+ var SKILLS_SOURCE_ROOT = join6("src", "skills");
3149
+ var MANIFEST_FILE_NAME = ".skill-manifest.json";
3150
+ function getManifestPath() {
3151
+ return join6(getCustomSkillsDir(), MANIFEST_FILE_NAME);
3152
+ }
3153
+ function listFilesRecursive(dirPath) {
3154
+ if (!existsSync7(dirPath)) {
3155
+ return [];
3130
3156
  }
3131
- return `${existing}${existing.endsWith("\n") || existing.length === 0 ? "" : "\n"}
3132
- ${managedBlock}`;
3157
+ const files = [];
3158
+ const entries = readdirSync2(dirPath).sort(
3159
+ (left, right) => left.localeCompare(right)
3160
+ );
3161
+ for (const entry of entries) {
3162
+ const entryPath = join6(dirPath, entry);
3163
+ const stat = statSync2(entryPath);
3164
+ if (stat.isDirectory()) {
3165
+ files.push(...listFilesRecursive(entryPath));
3166
+ continue;
3167
+ }
3168
+ files.push(entryPath);
3169
+ }
3170
+ return files;
3133
3171
  }
3134
- function writeTextWithBackup(path3, content) {
3135
- mkdirSync3(dirname4(path3), { recursive: true });
3136
- if (existsSync7(path3) && readFileSync6(path3, "utf8") === content) return false;
3137
- if (existsSync7(path3)) copyFileSync3(path3, `${path3}.bak`);
3138
- writeFileSync3(path3, content);
3139
- return true;
3172
+ function readPackageVersion(packageRoot) {
3173
+ const packageJsonPath = join6(packageRoot, "package.json");
3174
+ const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
3175
+ if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
3176
+ throw new Error(`Invalid package version in ${packageJsonPath}`);
3177
+ }
3178
+ return packageJson.version;
3140
3179
  }
3141
- function packageArtifactTarget(packageRoot, artifact) {
3142
- return join6(packageRoot, codexPluginRootArtifactPath(artifact.path));
3180
+ function readManifest() {
3181
+ const manifestPath = getManifestPath();
3182
+ if (!existsSync7(manifestPath)) {
3183
+ return null;
3184
+ }
3185
+ try {
3186
+ return JSON.parse(readFileSync6(manifestPath, "utf-8"));
3187
+ } catch (error) {
3188
+ console.warn(`Failed to read skill manifest: ${manifestPath}`, error);
3189
+ return null;
3190
+ }
3143
3191
  }
3144
- function normalizeRelativeMarketplacePath(path3) {
3145
- const normalized = path3.replaceAll("\\", "/");
3146
- if (isAbsolute(path3) || /^[A-Za-z]:\//.test(normalized)) return normalized;
3147
- if (normalized.startsWith("./")) return normalized;
3148
- return `./${normalized}`;
3192
+ function writeManifest(manifest) {
3193
+ const manifestPath = getManifestPath();
3194
+ mkdirSync3(getCustomSkillsDir(), { recursive: true });
3195
+ writeFileSync3(manifestPath, `${JSON.stringify(manifest, null, 2)}
3196
+ `);
3149
3197
  }
3150
- function marketplaceSourcePath(homeDir, personalPluginRoot) {
3151
- return normalizeRelativeMarketplacePath(
3152
- relative2(homeDir, personalPluginRoot)
3198
+ function computeSkillHash(skillDirPath) {
3199
+ const hash = createHash3("sha256");
3200
+ for (const filePath of listFilesRecursive(skillDirPath)) {
3201
+ hash.update(`${relative2(skillDirPath, filePath)}
3202
+ `);
3203
+ hash.update(readFileSync6(filePath));
3204
+ hash.update("\n");
3205
+ }
3206
+ return hash.digest("hex");
3207
+ }
3208
+ function findRemovedSkills(manifest) {
3209
+ const currentSkillNames = new Set(CUSTOM_SKILLS.map((skill) => skill.name));
3210
+ return Object.keys(manifest.skills).filter(
3211
+ (skillName) => !currentSkillNames.has(skillName)
3153
3212
  );
3154
3213
  }
3155
- function managedMarketplaceEntry(homeDir, personalPluginRoot) {
3214
+ function checkSkillsNeedUpdate(packageRoot) {
3215
+ const manifest = readManifest();
3216
+ const pluginVersion = readPackageVersion(packageRoot);
3217
+ const sharedHash = computeSkillHash(
3218
+ join6(packageRoot, SKILLS_SOURCE_ROOT, SHARED_SKILL_DIRECTORY)
3219
+ );
3220
+ const versionChanged = manifest !== null && manifest.pluginVersion !== pluginVersion;
3221
+ const sharedChanged = manifest !== null && manifest.sharedHash !== sharedHash;
3222
+ const removedSkills = manifest ? findRemovedSkills(manifest) : [];
3223
+ const skillsNeedingUpdate = CUSTOM_SKILLS.flatMap((skill) => {
3224
+ const sourcePath = join6(packageRoot, skill.sourcePath);
3225
+ const targetPath = join6(getCustomSkillsDir(), skill.name);
3226
+ const sourceHash = computeSkillHash(sourcePath);
3227
+ const manifestEntry = manifest?.skills[skill.name];
3228
+ const reasons = [];
3229
+ if (!manifest) {
3230
+ reasons.push("manifest-missing");
3231
+ }
3232
+ if (versionChanged) {
3233
+ reasons.push("version-change");
3234
+ }
3235
+ if (sharedChanged) {
3236
+ reasons.push("shared-hash-mismatch");
3237
+ }
3238
+ if (!manifestEntry) {
3239
+ reasons.push("new-skill");
3240
+ } else if (manifestEntry.hash !== sourceHash) {
3241
+ reasons.push("hash-mismatch");
3242
+ }
3243
+ if (!existsSync7(targetPath)) {
3244
+ reasons.push("missing-install");
3245
+ }
3246
+ if (reasons.length === 0) {
3247
+ return [];
3248
+ }
3249
+ return [
3250
+ {
3251
+ skill,
3252
+ sourceHash,
3253
+ targetPath,
3254
+ reasons
3255
+ }
3256
+ ];
3257
+ });
3156
3258
  return {
3157
- name: "thoth-agents",
3158
- source: {
3159
- source: "local",
3160
- path: marketplaceSourcePath(homeDir, personalPluginRoot)
3161
- },
3162
- policy: {
3163
- installation: "AVAILABLE",
3164
- authentication: "ON_INSTALL"
3165
- },
3166
- category: "Productivity"
3259
+ pluginVersion,
3260
+ sharedHash,
3261
+ manifest,
3262
+ versionChanged,
3263
+ sharedChanged,
3264
+ needsUpdate: skillsNeedingUpdate.length > 0 || removedSkills.length > 0,
3265
+ skillsNeedingUpdate,
3266
+ removedSkills
3167
3267
  };
3168
3268
  }
3169
- function stableJson3(value) {
3170
- return `${JSON.stringify(value, null, 2)}
3171
- `;
3172
- }
3173
- function emptyManagedModelState() {
3174
- return {
3175
- version: MANAGED_MODEL_STATE_VERSION,
3176
- models: {}
3177
- };
3269
+
3270
+ // src/cli/custom-skills.ts
3271
+ var SHARED_SKILL_DIRECTORY2 = "_shared";
3272
+ var SHARED_SKILL_SOURCE_PATH = `src/skills/${SHARED_SKILL_DIRECTORY2}`;
3273
+ var CUSTOM_SKILLS = BUNDLED_SKILL_REGISTRY.map(
3274
+ (skill) => ({
3275
+ name: skill.name,
3276
+ description: skill.description,
3277
+ allowedAgents: [...skill.allowedRoles],
3278
+ sourcePath: skill.sourcePath
3279
+ })
3280
+ );
3281
+ function getCustomSkillsDir() {
3282
+ return join7(getConfigDir(), "skills");
3178
3283
  }
3179
- function readManagedModelState(path3) {
3180
- if (!existsSync7(path3)) return emptyManagedModelState();
3181
- try {
3182
- const parsed = JSON.parse(readFileSync6(path3, "utf8"));
3183
- if (parsed.version !== MANAGED_MODEL_STATE_VERSION || !parsed.models || typeof parsed.models !== "object" || Array.isArray(parsed.models)) {
3184
- return emptyManagedModelState();
3284
+ function copyDirRecursive(src, dest) {
3285
+ if (!existsSync8(dest)) {
3286
+ mkdirSync4(dest, { recursive: true });
3287
+ }
3288
+ const entries = readdirSync3(src);
3289
+ for (const entry of entries) {
3290
+ const srcPath = join7(src, entry);
3291
+ const destPath = join7(dest, entry);
3292
+ const stat = statSync3(srcPath);
3293
+ if (stat.isDirectory()) {
3294
+ copyDirRecursive(srcPath, destPath);
3295
+ } else {
3296
+ const destDir = dirname4(destPath);
3297
+ if (!existsSync8(destDir)) {
3298
+ mkdirSync4(destDir, { recursive: true });
3299
+ }
3300
+ copyFileSync3(srcPath, destPath);
3185
3301
  }
3186
- return {
3187
- version: MANAGED_MODEL_STATE_VERSION,
3188
- models: Object.fromEntries(
3189
- Object.entries(parsed.models).filter(
3190
- (entry) => typeof entry[0] === "string" && typeof entry[1] === "string"
3191
- )
3192
- )
3193
- };
3194
- } catch {
3195
- return emptyManagedModelState();
3196
3302
  }
3197
3303
  }
3198
- function parseRoleTomlModel(content) {
3199
- const match = /^model\s*=\s*"((?:\\.|[^"\\])*)"\s*$/m.exec(content);
3200
- if (!match) return void 0;
3201
- return match[1].replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
3202
- }
3203
- function escapeTomlString2(value) {
3204
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\t/g, "\\t").replace(/\n/g, "\\n").replace(/\f/g, "\\f").replace(/\r/g, "\\r");
3304
+ function installSharedSkillAssets(packageRoot) {
3305
+ const sharedSourcePath = join7(packageRoot, SHARED_SKILL_SOURCE_PATH);
3306
+ const sharedTargetPath = join7(getCustomSkillsDir(), SHARED_SKILL_DIRECTORY2);
3307
+ if (!existsSync8(sharedSourcePath)) {
3308
+ console.error(`Custom skill shared assets not found: ${sharedSourcePath}`);
3309
+ return false;
3310
+ }
3311
+ rmSync(sharedTargetPath, { recursive: true, force: true });
3312
+ copyDirRecursive(sharedSourcePath, sharedTargetPath);
3313
+ return true;
3205
3314
  }
3206
- function replaceRoleTomlModel(content, model) {
3207
- const rendered = `model = "${escapeTomlString2(model)}"`;
3208
- if (/^model\s*=\s*"(?:\\.|[^"\\])*"\s*$/m.test(content)) {
3209
- return content.replace(/^model\s*=\s*"(?:\\.|[^"\\])*"\s*$/m, rendered);
3315
+ function findPackageRoot(startDir) {
3316
+ let currentDir = startDir;
3317
+ const filesystemRoot = parse(startDir).root;
3318
+ while (true) {
3319
+ if (existsSync8(join7(currentDir, "package.json")) && existsSync8(join7(currentDir, "src", "skills"))) {
3320
+ return currentDir;
3321
+ }
3322
+ if (currentDir === filesystemRoot) {
3323
+ return null;
3324
+ }
3325
+ const parentDir = dirname4(currentDir);
3326
+ if (parentDir === currentDir) {
3327
+ return null;
3328
+ }
3329
+ currentDir = parentDir;
3210
3330
  }
3211
- return `${rendered}
3212
- ${content}`;
3213
3331
  }
3214
- function roleManagedModelStateKey(path3) {
3215
- return basename(path3);
3332
+ function resolvePackageRoot(packageRoot) {
3333
+ if (packageRoot) {
3334
+ return packageRoot;
3335
+ }
3336
+ const moduleDir = fileURLToPath2(new URL(".", import.meta.url));
3337
+ return findPackageRoot(moduleDir) ?? fileURLToPath2(new URL("../..", import.meta.url));
3216
3338
  }
3217
- function resolveRoleTomlContent(options) {
3218
- const renderedModel = parseRoleTomlModel(options.renderedContent);
3219
- const key = roleManagedModelStateKey(options.targetPath);
3220
- if (!renderedModel) return options.renderedContent;
3221
- if (options.reset || !existsSync7(options.targetPath)) {
3222
- options.nextState.models[key] = renderedModel;
3223
- return options.renderedContent;
3339
+ function installCustomSkillFiles(skill, packageRoot) {
3340
+ const sourcePath = join7(packageRoot, skill.sourcePath);
3341
+ const targetPath = join7(getCustomSkillsDir(), skill.name);
3342
+ if (!existsSync8(sourcePath)) {
3343
+ console.error(`Custom skill source not found: ${sourcePath}`);
3344
+ return false;
3224
3345
  }
3225
- const currentModel = parseRoleTomlModel(
3226
- readFileSync6(options.targetPath, "utf8")
3227
- );
3228
- const trackedModel = options.state.models[key];
3229
- const isUserOwned = currentModel !== void 0 && (trackedModel === void 0 ? currentModel !== renderedModel : currentModel !== trackedModel);
3230
- if (isUserOwned) {
3346
+ rmSync(targetPath, { recursive: true, force: true });
3347
+ copyDirRecursive(sourcePath, targetPath);
3348
+ return true;
3349
+ }
3350
+ function removeObsoleteSkills(removedSkillNames) {
3351
+ const removedSkills = [];
3352
+ for (const skillName of removedSkillNames) {
3353
+ const targetPath = join7(getCustomSkillsDir(), skillName);
3354
+ try {
3355
+ console.log(`Removing obsolete bundled skill: ${skillName}`);
3356
+ rmSync(targetPath, { recursive: true, force: true });
3357
+ removedSkills.push(skillName);
3358
+ } catch (error) {
3359
+ console.warn(`Failed to remove obsolete bundled skill: ${skillName}`);
3360
+ console.warn(error);
3361
+ }
3362
+ }
3363
+ return removedSkills;
3364
+ }
3365
+ function buildManifest(packageRoot, updatedSkills, previousManifest, pluginVersion, sharedHash) {
3366
+ const installedAt = (/* @__PURE__ */ new Date()).toISOString();
3367
+ const updatedSkillNames = new Set(
3368
+ updatedSkills.map(({ skill }) => skill.name)
3369
+ );
3370
+ const skills = Object.fromEntries(
3371
+ CUSTOM_SKILLS.map((skill) => {
3372
+ const previousEntry = previousManifest?.skills[skill.name];
3373
+ const nextInstalledAt = updatedSkillNames.has(skill.name) ? installedAt : previousEntry?.installedAt ?? installedAt;
3374
+ return [
3375
+ skill.name,
3376
+ {
3377
+ hash: computeSkillHash(join7(packageRoot, skill.sourcePath)),
3378
+ installedAt: nextInstalledAt
3379
+ }
3380
+ ];
3381
+ })
3382
+ );
3383
+ return {
3384
+ pluginVersion,
3385
+ sharedHash,
3386
+ skills
3387
+ };
3388
+ }
3389
+ function pruneRemovedSkillsFromManifest(manifest, removedSkills) {
3390
+ const removedSkillNames = new Set(removedSkills);
3391
+ return {
3392
+ ...manifest,
3393
+ skills: Object.fromEntries(
3394
+ Object.entries(manifest.skills).filter(
3395
+ ([skillName]) => !removedSkillNames.has(skillName)
3396
+ )
3397
+ )
3398
+ };
3399
+ }
3400
+ function installCustomSkills(packageRoot = resolvePackageRoot()) {
3401
+ const updateCheck = checkSkillsNeedUpdate(packageRoot);
3402
+ if (!updateCheck.needsUpdate) {
3403
+ return {
3404
+ success: true,
3405
+ updatedSkills: [],
3406
+ skippedSkills: [...CUSTOM_SKILLS],
3407
+ failedSkills: [],
3408
+ removedSkills: []
3409
+ };
3410
+ }
3411
+ const removedSkills = removeObsoleteSkills(updateCheck.removedSkills);
3412
+ if (removedSkills.length > 0 && updateCheck.manifest) {
3413
+ writeManifest(
3414
+ pruneRemovedSkillsFromManifest(updateCheck.manifest, removedSkills)
3415
+ );
3416
+ }
3417
+ if (updateCheck.skillsNeedingUpdate.length === 0) {
3418
+ writeManifest(
3419
+ buildManifest(
3420
+ packageRoot,
3421
+ [],
3422
+ updateCheck.manifest,
3423
+ updateCheck.pluginVersion,
3424
+ updateCheck.sharedHash
3425
+ )
3426
+ );
3427
+ return {
3428
+ success: true,
3429
+ updatedSkills: [],
3430
+ skippedSkills: [...CUSTOM_SKILLS],
3431
+ failedSkills: [],
3432
+ removedSkills
3433
+ };
3434
+ }
3435
+ if (!installSharedSkillAssets(packageRoot)) {
3436
+ return {
3437
+ success: false,
3438
+ updatedSkills: [],
3439
+ skippedSkills: [],
3440
+ failedSkills: updateCheck.skillsNeedingUpdate.map(
3441
+ ({ skill, reasons }) => ({
3442
+ skill,
3443
+ reasons
3444
+ })
3445
+ ),
3446
+ removedSkills
3447
+ };
3448
+ }
3449
+ const updatesBySkillName = new Map(
3450
+ updateCheck.skillsNeedingUpdate.map((entry) => [entry.skill.name, entry])
3451
+ );
3452
+ const updatedSkills = [];
3453
+ const skippedSkills = [];
3454
+ const failedSkills = [];
3455
+ for (const skill of CUSTOM_SKILLS) {
3456
+ const pendingUpdate = updatesBySkillName.get(skill.name);
3457
+ if (!pendingUpdate) {
3458
+ skippedSkills.push(skill);
3459
+ continue;
3460
+ }
3461
+ if (installCustomSkillFiles(skill, packageRoot)) {
3462
+ updatedSkills.push({
3463
+ skill,
3464
+ reasons: pendingUpdate.reasons
3465
+ });
3466
+ continue;
3467
+ }
3468
+ failedSkills.push({
3469
+ skill,
3470
+ reasons: pendingUpdate.reasons
3471
+ });
3472
+ }
3473
+ if (failedSkills.length > 0) {
3474
+ return {
3475
+ success: false,
3476
+ updatedSkills,
3477
+ skippedSkills,
3478
+ failedSkills,
3479
+ removedSkills
3480
+ };
3481
+ }
3482
+ writeManifest(
3483
+ buildManifest(
3484
+ packageRoot,
3485
+ updatedSkills,
3486
+ updateCheck.manifest,
3487
+ updateCheck.pluginVersion,
3488
+ updateCheck.sharedHash
3489
+ )
3490
+ );
3491
+ return {
3492
+ success: true,
3493
+ updatedSkills,
3494
+ skippedSkills,
3495
+ failedSkills,
3496
+ removedSkills
3497
+ };
3498
+ }
3499
+
3500
+ // src/cli/codex-install.ts
3501
+ var ROOT_START = "<!-- thoth-agents:codex-root:start -->";
3502
+ var ROOT_END = "<!-- thoth-agents:codex-root:end -->";
3503
+ var MANAGED_MODEL_STATE_VERSION = 1;
3504
+ function mergeManagedBlock(existing, managedBlock) {
3505
+ const start = existing.indexOf(ROOT_START);
3506
+ const end = existing.indexOf(ROOT_END);
3507
+ if (start !== -1 && end !== -1 && end > start) {
3508
+ return `${existing.slice(0, start)}${managedBlock}${existing.slice(end + ROOT_END.length).replace(/^\s*\n/, "")}`;
3509
+ }
3510
+ return `${existing}${existing.endsWith("\n") || existing.length === 0 ? "" : "\n"}
3511
+ ${managedBlock}`;
3512
+ }
3513
+ function writeTextWithBackup(path3, content) {
3514
+ mkdirSync5(dirname5(path3), { recursive: true });
3515
+ if (existsSync9(path3) && readFileSync7(path3, "utf8") === content) return false;
3516
+ if (existsSync9(path3)) copyFileSync4(path3, `${path3}.bak`);
3517
+ writeFileSync4(path3, content);
3518
+ return true;
3519
+ }
3520
+ function packageArtifactTarget(packageRoot, artifact) {
3521
+ return join8(packageRoot, codexPluginRootArtifactPath(artifact.path));
3522
+ }
3523
+ function resolvePackageRoot2(packageRoot) {
3524
+ if (packageRoot) return packageRoot;
3525
+ return findPackageRoot(fileURLToPath3(new URL(".", import.meta.url))) ?? void 0;
3526
+ }
3527
+ function normalizeRelativeMarketplacePath(path3) {
3528
+ const normalized = path3.replaceAll("\\", "/");
3529
+ if (isAbsolute(path3) || /^[A-Za-z]:\//.test(normalized)) return normalized;
3530
+ if (normalized.startsWith("./")) return normalized;
3531
+ return `./${normalized}`;
3532
+ }
3533
+ function marketplaceSourcePath(homeDir, personalPluginRoot) {
3534
+ return normalizeRelativeMarketplacePath(
3535
+ relative3(homeDir, personalPluginRoot)
3536
+ );
3537
+ }
3538
+ function managedMarketplaceEntry(homeDir, personalPluginRoot) {
3539
+ return {
3540
+ name: "thoth-agents",
3541
+ source: {
3542
+ source: "local",
3543
+ path: marketplaceSourcePath(homeDir, personalPluginRoot)
3544
+ },
3545
+ policy: {
3546
+ installation: "AVAILABLE",
3547
+ authentication: "ON_INSTALL"
3548
+ },
3549
+ category: "Productivity"
3550
+ };
3551
+ }
3552
+ function stableJson3(value) {
3553
+ return `${JSON.stringify(value, null, 2)}
3554
+ `;
3555
+ }
3556
+ function emptyManagedModelState() {
3557
+ return {
3558
+ version: MANAGED_MODEL_STATE_VERSION,
3559
+ models: {}
3560
+ };
3561
+ }
3562
+ function readManagedModelState(path3) {
3563
+ if (!existsSync9(path3)) return emptyManagedModelState();
3564
+ try {
3565
+ const parsed = JSON.parse(readFileSync7(path3, "utf8"));
3566
+ if (parsed.version !== MANAGED_MODEL_STATE_VERSION || !parsed.models || typeof parsed.models !== "object" || Array.isArray(parsed.models)) {
3567
+ return emptyManagedModelState();
3568
+ }
3569
+ return {
3570
+ version: MANAGED_MODEL_STATE_VERSION,
3571
+ models: Object.fromEntries(
3572
+ Object.entries(parsed.models).filter(
3573
+ (entry) => typeof entry[0] === "string" && typeof entry[1] === "string"
3574
+ )
3575
+ )
3576
+ };
3577
+ } catch {
3578
+ return emptyManagedModelState();
3579
+ }
3580
+ }
3581
+ function parseRoleTomlModel(content) {
3582
+ const match = /^model\s*=\s*"((?:\\.|[^"\\])*)"\s*$/m.exec(content);
3583
+ if (!match) return void 0;
3584
+ return match[1].replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
3585
+ }
3586
+ function escapeTomlString2(value) {
3587
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\t/g, "\\t").replace(/\n/g, "\\n").replace(/\f/g, "\\f").replace(/\r/g, "\\r");
3588
+ }
3589
+ function replaceRoleTomlModel(content, model) {
3590
+ const rendered = `model = "${escapeTomlString2(model)}"`;
3591
+ if (/^model\s*=\s*"(?:\\.|[^"\\])*"\s*$/m.test(content)) {
3592
+ return content.replace(/^model\s*=\s*"(?:\\.|[^"\\])*"\s*$/m, rendered);
3593
+ }
3594
+ return `${rendered}
3595
+ ${content}`;
3596
+ }
3597
+ function roleManagedModelStateKey(path3) {
3598
+ return basename(path3);
3599
+ }
3600
+ function resolveRoleTomlContent(options) {
3601
+ const renderedModel = parseRoleTomlModel(options.renderedContent);
3602
+ const key = roleManagedModelStateKey(options.targetPath);
3603
+ if (!renderedModel) return options.renderedContent;
3604
+ if (options.reset || !existsSync9(options.targetPath)) {
3605
+ options.nextState.models[key] = renderedModel;
3606
+ return options.renderedContent;
3607
+ }
3608
+ const currentModel = parseRoleTomlModel(
3609
+ readFileSync7(options.targetPath, "utf8")
3610
+ );
3611
+ const trackedModel = options.state.models[key];
3612
+ const isUserOwned = currentModel !== void 0 && (trackedModel === void 0 ? currentModel !== renderedModel : currentModel !== trackedModel);
3613
+ if (isUserOwned) {
3231
3614
  if (trackedModel !== void 0)
3232
3615
  options.nextState.models[key] = trackedModel;
3233
3616
  return replaceRoleTomlModel(options.renderedContent, currentModel);
@@ -3263,7 +3646,11 @@ function buildCodexSetupPlan(config) {
3263
3646
  homeDir: config.homeDir,
3264
3647
  codexHome: config.codexHome
3265
3648
  });
3266
- const render = codexAdapter.render({ projectRoot: config.projectRoot });
3649
+ const packageRoot = resolvePackageRoot2(config.packageRoot);
3650
+ const render = codexAdapter.render({
3651
+ projectRoot: config.projectRoot,
3652
+ ...packageRoot ? { packageRoot } : {}
3653
+ });
3267
3654
  const packageArtifacts = render.artifacts.filter(
3268
3655
  (artifact) => artifact.path.startsWith(".codex-plugin/")
3269
3656
  );
@@ -3285,7 +3672,7 @@ function buildCodexSetupPlan(config) {
3285
3672
  action: "write-role-toml",
3286
3673
  targetPath: target.path,
3287
3674
  description: `Materialize Codex role subagent ${target.role}.`,
3288
- requiresBackup: existsSync7(target.path),
3675
+ requiresBackup: existsSync9(target.path),
3289
3676
  role: target.role,
3290
3677
  content: resolveRoleTomlContent({
3291
3678
  renderedContent: roleArtifactContent(target.role, render.artifacts),
@@ -3301,7 +3688,7 @@ function buildCodexSetupPlan(config) {
3301
3688
  action: "write-managed-model-state",
3302
3689
  targetPath: targets.managedModelsPath,
3303
3690
  description: "Record thoth-agents-managed Codex role model ownership state.",
3304
- requiresBackup: existsSync7(targets.managedModelsPath),
3691
+ requiresBackup: existsSync9(targets.managedModelsPath),
3305
3692
  content: stableJson3(nextManagedModelState)
3306
3693
  },
3307
3694
  ...packageArtifacts.map(
@@ -3319,7 +3706,7 @@ function buildCodexSetupPlan(config) {
3319
3706
  action: "merge-marketplace",
3320
3707
  targetPath: targets.personalMarketplacePath,
3321
3708
  description: "Register Personal Codex marketplace entry for the local thoth-agents plugin source.",
3322
- requiresBackup: existsSync7(targets.personalMarketplacePath),
3709
+ requiresBackup: existsSync9(targets.personalMarketplacePath),
3323
3710
  content: targets.personalPluginRoot
3324
3711
  },
3325
3712
  {
@@ -3383,10 +3770,10 @@ function formatRefreshPackageGroup(kind, groups) {
3383
3770
  }
3384
3771
  function commonTargetDirectory(items) {
3385
3772
  if (items.length === 0) return "";
3386
- let common = dirname4(items[0]?.targetPath ?? "");
3773
+ let common = dirname5(items[0]?.targetPath ?? "");
3387
3774
  for (const item of items.slice(1)) {
3388
3775
  while (!isSameOrChildPath(item.targetPath, common)) {
3389
- const parent = dirname4(common);
3776
+ const parent = dirname5(common);
3390
3777
  if (parent === common) return common;
3391
3778
  common = parent;
3392
3779
  }
@@ -3408,7 +3795,7 @@ function applyCodexSetup(plan) {
3408
3795
  if (plan.dryRun) return { success: true, changed, diagnostics };
3409
3796
  try {
3410
3797
  for (const targetPath of managedRefreshRoots(plan)) {
3411
- rmSync(targetPath, { recursive: true, force: true });
3798
+ rmSync2(targetPath, { recursive: true, force: true });
3412
3799
  }
3413
3800
  for (const item of plan.items) {
3414
3801
  if (item.action === "diagnose-only") continue;
@@ -3426,8 +3813,8 @@ function applyCodexSetup(plan) {
3426
3813
  if (item.action === "merge-marketplace") {
3427
3814
  if (item.content === void 0) continue;
3428
3815
  const content2 = mergePersonalMarketplace(
3429
- existsSync7(item.targetPath) ? readFileSync6(item.targetPath, "utf8") : "",
3430
- dirname4(dirname4(dirname4(item.targetPath))),
3816
+ existsSync9(item.targetPath) ? readFileSync7(item.targetPath, "utf8") : "",
3817
+ dirname5(dirname5(dirname5(item.targetPath))),
3431
3818
  item.content
3432
3819
  );
3433
3820
  if (writeTextWithBackup(item.targetPath, content2))
@@ -3436,7 +3823,7 @@ function applyCodexSetup(plan) {
3436
3823
  }
3437
3824
  if (item.content === void 0) continue;
3438
3825
  const content = item.action === "merge-managed-block" ? mergeManagedBlock(
3439
- existsSync7(item.targetPath) ? readFileSync6(item.targetPath, "utf8") : "",
3826
+ existsSync9(item.targetPath) ? readFileSync7(item.targetPath, "utf8") : "",
3440
3827
  item.content
3441
3828
  ) : item.content;
3442
3829
  if (writeTextWithBackup(item.targetPath, content))
@@ -3464,7 +3851,7 @@ function managedRefreshRoots(plan) {
3464
3851
  }
3465
3852
 
3466
3853
  // src/cli/system.ts
3467
- import { statSync as statSync2 } from "fs";
3854
+ import { statSync as statSync4 } from "fs";
3468
3855
 
3469
3856
  // src/utils/subprocess.ts
3470
3857
  import {
@@ -3579,7 +3966,7 @@ function resolveOpenCodePath() {
3579
3966
  for (const opencodePath of paths) {
3580
3967
  if (opencodePath === "opencode") continue;
3581
3968
  try {
3582
- const stat = statSync2(opencodePath);
3969
+ const stat = statSync4(opencodePath);
3583
3970
  if (stat.isFile()) {
3584
3971
  cachedOpenCodePath = opencodePath;
3585
3972
  return opencodePath;
@@ -3628,382 +4015,6 @@ function getOpenCodePath() {
3628
4015
  return path3 === "opencode" ? null : path3;
3629
4016
  }
3630
4017
 
3631
- // src/cli/custom-skills.ts
3632
- import {
3633
- copyFileSync as copyFileSync4,
3634
- existsSync as existsSync9,
3635
- mkdirSync as mkdirSync5,
3636
- readdirSync as readdirSync3,
3637
- rmSync as rmSync2,
3638
- statSync as statSync4
3639
- } from "fs";
3640
- import { dirname as dirname5, join as join8, parse } from "path";
3641
- import { fileURLToPath as fileURLToPath2 } from "url";
3642
-
3643
- // src/cli/skill-manifest.ts
3644
- import { createHash as createHash3 } from "crypto";
3645
- import {
3646
- existsSync as existsSync8,
3647
- mkdirSync as mkdirSync4,
3648
- readdirSync as readdirSync2,
3649
- readFileSync as readFileSync7,
3650
- statSync as statSync3,
3651
- writeFileSync as writeFileSync4
3652
- } from "fs";
3653
- import { join as join7, relative as relative3 } from "path";
3654
- var SHARED_SKILL_DIRECTORY = "_shared";
3655
- var SKILLS_SOURCE_ROOT = join7("src", "skills");
3656
- var MANIFEST_FILE_NAME = ".skill-manifest.json";
3657
- function getManifestPath() {
3658
- return join7(getCustomSkillsDir(), MANIFEST_FILE_NAME);
3659
- }
3660
- function listFilesRecursive(dirPath) {
3661
- if (!existsSync8(dirPath)) {
3662
- return [];
3663
- }
3664
- const files = [];
3665
- const entries = readdirSync2(dirPath).sort(
3666
- (left, right) => left.localeCompare(right)
3667
- );
3668
- for (const entry of entries) {
3669
- const entryPath = join7(dirPath, entry);
3670
- const stat = statSync3(entryPath);
3671
- if (stat.isDirectory()) {
3672
- files.push(...listFilesRecursive(entryPath));
3673
- continue;
3674
- }
3675
- files.push(entryPath);
3676
- }
3677
- return files;
3678
- }
3679
- function readPackageVersion(packageRoot) {
3680
- const packageJsonPath = join7(packageRoot, "package.json");
3681
- const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
3682
- if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
3683
- throw new Error(`Invalid package version in ${packageJsonPath}`);
3684
- }
3685
- return packageJson.version;
3686
- }
3687
- function readManifest() {
3688
- const manifestPath = getManifestPath();
3689
- if (!existsSync8(manifestPath)) {
3690
- return null;
3691
- }
3692
- try {
3693
- return JSON.parse(readFileSync7(manifestPath, "utf-8"));
3694
- } catch (error) {
3695
- console.warn(`Failed to read skill manifest: ${manifestPath}`, error);
3696
- return null;
3697
- }
3698
- }
3699
- function writeManifest(manifest) {
3700
- const manifestPath = getManifestPath();
3701
- mkdirSync4(getCustomSkillsDir(), { recursive: true });
3702
- writeFileSync4(manifestPath, `${JSON.stringify(manifest, null, 2)}
3703
- `);
3704
- }
3705
- function computeSkillHash(skillDirPath) {
3706
- const hash = createHash3("sha256");
3707
- for (const filePath of listFilesRecursive(skillDirPath)) {
3708
- hash.update(`${relative3(skillDirPath, filePath)}
3709
- `);
3710
- hash.update(readFileSync7(filePath));
3711
- hash.update("\n");
3712
- }
3713
- return hash.digest("hex");
3714
- }
3715
- function findRemovedSkills(manifest) {
3716
- const currentSkillNames = new Set(CUSTOM_SKILLS.map((skill) => skill.name));
3717
- return Object.keys(manifest.skills).filter(
3718
- (skillName) => !currentSkillNames.has(skillName)
3719
- );
3720
- }
3721
- function checkSkillsNeedUpdate(packageRoot) {
3722
- const manifest = readManifest();
3723
- const pluginVersion = readPackageVersion(packageRoot);
3724
- const sharedHash = computeSkillHash(
3725
- join7(packageRoot, SKILLS_SOURCE_ROOT, SHARED_SKILL_DIRECTORY)
3726
- );
3727
- const versionChanged = manifest !== null && manifest.pluginVersion !== pluginVersion;
3728
- const sharedChanged = manifest !== null && manifest.sharedHash !== sharedHash;
3729
- const removedSkills = manifest ? findRemovedSkills(manifest) : [];
3730
- const skillsNeedingUpdate = CUSTOM_SKILLS.flatMap((skill) => {
3731
- const sourcePath = join7(packageRoot, skill.sourcePath);
3732
- const targetPath = join7(getCustomSkillsDir(), skill.name);
3733
- const sourceHash = computeSkillHash(sourcePath);
3734
- const manifestEntry = manifest?.skills[skill.name];
3735
- const reasons = [];
3736
- if (!manifest) {
3737
- reasons.push("manifest-missing");
3738
- }
3739
- if (versionChanged) {
3740
- reasons.push("version-change");
3741
- }
3742
- if (sharedChanged) {
3743
- reasons.push("shared-hash-mismatch");
3744
- }
3745
- if (!manifestEntry) {
3746
- reasons.push("new-skill");
3747
- } else if (manifestEntry.hash !== sourceHash) {
3748
- reasons.push("hash-mismatch");
3749
- }
3750
- if (!existsSync8(targetPath)) {
3751
- reasons.push("missing-install");
3752
- }
3753
- if (reasons.length === 0) {
3754
- return [];
3755
- }
3756
- return [
3757
- {
3758
- skill,
3759
- sourceHash,
3760
- targetPath,
3761
- reasons
3762
- }
3763
- ];
3764
- });
3765
- return {
3766
- pluginVersion,
3767
- sharedHash,
3768
- manifest,
3769
- versionChanged,
3770
- sharedChanged,
3771
- needsUpdate: skillsNeedingUpdate.length > 0 || removedSkills.length > 0,
3772
- skillsNeedingUpdate,
3773
- removedSkills
3774
- };
3775
- }
3776
-
3777
- // src/cli/custom-skills.ts
3778
- var SHARED_SKILL_DIRECTORY2 = "_shared";
3779
- var SHARED_SKILL_SOURCE_PATH = `src/skills/${SHARED_SKILL_DIRECTORY2}`;
3780
- var CUSTOM_SKILLS = BUNDLED_SKILL_REGISTRY.map(
3781
- (skill) => ({
3782
- name: skill.name,
3783
- description: skill.description,
3784
- allowedAgents: [...skill.allowedRoles],
3785
- sourcePath: skill.sourcePath
3786
- })
3787
- );
3788
- function getCustomSkillsDir() {
3789
- return join8(getConfigDir(), "skills");
3790
- }
3791
- function copyDirRecursive(src, dest) {
3792
- if (!existsSync9(dest)) {
3793
- mkdirSync5(dest, { recursive: true });
3794
- }
3795
- const entries = readdirSync3(src);
3796
- for (const entry of entries) {
3797
- const srcPath = join8(src, entry);
3798
- const destPath = join8(dest, entry);
3799
- const stat = statSync4(srcPath);
3800
- if (stat.isDirectory()) {
3801
- copyDirRecursive(srcPath, destPath);
3802
- } else {
3803
- const destDir = dirname5(destPath);
3804
- if (!existsSync9(destDir)) {
3805
- mkdirSync5(destDir, { recursive: true });
3806
- }
3807
- copyFileSync4(srcPath, destPath);
3808
- }
3809
- }
3810
- }
3811
- function installSharedSkillAssets(packageRoot) {
3812
- const sharedSourcePath = join8(packageRoot, SHARED_SKILL_SOURCE_PATH);
3813
- const sharedTargetPath = join8(getCustomSkillsDir(), SHARED_SKILL_DIRECTORY2);
3814
- if (!existsSync9(sharedSourcePath)) {
3815
- console.error(`Custom skill shared assets not found: ${sharedSourcePath}`);
3816
- return false;
3817
- }
3818
- rmSync2(sharedTargetPath, { recursive: true, force: true });
3819
- copyDirRecursive(sharedSourcePath, sharedTargetPath);
3820
- return true;
3821
- }
3822
- function findPackageRoot(startDir) {
3823
- let currentDir = startDir;
3824
- const filesystemRoot = parse(startDir).root;
3825
- while (true) {
3826
- if (existsSync9(join8(currentDir, "package.json")) && existsSync9(join8(currentDir, "src", "skills"))) {
3827
- return currentDir;
3828
- }
3829
- if (currentDir === filesystemRoot) {
3830
- return null;
3831
- }
3832
- const parentDir = dirname5(currentDir);
3833
- if (parentDir === currentDir) {
3834
- return null;
3835
- }
3836
- currentDir = parentDir;
3837
- }
3838
- }
3839
- function resolvePackageRoot(packageRoot) {
3840
- if (packageRoot) {
3841
- return packageRoot;
3842
- }
3843
- const moduleDir = fileURLToPath2(new URL(".", import.meta.url));
3844
- return findPackageRoot(moduleDir) ?? fileURLToPath2(new URL("../..", import.meta.url));
3845
- }
3846
- function installCustomSkillFiles(skill, packageRoot) {
3847
- const sourcePath = join8(packageRoot, skill.sourcePath);
3848
- const targetPath = join8(getCustomSkillsDir(), skill.name);
3849
- if (!existsSync9(sourcePath)) {
3850
- console.error(`Custom skill source not found: ${sourcePath}`);
3851
- return false;
3852
- }
3853
- rmSync2(targetPath, { recursive: true, force: true });
3854
- copyDirRecursive(sourcePath, targetPath);
3855
- return true;
3856
- }
3857
- function removeObsoleteSkills(removedSkillNames) {
3858
- const removedSkills = [];
3859
- for (const skillName of removedSkillNames) {
3860
- const targetPath = join8(getCustomSkillsDir(), skillName);
3861
- try {
3862
- console.log(`Removing obsolete bundled skill: ${skillName}`);
3863
- rmSync2(targetPath, { recursive: true, force: true });
3864
- removedSkills.push(skillName);
3865
- } catch (error) {
3866
- console.warn(`Failed to remove obsolete bundled skill: ${skillName}`);
3867
- console.warn(error);
3868
- }
3869
- }
3870
- return removedSkills;
3871
- }
3872
- function buildManifest(packageRoot, updatedSkills, previousManifest, pluginVersion, sharedHash) {
3873
- const installedAt = (/* @__PURE__ */ new Date()).toISOString();
3874
- const updatedSkillNames = new Set(
3875
- updatedSkills.map(({ skill }) => skill.name)
3876
- );
3877
- const skills = Object.fromEntries(
3878
- CUSTOM_SKILLS.map((skill) => {
3879
- const previousEntry = previousManifest?.skills[skill.name];
3880
- const nextInstalledAt = updatedSkillNames.has(skill.name) ? installedAt : previousEntry?.installedAt ?? installedAt;
3881
- return [
3882
- skill.name,
3883
- {
3884
- hash: computeSkillHash(join8(packageRoot, skill.sourcePath)),
3885
- installedAt: nextInstalledAt
3886
- }
3887
- ];
3888
- })
3889
- );
3890
- return {
3891
- pluginVersion,
3892
- sharedHash,
3893
- skills
3894
- };
3895
- }
3896
- function pruneRemovedSkillsFromManifest(manifest, removedSkills) {
3897
- const removedSkillNames = new Set(removedSkills);
3898
- return {
3899
- ...manifest,
3900
- skills: Object.fromEntries(
3901
- Object.entries(manifest.skills).filter(
3902
- ([skillName]) => !removedSkillNames.has(skillName)
3903
- )
3904
- )
3905
- };
3906
- }
3907
- function installCustomSkills(packageRoot = resolvePackageRoot()) {
3908
- const updateCheck = checkSkillsNeedUpdate(packageRoot);
3909
- if (!updateCheck.needsUpdate) {
3910
- return {
3911
- success: true,
3912
- updatedSkills: [],
3913
- skippedSkills: [...CUSTOM_SKILLS],
3914
- failedSkills: [],
3915
- removedSkills: []
3916
- };
3917
- }
3918
- const removedSkills = removeObsoleteSkills(updateCheck.removedSkills);
3919
- if (removedSkills.length > 0 && updateCheck.manifest) {
3920
- writeManifest(
3921
- pruneRemovedSkillsFromManifest(updateCheck.manifest, removedSkills)
3922
- );
3923
- }
3924
- if (updateCheck.skillsNeedingUpdate.length === 0) {
3925
- writeManifest(
3926
- buildManifest(
3927
- packageRoot,
3928
- [],
3929
- updateCheck.manifest,
3930
- updateCheck.pluginVersion,
3931
- updateCheck.sharedHash
3932
- )
3933
- );
3934
- return {
3935
- success: true,
3936
- updatedSkills: [],
3937
- skippedSkills: [...CUSTOM_SKILLS],
3938
- failedSkills: [],
3939
- removedSkills
3940
- };
3941
- }
3942
- if (!installSharedSkillAssets(packageRoot)) {
3943
- return {
3944
- success: false,
3945
- updatedSkills: [],
3946
- skippedSkills: [],
3947
- failedSkills: updateCheck.skillsNeedingUpdate.map(
3948
- ({ skill, reasons }) => ({
3949
- skill,
3950
- reasons
3951
- })
3952
- ),
3953
- removedSkills
3954
- };
3955
- }
3956
- const updatesBySkillName = new Map(
3957
- updateCheck.skillsNeedingUpdate.map((entry) => [entry.skill.name, entry])
3958
- );
3959
- const updatedSkills = [];
3960
- const skippedSkills = [];
3961
- const failedSkills = [];
3962
- for (const skill of CUSTOM_SKILLS) {
3963
- const pendingUpdate = updatesBySkillName.get(skill.name);
3964
- if (!pendingUpdate) {
3965
- skippedSkills.push(skill);
3966
- continue;
3967
- }
3968
- if (installCustomSkillFiles(skill, packageRoot)) {
3969
- updatedSkills.push({
3970
- skill,
3971
- reasons: pendingUpdate.reasons
3972
- });
3973
- continue;
3974
- }
3975
- failedSkills.push({
3976
- skill,
3977
- reasons: pendingUpdate.reasons
3978
- });
3979
- }
3980
- if (failedSkills.length > 0) {
3981
- return {
3982
- success: false,
3983
- updatedSkills,
3984
- skippedSkills,
3985
- failedSkills,
3986
- removedSkills
3987
- };
3988
- }
3989
- writeManifest(
3990
- buildManifest(
3991
- packageRoot,
3992
- updatedSkills,
3993
- updateCheck.manifest,
3994
- updateCheck.pluginVersion,
3995
- updateCheck.sharedHash
3996
- )
3997
- );
3998
- return {
3999
- success: true,
4000
- updatedSkills,
4001
- skippedSkills,
4002
- failedSkills,
4003
- removedSkills
4004
- };
4005
- }
4006
-
4007
4018
  // src/cli/skills.ts
4008
4019
  import { spawnSync } from "child_process";
4009
4020
  var RECOMMENDED_SKILLS = [