@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/CHANGELOG.md +36 -0
- package/README.md +8 -4
- package/dist/manifest.js +3 -3
- package/dist/manifest.js.map +1 -1
- package/dist/ui/index.js +52 -4
- package/dist/ui/index.js.map +3 -3
- package/dist/worker.js +139 -34
- package/dist/worker.js.map +2 -2
- package/docs/PAPERCLIP-COMPATIBILITY.md +5 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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-
|
|
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
|
|
29594
|
-
contentHash:
|
|
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(
|
|
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
|
-
|
|
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(),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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.
|
|
32171
|
+
name: skill.slug,
|
|
32091
32172
|
slug: skill.slug,
|
|
32092
32173
|
description: skill.description,
|
|
32093
|
-
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 ?? "") !==
|
|
32183
|
+
if ((found.markdown ?? "") !== markdown) {
|
|
32103
32184
|
await client.updateCompanySkillFile(companyId, found.id, {
|
|
32104
32185
|
path: "SKILL.md",
|
|
32105
|
-
content:
|
|
32186
|
+
content: markdown
|
|
32106
32187
|
});
|
|
32107
32188
|
updated = true;
|
|
32108
32189
|
}
|
|
32109
|
-
if ((found.name ?? "") !== skill.
|
|
32190
|
+
if ((found.name ?? "") !== skill.slug) {
|
|
32110
32191
|
const renamed = await client.renameCompanySkill(companyId, found.id, {
|
|
32111
|
-
name: skill.
|
|
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
|
-
|
|
32373
|
-
|
|
32374
|
-
|
|
32375
|
-
|
|
32376
|
-
|
|
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 || ""
|