@starlein/paperclip-plugin-company-wizard 0.6.2 → 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.2",
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 operator-managed templates directory. Defaults to templates bundled with this plugin release. Local files are never overwritten by refresh."
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",
@@ -29399,7 +29399,7 @@ var manifest = {
29399
29399
  type: "string",
29400
29400
  enum: ["anthropic", "openai"],
29401
29401
  default: "anthropic",
29402
- description: "AI provider used for company generation. Anthropic uses Claude Opus 5 at max effort; OpenAI uses GPT-5.6 Sol at high reasoning effort."
29402
+ description: "AI provider used for company generation. Anthropic uses Claude Opus 5 at max effort; OpenAI uses GPT-6-Astra at high reasoning effort."
29403
29403
  },
29404
29404
  anthropicApiKey: {
29405
29405
  // Paperclip's secret picker submits an EnvSecretRefBinding object,
@@ -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 {
@@ -31673,17 +31693,13 @@ function isDockerLayout() {
31673
31693
  async function ensureTemplatesDir(cfg) {
31674
31694
  const repoUrl = cfg.templatesRepoUrl || DEFAULT_TEMPLATES_REPO_URL;
31675
31695
  if (cfg.templatesPath) {
31676
- if (fs2.existsSync(cfg.templatesPath)) return cfg.templatesPath;
31677
- throw new Error(`Configured templatesPath does not exist: ${cfg.templatesPath}`);
31696
+ return validateTemplatesDir(cfg.templatesPath, "Configured templatesPath");
31678
31697
  }
31679
31698
  if (!cfg.templatesRepoUrl || cfg.templatesRepoUrl === DEFAULT_TEMPLATES_REPO_URL) {
31680
- if (fs2.existsSync(BUNDLED_TEMPLATES_DIR)) return BUNDLED_TEMPLATES_DIR;
31681
- throw new Error(
31682
- "Bundled templates are missing. Reinstall Company Wizard or configure templatesPath."
31683
- );
31699
+ return validateTemplatesDir(BUNDLED_TEMPLATES_DIR, "Bundled templates");
31684
31700
  }
31685
31701
  const cacheDir = resolveTemplatesCacheDir(cfg);
31686
- if (fs2.existsSync(cacheDir)) return cacheDir;
31702
+ if (fs2.existsSync(cacheDir)) return validateTemplatesDir(cacheDir, "Cached templates");
31687
31703
  return refreshTemplatesCache(cfg);
31688
31704
  }
31689
31705
  function resolveTemplatesCacheDir(cfg) {
@@ -31695,14 +31711,12 @@ function resolveTemplatesCacheDir(cfg) {
31695
31711
  }
31696
31712
  function refreshTemplatesCache(cfg, log) {
31697
31713
  if (cfg.templatesPath) {
31698
- if (!fs2.existsSync(cfg.templatesPath))
31699
- throw new Error(`Configured templatesPath does not exist: ${cfg.templatesPath}`);
31714
+ validateTemplatesDir(cfg.templatesPath, "Configured templatesPath");
31700
31715
  log?.("Using operator-managed templatesPath; no files overwritten.");
31701
31716
  return cfg.templatesPath;
31702
31717
  }
31703
31718
  if (!cfg.templatesRepoUrl || cfg.templatesRepoUrl === DEFAULT_TEMPLATES_REPO_URL) {
31704
- if (!fs2.existsSync(BUNDLED_TEMPLATES_DIR))
31705
- throw new Error("Bundled templates are missing. Reinstall Company Wizard.");
31719
+ validateTemplatesDir(BUNDLED_TEMPLATES_DIR, "Bundled templates");
31706
31720
  log?.(`Using bundled Company Wizard ${CURRENT_PLUGIN_VERSION} templates.`);
31707
31721
  return BUNDLED_TEMPLATES_DIR;
31708
31722
  }
@@ -31713,6 +31727,9 @@ function refreshTemplatesCache(cfg, log) {
31713
31727
  const backupDir = `${stagingDir}-previous`;
31714
31728
  try {
31715
31729
  downloadTemplatesFromGithub(stagingDir, repoUrl);
31730
+ validateTemplatesDir(stagingDir, "Downloaded templates");
31731
+ const { loadErrors } = loadTemplates(stagingDir);
31732
+ if (loadErrors.length > 0) throw new Error(loadErrors.join("\n"));
31716
31733
  if (fs2.existsSync(targetDir)) fs2.renameSync(targetDir, backupDir);
31717
31734
  try {
31718
31735
  fs2.renameSync(stagingDir, targetDir);
@@ -31727,8 +31744,50 @@ function refreshTemplatesCache(cfg, log) {
31727
31744
  log?.(`\u2713 Refreshed templates cache from ${repoUrl} \u2192 ${targetDir}`);
31728
31745
  return targetDir;
31729
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
+ }
31730
31789
  var ANTHROPIC_AI_MODEL = "claude-opus-5";
31731
- var OPENAI_AI_MODEL = "gpt-5.6-sol";
31790
+ var OPENAI_AI_MODEL = "gpt-6-astra";
31732
31791
  function isLikelyAnthropicApiKey(value) {
31733
31792
  return value.startsWith("sk-ant-");
31734
31793
  }
@@ -31878,7 +31937,11 @@ function loadJsonFiles(dir, filename) {
31878
31937
  const fp = path2.join(fullDir, filename);
31879
31938
  if (!fs2.existsSync(fp)) continue;
31880
31939
  try {
31881
- items.push(JSON.parse(fs2.readFileSync(fp, "utf-8")));
31940
+ const item = JSON.parse(fs2.readFileSync(fp, "utf-8"));
31941
+ if (!item || typeof item !== "object" || Array.isArray(item) || typeof item.name !== "string" || !item.name.trim()) {
31942
+ throw new Error("Template metadata must be an object with a non-empty name");
31943
+ }
31944
+ items.push(item);
31882
31945
  } catch (err) {
31883
31946
  errors.push(`Failed to parse ${fp}: ${err instanceof Error ? err.message : String(err)}`);
31884
31947
  }
@@ -31909,6 +31972,23 @@ function loadTemplates(templatesDir) {
31909
31972
  loadErrors: [...presetLoad.errors, ...moduleLoad.errors, ...roleLoad.errors]
31910
31973
  };
31911
31974
  }
31975
+ function validateTemplatesDir(dir, source) {
31976
+ try {
31977
+ if (!fs2.existsSync(dir)) throw new Error("directory does not exist");
31978
+ if (!fs2.statSync(dir).isDirectory()) throw new Error("path is not a directory");
31979
+ const templates = loadTemplates(dir);
31980
+ if (!templates.roles.some((role) => role.name === "ceo" && role._base)) {
31981
+ throw new Error(
31982
+ 'No base CEO template found. Expected roles/ceo/role.meta.json with name "ceo" and base: true.' + (templates.loadErrors.length ? ` ${templates.loadErrors.join(" ")}` : "")
31983
+ );
31984
+ }
31985
+ return dir;
31986
+ } catch (err) {
31987
+ const detail = err instanceof Error ? err.message : String(err);
31988
+ const guidance = source === "Configured templatesPath" ? "Point templatesPath to the template root (containing roles/, modules/, presets/), or clear templatesPath to use bundled templates / the configured GitHub source. templatesPath overrides templatesRepoUrl; refresh never populates or overwrites local files." : "Reinstall Company Wizard for bundled templates, or refresh the configured custom GitHub source.";
31989
+ throw new Error(`${source} "${dir}": ${detail} ${guidance}`);
31990
+ }
31991
+ }
31912
31992
  function resolvePaperclipCredentials(cfg) {
31913
31993
  return {
31914
31994
  url: cfg.paperclipUrl || process.env.PAPERCLIP_PUBLIC_URL || "http://localhost:3100",
@@ -32084,13 +32164,14 @@ async function provisionCompanySkills(client, companyId, companySkills, log) {
32084
32164
  }
32085
32165
  }
32086
32166
  for (const skill of companySkills) {
32167
+ const markdown = ensureSkillFrontmatter(skill.slug, skill.description, skill.markdown);
32087
32168
  const found = bySlug.get(skill.slug);
32088
32169
  if (!found) {
32089
32170
  const created = await client.createCompanySkill(companyId, {
32090
- name: skill.name,
32171
+ name: skill.slug,
32091
32172
  slug: skill.slug,
32092
32173
  description: skill.description,
32093
- markdown: skill.markdown,
32174
+ markdown,
32094
32175
  categories: skill.categories
32095
32176
  });
32096
32177
  slugToKey.set(skill.slug, created?.key || created?.slug || skill.slug);
@@ -32099,16 +32180,16 @@ async function provisionCompanySkills(client, companyId, companySkills, log) {
32099
32180
  }
32100
32181
  slugToKey.set(skill.slug, found.key || skill.slug);
32101
32182
  let updated = false;
32102
- if ((found.markdown ?? "") !== skill.markdown) {
32183
+ if ((found.markdown ?? "") !== markdown) {
32103
32184
  await client.updateCompanySkillFile(companyId, found.id, {
32104
32185
  path: "SKILL.md",
32105
- content: skill.markdown
32186
+ content: markdown
32106
32187
  });
32107
32188
  updated = true;
32108
32189
  }
32109
- if ((found.name ?? "") !== skill.name) {
32190
+ if ((found.name ?? "") !== skill.slug) {
32110
32191
  const renamed = await client.renameCompanySkill(companyId, found.id, {
32111
- name: skill.name,
32192
+ name: skill.slug,
32112
32193
  slug: found.slug
32113
32194
  });
32114
32195
  if (renamed == null) {
@@ -32369,14 +32450,23 @@ ${moduleNames.length > 0 ? moduleNames.map((mod) => `- ${mod}`).join("\n") : "-
32369
32450
  var plugin = definePlugin({
32370
32451
  async setup(ctx) {
32371
32452
  ctx.data.register("templates", async () => {
32372
- const cfg = await ctx.config.get() ?? {};
32373
- const templates = loadTemplates(await ensureTemplatesDir(cfg));
32374
- if (templates.loadErrors.length > 0) {
32375
- for (const err of templates.loadErrors) {
32376
- ctx.logger.info(`\u26A0 Template load warning: ${err}`);
32453
+ try {
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
+ }
32460
+ const templates = loadTemplates(await ensureTemplatesDir(cfg));
32461
+ for (const warning of templates.loadErrors) {
32462
+ ctx.logger.info(`\u26A0 Template load warning: ${warning}`);
32377
32463
  }
32464
+ return templates;
32465
+ } catch (err) {
32466
+ const error61 = err instanceof Error ? err.message : String(err);
32467
+ ctx.logger.info(`Template load failed: ${error61}`);
32468
+ return { presets: [], modules: [], roles: [], loadErrors: [error61], error: error61 };
32378
32469
  }
32379
- return templates;
32380
32470
  });
32381
32471
  ctx.actions.register("refresh-templates", async () => {
32382
32472
  try {
@@ -32387,6 +32477,19 @@ var plugin = definePlugin({
32387
32477
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
32388
32478
  }
32389
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
+ });
32390
32493
  ctx.actions.register("check-update", async () => {
32391
32494
  try {
32392
32495
  const latestVersion = await fetchLatestPluginVersion();
@@ -33551,6 +33654,8 @@ var plugin = definePlugin({
33551
33654
  async onValidateConfig(config2) {
33552
33655
  const paperclipUrl = config2.paperclipUrl || process.env.PAPERCLIP_PUBLIC_URL || "http://localhost:3100";
33553
33656
  try {
33657
+ const templates = loadTemplates(await ensureTemplatesDir(config2));
33658
+ if (templates.loadErrors.length > 0) return { ok: false, errors: templates.loadErrors };
33554
33659
  const client = new PaperclipClient(paperclipUrl, {
33555
33660
  email: config2.paperclipEmail || "",
33556
33661
  password: config2.paperclipPassword || ""