@starlein/paperclip-plugin-company-wizard 0.6.3 → 0.6.4

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/worker.js CHANGED
@@ -29352,7 +29352,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
29352
29352
  var manifest = {
29353
29353
  id: "starlein.paperclip-plugin-company-wizard",
29354
29354
  apiVersion: 1,
29355
- version: "0.6.3",
29355
+ version: "0.6.4",
29356
29356
  displayName: "Company Wizard",
29357
29357
  description: "AI-powered wizard to bootstrap agent companies from composable templates",
29358
29358
  author: "Sascha Pietrowski <sp@speednetwork.de>",
@@ -29388,7 +29388,7 @@ var manifest = {
29388
29388
  },
29389
29389
  templatesPath: {
29390
29390
  type: "string",
29391
- description: "Optional existing template root containing roles/, modules/, and presets/. Overrides the GitHub URL. Leave empty to use bundled release templates or a custom GitHub source. Refresh never populates or overwrites this directory."
29391
+ description: "Optional existing template root containing roles/, modules/, and presets/. Overrides the GitHub URL. Leave empty to use bundled release templates or a custom GitHub source. Refresh never overwrites this directory. For an existing empty directory, the wizard offers an explicit sync from the configured template URL."
29392
29392
  },
29393
29393
  templatesRepoUrl: {
29394
29394
  type: "string",
@@ -29562,6 +29562,16 @@ function humanizeSkillName(baseName, variant) {
29562
29562
  function hashSkillContent(markdown) {
29563
29563
  return createHash("sha256").update(String(markdown), "utf-8").digest("hex").slice(0, 16);
29564
29564
  }
29565
+ function ensureSkillFrontmatter(slug, description, markdown) {
29566
+ const content = String(markdown);
29567
+ if (/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/.test(content)) return content;
29568
+ return `---
29569
+ name: ${slug}
29570
+ description: ${JSON.stringify(String(description || slug))}
29571
+ ---
29572
+
29573
+ ${content}`;
29574
+ }
29565
29575
  function buildCompanySkillSet(records) {
29566
29576
  const byBaseSlug = /* @__PURE__ */ new Map();
29567
29577
  for (const rec of records) {
@@ -29585,13 +29595,14 @@ function buildCompanySkillSet(records) {
29585
29595
  const groups = [...byHash.values()].map((g) => ({ ...g, repRole: [...g.roles].sort()[0] })).sort((a, b) => a.repRole.localeCompare(b.repRole));
29586
29596
  groups.forEach((group, idx) => {
29587
29597
  const slug = idx === 0 ? baseSlug : `${baseSlug}-${group.repRole}`;
29598
+ const markdown = ensureSkillFrontmatter(slug, group.rec.description, group.rec.markdown);
29588
29599
  companySkills.push({
29589
29600
  slug,
29590
29601
  name: group.rec.name,
29591
29602
  description: group.rec.description,
29592
29603
  categories: group.rec.categories,
29593
- markdown: group.rec.markdown,
29594
- contentHash: group.hash
29604
+ markdown,
29605
+ contentHash: hashSkillContent(markdown)
29595
29606
  });
29596
29607
  for (const roleName of group.roles) addRoleSlug(roleName, slug);
29597
29608
  });
@@ -31627,19 +31638,25 @@ function copyDirSync(src, dest) {
31627
31638
  const s = path2.join(src, entry.name);
31628
31639
  const d = path2.join(dest, entry.name);
31629
31640
  if (entry.isDirectory()) copyDirSync(s, d);
31630
- else fs2.copyFileSync(s, d);
31641
+ else if (entry.isFile()) fs2.copyFileSync(s, d);
31642
+ else throw new Error("Template downloads must contain only regular files and directories.");
31631
31643
  }
31632
31644
  }
31633
31645
  function downloadTemplatesFromGithub(destDir, githubUrl) {
31634
- const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
31646
+ const match = githubUrl.match(
31647
+ /^https:\/\/github\.com\/([\w-]+)\/([\w.-]+)\/tree\/([\w.-]+)\/([\w./-]+)$/
31648
+ );
31635
31649
  if (!match) {
31636
31650
  throw new Error(
31637
- `Unsupported templates URL: ${githubUrl}. Expected https://github.com/{owner}/{repo}/tree/{branch}/{path}`
31651
+ "Unsupported templates URL. Expected https://github.com/{owner}/{repo}/tree/{branch}/{path} (no credentials or query string)."
31638
31652
  );
31639
31653
  }
31640
31654
  const [, owner, repo, branch, subpath] = match;
31655
+ if (subpath.split("/").some((part) => !part || part === "." || part === ".." || part.startsWith("-"))) {
31656
+ throw new Error("Unsupported templates URL path.");
31657
+ }
31641
31658
  const cloneUrl = `https://github.com/${owner}/${repo}.git`;
31642
- const tmpDir = path2.join(os.tmpdir(), `plugin-templates-dl-${Date.now()}`);
31659
+ const tmpDir = fs2.mkdtempSync(path2.join(os.tmpdir(), "plugin-templates-dl-"));
31643
31660
  try {
31644
31661
  execFileSync(
31645
31662
  "git",
@@ -31658,6 +31675,9 @@ function downloadTemplatesFromGithub(destDir, githubUrl) {
31658
31675
  );
31659
31676
  execFileSync("git", ["sparse-checkout", "set", subpath], { cwd: tmpDir, stdio: "pipe" });
31660
31677
  const srcDir = path2.join(tmpDir, subpath);
31678
+ if (fs2.existsSync(srcDir) && (!fs2.lstatSync(srcDir).isDirectory() || !fs2.realpathSync(srcDir).startsWith(`${tmpDir}${path2.sep}`))) {
31679
+ throw new Error("Template source must be a directory inside the downloaded repository.");
31680
+ }
31661
31681
  if (!fs2.existsSync(srcDir)) throw new Error(`Path '${subpath}' not found in ${cloneUrl}`);
31662
31682
  copyDirSync(srcDir, destDir);
31663
31683
  } finally {
@@ -31724,6 +31744,48 @@ function refreshTemplatesCache(cfg, log) {
31724
31744
  log?.(`\u2713 Refreshed templates cache from ${repoUrl} \u2192 ${targetDir}`);
31725
31745
  return targetDir;
31726
31746
  }
31747
+ function emptyTemplateSyncOffer(cfg) {
31748
+ try {
31749
+ if (!cfg.templatesPath) return void 0;
31750
+ const stat = fs2.lstatSync(cfg.templatesPath);
31751
+ if (!stat.isDirectory() || fs2.readdirSync(cfg.templatesPath).length) return void 0;
31752
+ const targetDir = fs2.realpathSync(cfg.templatesPath);
31753
+ const token = createHash2("sha256").update(
31754
+ JSON.stringify([
31755
+ cfg.templatesPath,
31756
+ targetDir,
31757
+ cfg.templatesRepoUrl || "",
31758
+ stat.dev,
31759
+ stat.ino
31760
+ ])
31761
+ ).digest("hex");
31762
+ return { targetDir, token, canSync: !!cfg.templatesRepoUrl?.trim() };
31763
+ } catch {
31764
+ return void 0;
31765
+ }
31766
+ }
31767
+ function syncEmptyTemplates(cfg, confirmation) {
31768
+ const offer = emptyTemplateSyncOffer(cfg);
31769
+ if (!offer?.canSync || confirmation !== offer.token) {
31770
+ throw new Error(
31771
+ "Sync requires confirmation for the current empty templatesPath and a configured templatesRepoUrl. Reload the page and check settings."
31772
+ );
31773
+ }
31774
+ const stagingDir = fs2.mkdtempSync(`${offer.targetDir}-sync-`);
31775
+ try {
31776
+ downloadTemplatesFromGithub(stagingDir, cfg.templatesRepoUrl);
31777
+ validateTemplatesDir(stagingDir, "Downloaded templates");
31778
+ const { loadErrors } = loadTemplates(stagingDir);
31779
+ if (loadErrors.length) throw new Error("Downloaded template metadata is invalid.");
31780
+ if (emptyTemplateSyncOffer(cfg)?.token !== offer.token) {
31781
+ throw new Error("Template directory changed during sync. No existing files were replaced.");
31782
+ }
31783
+ fs2.renameSync(stagingDir, offer.targetDir);
31784
+ return offer.targetDir;
31785
+ } finally {
31786
+ fs2.rmSync(stagingDir, { recursive: true, force: true });
31787
+ }
31788
+ }
31727
31789
  var ANTHROPIC_AI_MODEL = "claude-opus-5";
31728
31790
  var OPENAI_AI_MODEL = "gpt-6-astra";
31729
31791
  function isLikelyAnthropicApiKey(value) {
@@ -32102,13 +32164,14 @@ async function provisionCompanySkills(client, companyId, companySkills, log) {
32102
32164
  }
32103
32165
  }
32104
32166
  for (const skill of companySkills) {
32167
+ const markdown = ensureSkillFrontmatter(skill.slug, skill.description, skill.markdown);
32105
32168
  const found = bySlug.get(skill.slug);
32106
32169
  if (!found) {
32107
32170
  const created = await client.createCompanySkill(companyId, {
32108
- name: skill.name,
32171
+ name: skill.slug,
32109
32172
  slug: skill.slug,
32110
32173
  description: skill.description,
32111
- markdown: skill.markdown,
32174
+ markdown,
32112
32175
  categories: skill.categories
32113
32176
  });
32114
32177
  slugToKey.set(skill.slug, created?.key || created?.slug || skill.slug);
@@ -32117,16 +32180,16 @@ async function provisionCompanySkills(client, companyId, companySkills, log) {
32117
32180
  }
32118
32181
  slugToKey.set(skill.slug, found.key || skill.slug);
32119
32182
  let updated = false;
32120
- if ((found.markdown ?? "") !== skill.markdown) {
32183
+ if ((found.markdown ?? "") !== markdown) {
32121
32184
  await client.updateCompanySkillFile(companyId, found.id, {
32122
32185
  path: "SKILL.md",
32123
- content: skill.markdown
32186
+ content: markdown
32124
32187
  });
32125
32188
  updated = true;
32126
32189
  }
32127
- if ((found.name ?? "") !== skill.name) {
32190
+ if ((found.name ?? "") !== skill.slug) {
32128
32191
  const renamed = await client.renameCompanySkill(companyId, found.id, {
32129
- name: skill.name,
32192
+ name: skill.slug,
32130
32193
  slug: found.slug
32131
32194
  });
32132
32195
  if (renamed == null) {
@@ -32389,6 +32452,11 @@ var plugin = definePlugin({
32389
32452
  ctx.data.register("templates", async () => {
32390
32453
  try {
32391
32454
  const cfg = await ctx.config.get() ?? {};
32455
+ const syncOffer = emptyTemplateSyncOffer(cfg);
32456
+ if (syncOffer) {
32457
+ const error61 = `Configured templatesPath "${syncOffer.targetDir}" is empty. ` + (syncOffer.canSync ? "Confirm sync from the configured template URL to populate it." : "Set templatesRepoUrl in plugin settings to enable sync, or clear templatesPath to use bundled templates.");
32458
+ return { presets: [], modules: [], roles: [], loadErrors: [error61], error: error61, syncOffer };
32459
+ }
32392
32460
  const templates = loadTemplates(await ensureTemplatesDir(cfg));
32393
32461
  for (const warning of templates.loadErrors) {
32394
32462
  ctx.logger.info(`\u26A0 Template load warning: ${warning}`);
@@ -32409,6 +32477,19 @@ var plugin = definePlugin({
32409
32477
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
32410
32478
  }
32411
32479
  });
32480
+ ctx.actions.register("sync-empty-templates", async (params) => {
32481
+ try {
32482
+ const cfg = await ctx.config.get() ?? {};
32483
+ const targetDir = syncEmptyTemplates(cfg, params.confirmation);
32484
+ ctx.logger.info("Synced empty configured template directory.");
32485
+ return { ok: true, targetDir };
32486
+ } catch {
32487
+ return {
32488
+ ok: false,
32489
+ error: "Template sync failed. Check the configured GitHub tree URL, permissions, and that the directory is still empty. Existing files were not overwritten. Reload to retry."
32490
+ };
32491
+ }
32492
+ });
32412
32493
  ctx.actions.register("check-update", async () => {
32413
32494
  try {
32414
32495
  const latestVersion = await fetchLatestPluginVersion();