contentrain 0.16.0 → 0.17.0

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/README.md CHANGED
@@ -332,11 +332,18 @@ Import a WordPress site into a `.contentrain` content store:
332
332
  ```bash
333
333
  contentrain import export.xml # WXR export file (highest offline fidelity)
334
334
  contentrain import https://site.example # public REST
335
- contentrain import https://site.example --auth user:app-password # authenticated REST
335
+ contentrain import https://site.example --auth editor # authenticated REST: prompts for the Application Password
336
+ CONTENTRAIN_WP_APP_PASSWORD=… contentrain import https://site.example --auth editor
337
+ CONTENTRAIN_WP_AUTH=editor:… contentrain import https://site.example
338
+ contentrain import export.xml --include-emails # also write author/commenter e-mails
336
339
  ```
337
340
 
338
341
  Writes the canonical store plus `import-report.json`, `entry-source-map.json` (WP id → entry address), and — when the source has comments — `comments-export.json` (`contentrain-comments@1`, ready for a comments-service intake). An existing `.contentrain` is only overwritten with `--force`. Powered by `@contentrain/wp-import`.
339
342
 
343
+ **Personal data.** The store holds author names and logins, and commenters' names, website URLs and comment text. A WXR export also carries author and commenter e-mail addresses; they are left out of the store and of `comments-export.json` unless you pass `--include-emails`. Commenter IPs, user agents and avatar URLs (including Akismet's comment meta) are never written. The e-mails stay out by default because the store usually ends up in git (sometimes a public repo). The REST path never sees them.
344
+
345
+ **Credentials.** The Application Password is read from `CONTENTRAIN_WP_APP_PASSWORD` (with `--auth <user>`), from `CONTENTRAIN_WP_AUTH=user:password`, or from a hidden prompt. `--auth user:password` still works but is deprecated and warns: argv lands in shell history and process listings.
346
+
340
347
  ### `contentrain delta`
341
348
 
342
349
  Plan what a later WordPress change means for a store that was already delivered:
@@ -1,25 +1,24 @@
1
1
  import { appendFile, readFile, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { dirname, join } from "node:path";
3
3
  import { ensureDir, pathExists } from "@contentrain/mcp/util/fs";
4
4
  import { createGit } from "@contentrain/mcp/git/identity";
5
5
  //#region src/utils/ide.ts
6
- const AGENT_SKILL_NAMES = [
7
- "contentrain",
8
- "contentrain-normalize",
9
- "contentrain-quality",
10
- "contentrain-sdk",
11
- "contentrain-content",
12
- "contentrain-model",
13
- "contentrain-init",
14
- "contentrain-bulk",
15
- "contentrain-validate-fix",
16
- "contentrain-review",
17
- "contentrain-translate",
18
- "contentrain-generate",
19
- "contentrain-serve",
20
- "contentrain-diff",
21
- "contentrain-doctor"
22
- ];
6
+ /**
7
+ * Every skill the installed `@contentrain/skills` ships: the directories under
8
+ * its `skills/` that hold a `SKILL.md`, sorted. Read from disk rather than kept
9
+ * as a second hand-written list, which is how `contentrain skills` came to
10
+ * install 15 of 16 (`contentrain-migrate-wordpress` was never added).
11
+ *
12
+ * The package exports `./skills/*` only, so the directory is found through one
13
+ * known file: the core `contentrain` skill.
14
+ */
15
+ async function listAgentSkillNames(resolveSkillFile) {
16
+ const { readdir } = await import("node:fs/promises");
17
+ const skillsRoot = dirname(dirname(resolveSkillFile("skills/contentrain/SKILL.md")));
18
+ const dirs = (await readdir(skillsRoot, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
19
+ const hasSkill = await Promise.all(dirs.map((d) => pathExists(join(skillsRoot, d, "SKILL.md"))));
20
+ return dirs.filter((_, i) => hasSkill[i]).toSorted();
21
+ }
23
22
  const OLD_RULE_FILES = [
24
23
  "contentrain-content-quality.md",
25
24
  "contentrain-seo-rules.md",
@@ -223,7 +222,11 @@ async function installIdeRulesAndSkills(projectRoot, ide, resolveRuleFile, resol
223
222
  if (resolveSkillFile) {
224
223
  const skillsDir = join(projectRoot, ide.skillsDir);
225
224
  await ensureDir(skillsDir);
226
- for (const skillName of AGENT_SKILL_NAMES) {
225
+ let skillNames = [];
226
+ try {
227
+ skillNames = await listAgentSkillNames(resolveSkillFile);
228
+ } catch {}
229
+ for (const skillName of skillNames) {
227
230
  const skillDir = join(skillsDir, skillName);
228
231
  const skillMd = join(skillDir, "SKILL.md");
229
232
  try {
@@ -294,4 +297,4 @@ async function addClaudeMdReference(projectRoot) {
294
297
  }
295
298
  }
296
299
  //#endregion
297
- export { createPackageResolver as a, writeMcpConfig as c, addClaudeMdReference as i, IDE_CONFIGS as n, detectIdes as o, MCP_CONFIGS as r, installIdeRulesAndSkills as s, AGENT_SKILL_NAMES as t };
300
+ export { detectIdes as a, writeMcpConfig as c, createPackageResolver as i, MCP_CONFIGS as n, installIdeRulesAndSkills as o, addClaudeMdReference as r, listAgentSkillNames as s, IDE_CONFIGS as t };
@@ -1,10 +1,64 @@
1
1
  import { i as pc } from "./ui-B8l_LC36.mjs";
2
2
  import { defineCommand } from "citty";
3
- import { intro, log, outro, spinner } from "@clack/prompts";
3
+ import { intro, isCancel, log, outro, password, spinner } from "@clack/prompts";
4
4
  import { createReadStream, existsSync } from "node:fs";
5
5
  import { access, mkdir, writeFile } from "node:fs/promises";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  //#region src/commands/import.ts
8
+ /** `user:password` for REST, read from the environment so it stays out of argv and shell history. */
9
+ const WP_AUTH_ENV = "CONTENTRAIN_WP_AUTH";
10
+ /** The Application Password alone, for `--auth <user>`. */
11
+ const WP_APP_PASSWORD_ENV = "CONTENTRAIN_WP_APP_PASSWORD";
12
+ /** `user:password` → parts; the password may itself contain colons. */
13
+ const splitUserPassword = (value) => {
14
+ const i = value.indexOf(":");
15
+ if (i <= 0 || i === value.length - 1) return void 0;
16
+ return {
17
+ user: value.slice(0, i),
18
+ appPassword: value.slice(i + 1)
19
+ };
20
+ };
21
+ /**
22
+ * Where the REST credential comes from, in order:
23
+ * 1. `--auth user:password` — still works, but argv lands in shell history and
24
+ * process listings, so the caller prints a deprecation warning.
25
+ * 2. `--auth user` — the password from `CONTENTRAIN_WP_APP_PASSWORD`, else an
26
+ * interactive hidden prompt (only when `interactive`).
27
+ * 3. `CONTENTRAIN_WP_AUTH=user:password` in the environment.
28
+ * Nothing given → anonymous REST.
29
+ */
30
+ async function resolveRestAuth(input) {
31
+ const flag = input.auth?.trim();
32
+ if (flag) {
33
+ if (flag.includes(":")) {
34
+ const auth = splitUserPassword(flag);
35
+ return auth ? {
36
+ auth,
37
+ deprecatedArgv: true
38
+ } : { error: "--auth expects a user name (the password comes from the environment or a prompt)" };
39
+ }
40
+ const fromEnv = input.env[WP_APP_PASSWORD_ENV];
41
+ if (fromEnv) return { auth: {
42
+ user: flag,
43
+ appPassword: fromEnv
44
+ } };
45
+ if (input.interactive && input.prompt) {
46
+ const pw = await input.prompt(flag);
47
+ if (pw) return { auth: {
48
+ user: flag,
49
+ appPassword: pw
50
+ } };
51
+ return { error: "No Application Password entered" };
52
+ }
53
+ return { error: `--auth ${flag} needs the Application Password in ${WP_APP_PASSWORD_ENV} (or run in a terminal to be prompted)` };
54
+ }
55
+ const envAuth = input.env[WP_AUTH_ENV];
56
+ if (envAuth) {
57
+ const auth = splitUserPassword(envAuth);
58
+ return auth ? { auth } : { error: `${WP_AUTH_ENV} must be user:password` };
59
+ }
60
+ return {};
61
+ }
8
62
  var import_default = defineCommand({
9
63
  meta: {
10
64
  name: "import",
@@ -23,7 +77,12 @@ var import_default = defineCommand({
23
77
  },
24
78
  auth: {
25
79
  type: "string",
26
- description: "REST Application Password as user:password (lifts access to rest_auth)",
80
+ description: `REST user for an Application Password (lifts access to rest_auth). The password comes from ${WP_APP_PASSWORD_ENV} or a prompt; ${WP_AUTH_ENV}=user:password also works. user:password here is deprecated`,
81
+ required: false
82
+ },
83
+ "include-emails": {
84
+ type: "boolean",
85
+ description: "Write author and commenter e-mail addresses from a WXR export into the store (default: left out — they are personal data and the store usually lands in git)",
27
86
  required: false
28
87
  },
29
88
  force: {
@@ -58,11 +117,29 @@ var import_default = defineCommand({
58
117
  let raw;
59
118
  const warnings = [];
60
119
  if (isUrl) {
61
- const [user, ...pw] = (args.auth ?? "").split(":");
62
- const auth = args.auth && user && pw.length ? {
63
- user,
64
- appPassword: pw.join(":")
65
- } : void 0;
120
+ const resolved = await resolveRestAuth({
121
+ auth: args.auth,
122
+ env: process.env,
123
+ interactive: !args.json && Boolean(process.stdin.isTTY),
124
+ prompt: async (user) => {
125
+ s?.stop("Credential needed");
126
+ const value = await password({ message: `Application Password for ${user}` });
127
+ s?.start(`Fetching ${args.source} over REST`);
128
+ return isCancel(value) ? void 0 : value;
129
+ }
130
+ });
131
+ if (resolved.error) {
132
+ s?.stop("Credential missing");
133
+ log.error(resolved.error);
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+ if (resolved.deprecatedArgv) {
138
+ const msg = `--auth user:password is deprecated: the password lands in shell history and process listings. Use --auth <user> with ${WP_APP_PASSWORD_ENV} (or the prompt), or ${WP_AUTH_ENV}=user:password`;
139
+ if (args.json) process.stderr.write(`warning: ${msg}\n`);
140
+ else log.warning(msg);
141
+ }
142
+ const auth = resolved.auth;
66
143
  const result = await fetchRestRawIR({
67
144
  origin: args.source,
68
145
  auth,
@@ -73,9 +150,10 @@ var import_default = defineCommand({
73
150
  } else raw = (await parseWxr(createReadStream(args.source), { tool: "contentrain-cli" })).raw;
74
151
  s?.stop(`Source read: ${raw.posts.length} posts, ${raw.attachments.length} media, ${raw.comments?.length ?? 0} comments (${raw.provenance.kind})`);
75
152
  s?.start("Converting to .contentrain");
76
- const { files, entry_source_map, report } = rawToContentrain(raw);
153
+ const includeEmails = Boolean(args["include-emails"]);
154
+ const { files, entry_source_map, report } = rawToContentrain(raw, { includeEmails });
77
155
  if (raw.comments?.length) {
78
- const exp = buildCommentsExport(raw, entry_source_map);
156
+ const exp = buildCommentsExport(raw, entry_source_map, { includeEmails });
79
157
  files["comments-export.json"] = `${JSON.stringify(exp, null, 2)}\n`;
80
158
  const summary = summarizeComments(exp);
81
159
  if (summary.unresolved?.length) warnings.push(`${summary.unresolved.length} comments reference posts outside the import`);
@@ -99,6 +177,7 @@ var import_default = defineCommand({
99
177
  if (report.password_protected_drafts) log.warning(`${report.password_protected_drafts} password-protected posts brought in as drafts — their body is the protected text; publish one only after deciding what it should show`);
100
178
  if (report.dropped_relations) log.warning(`${report.dropped_relations} relations pointed outside the import and were dropped (details in import-report.json)`);
101
179
  for (const w of warnings) log.warning(w);
180
+ if (includeEmails && raw.provenance.kind === "wxr") log.warning("Author and commenter e-mail addresses were written into the store (--include-emails). They are personal data: keep this repository private");
102
181
  if (raw.comments?.length) log.info(`comments-export.json written (contentrain-comments@1) — ready for a comments-service intake`);
103
182
  outro(`Done. Next: ${pc.bold("contentrain validate")} to check the imported store.`);
104
183
  }
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { i as pc } from "./ui-B8l_LC36.mjs";
3
3
  import { defineCommand, runMain } from "citty";
4
4
  //#region package.json
5
- var version = "0.16.0";
5
+ var version = "0.17.0";
6
6
  //#endregion
7
7
  //#region src/utils/debug.ts
8
8
  /**
@@ -43,8 +43,8 @@ runMain(defineCommand({
43
43
  description: "Contentrain CLI — AI content governance infrastructure"
44
44
  },
45
45
  subCommands: {
46
- init: () => import("./init-Bp5aMELm.mjs").then((m) => m.default),
47
- import: () => import("./import-D9TtmPgM.mjs").then((m) => m.default),
46
+ init: () => import("./init-CqQgYoJq.mjs").then((m) => m.default),
47
+ import: () => import("./import-DXA71z9x.mjs").then((m) => m.default),
48
48
  delta: () => import("./delta-D6Cp9BdP.mjs").then((m) => m.default),
49
49
  status: () => import("./status-DSzoldCi.mjs").then((m) => m.default),
50
50
  doctor: () => import("./doctor-B9MJgRO5.mjs").then((m) => m.default),
@@ -58,8 +58,8 @@ runMain(defineCommand({
58
58
  describe: () => import("./describe-C6-AWlKx.mjs").then((m) => m.default),
59
59
  "describe-format": () => import("./describe-format-CR0-NMR8.mjs").then((m) => m.default),
60
60
  scaffold: () => import("./scaffold-DX6eJ0n1.mjs").then((m) => m.default),
61
- setup: () => import("./setup-BRVyTOxY.mjs").then((m) => m.default),
62
- skills: () => import("./skills-DZT0pbmO.mjs").then((m) => m.default),
61
+ setup: () => import("./setup-aaeHJ90i.mjs").then((m) => m.default),
62
+ skills: () => import("./skills-ix03X2Rj.mjs").then((m) => m.default),
63
63
  studio: () => import("./studio-CU0fTK5f.mjs").then((m) => m.default)
64
64
  }
65
65
  }));
@@ -1,6 +1,6 @@
1
1
  import { i as pc } from "./ui-B8l_LC36.mjs";
2
2
  import { r as resolveProjectRoot, t as loadProjectContext } from "./context-SIGQmozp.mjs";
3
- import { a as createPackageResolver, c as writeMcpConfig, i as addClaudeMdReference, n as IDE_CONFIGS, o as detectIdes, s as installIdeRulesAndSkills } from "./ide-D1J4iAhK.mjs";
3
+ import { a as detectIdes, c as writeMcpConfig, i as createPackageResolver, o as installIdeRulesAndSkills, r as addClaudeMdReference, t as IDE_CONFIGS } from "./ide-zPN-87R5.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { confirm, intro, isCancel, log, multiselect, outro, select, spinner } from "@clack/prompts";
6
6
  import { appendFile, readFile, writeFile } from "node:fs/promises";
@@ -1,6 +1,6 @@
1
1
  import { i as pc } from "./ui-B8l_LC36.mjs";
2
2
  import { r as resolveProjectRoot, t as loadProjectContext } from "./context-SIGQmozp.mjs";
3
- import { a as createPackageResolver, c as writeMcpConfig, n as IDE_CONFIGS, o as detectIdes, r as MCP_CONFIGS, s as installIdeRulesAndSkills } from "./ide-D1J4iAhK.mjs";
3
+ import { a as detectIdes, c as writeMcpConfig, i as createPackageResolver, n as MCP_CONFIGS, o as installIdeRulesAndSkills, t as IDE_CONFIGS } from "./ide-zPN-87R5.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { intro, log, outro, spinner } from "@clack/prompts";
6
6
  //#region src/commands/setup.ts
@@ -1,6 +1,6 @@
1
1
  import { i as pc } from "./ui-B8l_LC36.mjs";
2
2
  import { r as resolveProjectRoot } from "./context-SIGQmozp.mjs";
3
- import { a as createPackageResolver, n as IDE_CONFIGS, o as detectIdes, s as installIdeRulesAndSkills, t as AGENT_SKILL_NAMES } from "./ide-D1J4iAhK.mjs";
3
+ import { a as detectIdes, i as createPackageResolver, o as installIdeRulesAndSkills, s as listAgentSkillNames, t as IDE_CONFIGS } from "./ide-zPN-87R5.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { intro, log, outro, spinner } from "@clack/prompts";
6
6
  import { join } from "node:path";
@@ -72,12 +72,18 @@ var skills_default = defineCommand({
72
72
  });
73
73
  async function listInstalledSkills(projectRoot) {
74
74
  let found = false;
75
+ const resolveSkillFile = await createPackageResolver("@contentrain/skills", projectRoot);
76
+ let skillNames = [];
77
+ if (resolveSkillFile) try {
78
+ skillNames = await listAgentSkillNames(resolveSkillFile);
79
+ } catch {}
80
+ if (skillNames.length === 0) log.warning("@contentrain/skills package not found — cannot tell which skills are missing");
75
81
  for (const [, ide] of Object.entries(IDE_CONFIGS)) {
76
82
  const skillsDir = join(projectRoot, ide.skillsDir);
77
83
  if (!await pathExists(skillsDir)) continue;
78
84
  found = true;
79
85
  log.info(`${pc.bold(ide.name)} (${ide.skillsDir}/):`);
80
- for (const skillName of AGENT_SKILL_NAMES) if (await pathExists(join(skillsDir, skillName, "SKILL.md"))) log.message(` ${pc.green("✓")} ${skillName}`);
86
+ for (const skillName of skillNames) if (await pathExists(join(skillsDir, skillName, "SKILL.md"))) log.message(` ${pc.green("✓")} ${skillName}`);
81
87
  else log.message(` ${pc.red("✗")} ${skillName} ${pc.dim("(missing)")}`);
82
88
  if (await pathExists(join(projectRoot, ide.rulesDir, ide.guardrailsFileName))) log.message(` ${pc.green("✓")} essential rules`);
83
89
  else log.message(` ${pc.red("✗")} essential rules ${pc.dim("(missing)")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentrain",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "license": "MIT",
5
5
  "description": "CLI for Contentrain — AI content governance infrastructure",
6
6
  "type": "module",
@@ -47,12 +47,12 @@
47
47
  "simple-git": "^3.27.0",
48
48
  "ws": "^8.18.0",
49
49
  "zod": "^3.24.0",
50
- "@contentrain/mcp": "3.10.0",
51
- "@contentrain/query": "7.6.41",
50
+ "@contentrain/mcp": "3.10.1",
52
51
  "@contentrain/rules": "0.7.3",
53
- "@contentrain/skills": "0.9.5",
54
- "@contentrain/types": "1.61.0",
55
- "@contentrain/wp-import": "0.12.1"
52
+ "@contentrain/query": "7.6.42",
53
+ "@contentrain/skills": "0.9.6",
54
+ "@contentrain/types": "1.62.0",
55
+ "@contentrain/wp-import": "0.13.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/ws": "^8.5.0",