@p11-core/cli 0.0.16 → 0.0.18

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/index.js CHANGED
@@ -3524,6 +3524,7 @@ var P11_HISTORY_LOCK_TIMEOUT_MS = 5e3;
3524
3524
  var P11_HISTORY_DEFAULT_LIMIT = 20;
3525
3525
  var P11_RUNTIME_VERSION = "0.0.1";
3526
3526
  var P11_MANIFEST_FILE = "p11-manifest.json";
3527
+ var P11_SOCIAL_PREVIEW_IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
3527
3528
  var cliPackageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
3528
3529
  var docsTopics = /* @__PURE__ */ new Map([
3529
3530
  ["index", "docs/index.md"],
@@ -3699,6 +3700,22 @@ Environment:
3699
3700
  );
3700
3701
  });
3701
3702
  allowLegacyParserBehavior(commentsCommand);
3703
+ const replyCommand = program2.command("reply").description("Reply to a comment on a read URL or id.").argument("[readUrl|readId]").argument("[commentId]").option("--name [name]", "Reply author name. Defaults to AI Agent.").option("--body [text]", "Plain-text reply body. Newlines are preserved.").option("--body-file [file]", "Read plain-text reply body from a file, or '-' for stdin.").option("--json", "Print the API response as JSON.").option("--version [n]", "Reply on a specific page version.").option("--api-url [url]", "Override the p11 API URL.").action(async (target, commentId, options2) => {
3704
+ await runCliAction(
3705
+ "reply",
3706
+ {
3707
+ has_target: Boolean(target),
3708
+ has_comment_id: Boolean(commentId),
3709
+ json: Boolean(options2.json),
3710
+ body: typeof options2.body === "string",
3711
+ body_file: typeof options2.bodyFile === "string",
3712
+ version: typeof options2.version === "string",
3713
+ api_url_override: typeof options2.apiUrl === "string"
3714
+ },
3715
+ () => reply(target, commentId, normalizeReplyOptions(options2))
3716
+ );
3717
+ });
3718
+ allowLegacyParserBehavior(replyCommand);
3702
3719
  const deleteCommand = program2.command("delete").description("Delete a document and all of its versions and comments.").argument("[editUrl|editId]").option("--json", "Print the API response as JSON.").option("--api-url [url]", "Override the p11 API URL.").action(async (target, options2) => {
3703
3720
  await runCliAction(
3704
3721
  "delete",
@@ -3791,6 +3808,16 @@ function normalizeCommentsOptions(options) {
3791
3808
  apiUrl: optionString(options.apiUrl, "--api-url requires a URL.")
3792
3809
  };
3793
3810
  }
3811
+ function normalizeReplyOptions(options) {
3812
+ return {
3813
+ json: options.json,
3814
+ name: optionString(options.name, "--name requires a value."),
3815
+ body: optionString(options.body, "--body requires reply text."),
3816
+ bodyFile: optionString(options.bodyFile, "--body-file requires a file path or '-'."),
3817
+ version: optionString(options.version, "--version requires a positive integer."),
3818
+ apiUrl: optionString(options.apiUrl, "--api-url requires a URL.")
3819
+ };
3820
+ }
3794
3821
  function normalizeDeleteOptions(options) {
3795
3822
  return {
3796
3823
  json: options.json,
@@ -3938,6 +3965,57 @@ async function comments(target, options) {
3938
3965
  printComments(data);
3939
3966
  }
3940
3967
  }
3968
+ async function reply(target, commentId, options) {
3969
+ if (!target || !commentId) {
3970
+ throw new Error("Usage: p11 reply <readUrl|readId> <commentId> --body TEXT [--name NAME] [--json] [--version N]");
3971
+ }
3972
+ validateCommentId(commentId);
3973
+ const body = await replyBody(options);
3974
+ if (!body.trim()) throw new Error("Reply body is required. Use --body TEXT or --body-file FILE.");
3975
+ const version = options.version === void 0 ? void 0 : parseVersionFlag(String(options.version));
3976
+ const url = replyApiUrlForTarget(target, {
3977
+ apiUrl: options.apiUrl,
3978
+ version
3979
+ });
3980
+ const response = await fetchForOperation("Reply", url, {
3981
+ method: "POST",
3982
+ headers: {
3983
+ "content-type": "application/json"
3984
+ },
3985
+ body: JSON.stringify({
3986
+ name: options.name ?? "AI Agent",
3987
+ body,
3988
+ parentId: commentId
3989
+ })
3990
+ });
3991
+ const text = await response.text();
3992
+ if (!response.ok) {
3993
+ throw new Error(`Reply failed (${response.status}): ${responseErrorMessage(text)}`);
3994
+ }
3995
+ const data = JSON.parse(text);
3996
+ if (options.json) {
3997
+ console.log(JSON.stringify(data, null, 2));
3998
+ return;
3999
+ }
4000
+ console.log(`Replied ${data.comment?.id ?? "comment"}`);
4001
+ if (data.comment?.parentId) console.log(`parentId: ${data.comment.parentId}`);
4002
+ if (data.comment?.createdAt) console.log(`createdAt: ${data.comment.createdAt}`);
4003
+ }
4004
+ async function replyBody(options) {
4005
+ if (options.body !== void 0 && options.bodyFile !== void 0) {
4006
+ throw new Error("Use either --body or --body-file, not both.");
4007
+ }
4008
+ if (options.body !== void 0) return options.body;
4009
+ if (options.bodyFile === "-") return readStdin();
4010
+ if (options.bodyFile !== void 0) return readFile(path.resolve(options.bodyFile), "utf8");
4011
+ return "";
4012
+ }
4013
+ async function readStdin() {
4014
+ let body = "";
4015
+ process.stdin.setEncoding("utf8");
4016
+ for await (const chunk of process.stdin) body += chunk;
4017
+ return body;
4018
+ }
3941
4019
  async function deleteDocument(target, options) {
3942
4020
  if (!target) {
3943
4021
  throw new Error("Usage: p11 delete <editUrl|editId> [--json] [--api-url URL]");
@@ -4270,10 +4348,10 @@ function printComments(data) {
4270
4348
  console.log(`[${comment.createdAt}]${target}${resolved} ${comment.name}`);
4271
4349
  if (comment.quote) console.log(`> ${comment.quote}`);
4272
4350
  console.log(comment.body);
4273
- for (const reply of comment.replies ?? []) {
4274
- const replyResolved = reply.resolvedAt ? " [resolved]" : "";
4275
- console.log(` [${reply.createdAt}] reply${replyResolved} ${reply.name}`);
4276
- console.log(` ${reply.body}`);
4351
+ for (const reply2 of comment.replies ?? []) {
4352
+ const replyResolved = reply2.resolvedAt ? " [resolved]" : "";
4353
+ console.log(` [${reply2.createdAt}] reply${replyResolved} ${reply2.name}`);
4354
+ console.log(` ${reply2.body}`);
4277
4355
  }
4278
4356
  console.log("");
4279
4357
  }
@@ -4339,7 +4417,12 @@ async function buildPageModule(inputFile, outDir) {
4339
4417
  const pageSource = await readFile(inputFile, "utf8");
4340
4418
  const pageAst = parsePageSource(pageSource);
4341
4419
  validatePageAst(pageAst, inputFile);
4420
+ const pageMetadata = extractPageMetadata(pageAst, inputFile);
4421
+ rewriteRelativeModuleSpecifiers(pageAst, path.dirname(inputFile));
4422
+ await mkdir(tempDir, { recursive: true });
4342
4423
  await writeFile(pagePath, addSourceLineAttributes(pageAst, inputFile));
4424
+ const metadataAssetTracker = createSocialPreviewAssetTracker(pageMetadata, tempDir);
4425
+ const pageImport = pageMetadata ? 'import Page, { metadata as __p11PageMetadata } from "./Page";' : 'import Page from "./Page";';
4343
4426
  await writeFile(
4344
4427
  entryPath,
4345
4428
  [
@@ -4353,7 +4436,8 @@ async function buildPageModule(inputFile, outDir) {
4353
4436
  " __P11_CREATE_TEXT_ANNOTATOR__: createTextAnnotator,",
4354
4437
  " __P11_RANGE_TO_SELECTOR__: rangeToSelector",
4355
4438
  "});",
4356
- 'import Page from "./Page";',
4439
+ pageImport,
4440
+ ...pageMetadata ? ["void __p11PageMetadata;"] : [],
4357
4441
  "",
4358
4442
  'createRoot(document.getElementById("root")!).render(',
4359
4443
  " <React.StrictMode>",
@@ -4387,7 +4471,7 @@ async function buildPageModule(inputFile, outDir) {
4387
4471
  base: "./",
4388
4472
  configFile: false,
4389
4473
  logLevel: "warn",
4390
- plugins: [react(), tailwindcss()],
4474
+ plugins: [react(), tailwindcss(), ...metadataAssetTracker.plugin ? [metadataAssetTracker.plugin] : []],
4391
4475
  resolve: {
4392
4476
  alias: {
4393
4477
  "@p11-core/components/styles.css": componentStyles,
@@ -4406,6 +4490,7 @@ async function buildPageModule(inputFile, outDir) {
4406
4490
  outDir: path.relative(tempDir, outDir),
4407
4491
  emptyOutDir: true,
4408
4492
  assetsDir: "assets",
4493
+ assetsInlineLimit: 0,
4409
4494
  rollupOptions: {
4410
4495
  input: {
4411
4496
  index: "index.html"
@@ -4416,11 +4501,167 @@ async function buildPageModule(inputFile, outDir) {
4416
4501
  } finally {
4417
4502
  process.chdir(previousCwd);
4418
4503
  }
4419
- await writeFile(path.join(outDir, P11_MANIFEST_FILE), JSON.stringify({ runtimeVersion: P11_RUNTIME_VERSION }, null, 2));
4504
+ const socialPreviewImage = pageMetadata?.imageSourcePath ? socialPreviewImageForMetadata(pageMetadata, metadataAssetTracker.fileName()) : await firstSocialPreviewImage(outDir);
4505
+ await writeFile(
4506
+ path.join(outDir, P11_MANIFEST_FILE),
4507
+ JSON.stringify(
4508
+ {
4509
+ runtimeVersion: P11_RUNTIME_VERSION,
4510
+ ...pageMetadata?.title ? { title: pageMetadata.title } : {},
4511
+ ...pageMetadata?.description ? { description: pageMetadata.description } : {},
4512
+ ...socialPreviewImage ? { socialPreviewImage } : {}
4513
+ },
4514
+ null,
4515
+ 2
4516
+ )
4517
+ );
4518
+ }
4519
+ function createSocialPreviewAssetTracker(metadata, buildRoot) {
4520
+ let fileName;
4521
+ return {
4522
+ plugin: socialPreviewAssetPlugin(metadata, buildRoot, (nextFileName) => {
4523
+ fileName = nextFileName;
4524
+ }),
4525
+ fileName: () => fileName
4526
+ };
4527
+ }
4528
+ function socialPreviewAssetPlugin(metadata, buildRoot, onAssetPath) {
4529
+ if (!metadata?.imageSourcePath) return null;
4530
+ const metadataImagePath = normalizeFilePath(realpathSync(metadata.imageSourcePath));
4531
+ return {
4532
+ name: "p11-social-preview-asset",
4533
+ generateBundle(_options, bundle) {
4534
+ const matches = Object.values(bundle).filter((output) => {
4535
+ if (output.type !== "asset") return false;
4536
+ return (output.originalFileNames ?? []).some((fileName) => originalFileNameMatches(fileName, metadataImagePath, buildRoot));
4537
+ });
4538
+ if (matches.length === 1) {
4539
+ onAssetPath(matches[0].fileName);
4540
+ return;
4541
+ }
4542
+ if (matches.length > 1) {
4543
+ throw new Error(`metadata.image matched multiple built assets for ${metadata.imageSourcePath}`);
4544
+ }
4545
+ }
4546
+ };
4547
+ }
4548
+ function originalFileNameMatches(fileName, metadataImagePath, buildRoot) {
4549
+ const candidate = path.isAbsolute(fileName) ? fileName : path.resolve(buildRoot, fileName);
4550
+ try {
4551
+ return normalizeFilePath(realpathSync(candidate)) === metadataImagePath;
4552
+ } catch (_error) {
4553
+ return false;
4554
+ }
4555
+ }
4556
+ function normalizeFilePath(filePath) {
4557
+ return path.normalize(filePath).split(path.sep).join("/");
4558
+ }
4559
+ function socialPreviewImageForMetadata(metadata, builtAssetPath) {
4560
+ if (builtAssetPath) return builtAssetPath;
4561
+ throw new Error(`metadata.image was not emitted as a built asset: ${metadata.imageSourcePath}`);
4562
+ }
4563
+ async function firstSocialPreviewImage(outDir) {
4564
+ const imagePaths = await socialPreviewImagesInDir(outDir);
4565
+ return imagePaths.sort()[0];
4566
+ }
4567
+ async function socialPreviewImagesInDir(dir, rootDir = dir) {
4568
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => null);
4569
+ if (!entries) return [];
4570
+ const images = [];
4571
+ for (const entry of entries) {
4572
+ const entryPath = path.join(dir, entry.name);
4573
+ if (entry.isDirectory()) {
4574
+ images.push(...await socialPreviewImagesInDir(entryPath, rootDir));
4575
+ continue;
4576
+ }
4577
+ if (!entry.isFile() || !P11_SOCIAL_PREVIEW_IMAGE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) continue;
4578
+ images.push(path.relative(rootDir, entryPath).split(path.sep).join("/"));
4579
+ }
4580
+ return images;
4420
4581
  }
4421
4582
  function resolveBuildParentDir(buildDir) {
4422
4583
  return path.resolve(buildDir || process.env.P11_BUILD_DIR || tmpdir());
4423
4584
  }
4585
+ function rewriteRelativeModuleSpecifiers(ast, sourceDir) {
4586
+ for (const statement of ast.program.body) {
4587
+ if (statement.type === "ImportDeclaration" || statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration") {
4588
+ const source = statement.source;
4589
+ if (!source || !isRelativeModuleSpecifier(source.value)) continue;
4590
+ source.value = path.resolve(sourceDir, source.value);
4591
+ }
4592
+ }
4593
+ }
4594
+ function isRelativeModuleSpecifier(value) {
4595
+ return value.startsWith("./") || value.startsWith("../");
4596
+ }
4597
+ function extractPageMetadata(ast, inputFile) {
4598
+ const sourceDir = path.dirname(inputFile);
4599
+ const importSources = importedDefaultSpecifiers(ast);
4600
+ for (const statement of ast.program.body) {
4601
+ if (statement.type !== "ExportNamedDeclaration") continue;
4602
+ const declaration = statement.declaration;
4603
+ if (!declaration || declaration.type !== "VariableDeclaration") continue;
4604
+ for (const declarator of declaration.declarations) {
4605
+ if (declarator.id.type !== "Identifier" || declarator.id.name !== "metadata") continue;
4606
+ if (!declarator.init || declarator.init.type !== "ObjectExpression") {
4607
+ throw new Error("metadata export must be an object literal.");
4608
+ }
4609
+ return metadataFromObjectExpression(declarator.init, importSources, sourceDir);
4610
+ }
4611
+ }
4612
+ return null;
4613
+ }
4614
+ function importedDefaultSpecifiers(ast) {
4615
+ const imports = /* @__PURE__ */ new Map();
4616
+ for (const statement of ast.program.body) {
4617
+ if (statement.type !== "ImportDeclaration") continue;
4618
+ for (const specifier of statement.specifiers) {
4619
+ if (specifier.type === "ImportDefaultSpecifier") {
4620
+ imports.set(specifier.local.name, statement.source.value);
4621
+ }
4622
+ }
4623
+ }
4624
+ return imports;
4625
+ }
4626
+ function metadataFromObjectExpression(objectExpression, importSources, sourceDir) {
4627
+ const metadata = {};
4628
+ for (const property of objectExpression.properties) {
4629
+ if (property.type !== "ObjectProperty" || property.computed) continue;
4630
+ const key = objectPropertyName(property.key);
4631
+ if (!key || !["title", "description", "image"].includes(key)) continue;
4632
+ if (key === "title" || key === "description") {
4633
+ metadata[key] = metadataStringValue(property.value, key);
4634
+ continue;
4635
+ }
4636
+ metadata.imageSourcePath = metadataImageSourcePath(property.value, importSources, sourceDir);
4637
+ }
4638
+ return metadata;
4639
+ }
4640
+ function objectPropertyName(key) {
4641
+ if (key.type === "Identifier") return key.name;
4642
+ if (key.type === "StringLiteral") return key.value;
4643
+ return null;
4644
+ }
4645
+ function metadataStringValue(value, key) {
4646
+ if (value.type === "StringLiteral") return value.value.trim();
4647
+ if (value.type === "TemplateLiteral" && value.expressions.length === 0) {
4648
+ return value.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join("").trim();
4649
+ }
4650
+ throw new Error(`metadata.${key} must be a string literal.`);
4651
+ }
4652
+ function metadataImageSourcePath(value, importSources, sourceDir) {
4653
+ if (value.type !== "Identifier") {
4654
+ throw new Error("metadata.image must reference a default-imported PNG, JPEG, or WebP asset.");
4655
+ }
4656
+ const importSource = importSources.get(value.name);
4657
+ if (!importSource) {
4658
+ throw new Error("metadata.image must reference a default-imported PNG, JPEG, or WebP asset.");
4659
+ }
4660
+ if (!P11_SOCIAL_PREVIEW_IMAGE_EXTENSIONS.has(path.extname(importSource).toLowerCase())) {
4661
+ throw new Error("metadata.image must reference a PNG, JPEG, or WebP asset.");
4662
+ }
4663
+ return isRelativeModuleSpecifier(importSource) ? path.resolve(sourceDir, importSource) : importSource;
4664
+ }
4424
4665
  function validatePageSource(code, inputFile = "page.tsx") {
4425
4666
  validatePageAst(parsePageSource(code), inputFile);
4426
4667
  }
@@ -4642,6 +4883,14 @@ function commentsApiUrlForTarget(value, options = {}) {
4642
4883
  if (version !== void 0) url.searchParams.set("v", String(version));
4643
4884
  return url;
4644
4885
  }
4886
+ function replyApiUrlForTarget(value, options = {}) {
4887
+ const target = parseAccessTarget(value, ["read"], "a read URL with /r/<readId> or a read id", { parseVersion: true });
4888
+ const apiUrl = normalizeApiUrl(String(options.apiUrl ?? target.apiUrl ?? defaultApiUrl));
4889
+ const url = new URL(`/api/read/${encodeURIComponent(target.id)}/comments`, apiUrl);
4890
+ const version = options.version ?? target.version;
4891
+ if (version !== void 0) url.searchParams.set("v", String(version));
4892
+ return url;
4893
+ }
4645
4894
  function deleteApiUrlForTarget(value, options = {}) {
4646
4895
  const target = parseEditTarget(value);
4647
4896
  const apiUrl = normalizeApiUrl(String(options.apiUrl ?? target.apiUrl ?? defaultApiUrl));
@@ -4682,6 +4931,9 @@ function validateAccessTarget(target, expected) {
4682
4931
  const prefix = target.kind === "read" ? "read" : "edit";
4683
4932
  if (!new RegExp(`^${prefix}_[A-Za-z0-9_-]{6,80}$`).test(target.id)) throw new Error(`Invalid ${expected}.`);
4684
4933
  }
4934
+ function validateCommentId(commentId) {
4935
+ if (!/^cmt_[A-Za-z0-9_-]{8,64}$/.test(commentId)) throw new Error("Invalid comment id.");
4936
+ }
4685
4937
  function normalizeApiUrl(value) {
4686
4938
  return value.endsWith("/") ? value.slice(0, -1) : value;
4687
4939
  }
@@ -4769,6 +5021,7 @@ export {
4769
5021
  parseReadId,
4770
5022
  publishOutputLines,
4771
5023
  readHistoryEntries,
5024
+ replyApiUrlForTarget,
4772
5025
  resolveBuildParentDir,
4773
5026
  runCli,
4774
5027
  validatePageSource
package/docs/index.md CHANGED
@@ -11,10 +11,13 @@ p11 share <page.tsx>
11
11
  p11 share <page.tsx> --edit-url <editUrl>
12
12
  p11 history
13
13
  p11 comments <readUrl|editUrl|readId|editId>
14
+ p11 reply <readUrl|readId> <commentId> --body "Reply text"
14
15
  p11 delete <editUrl|editId>
15
16
  ```
16
17
 
17
- For `share`, `history`, `comments`, and `delete`, add `--json` when scripting or when exact structured fields are needed.
18
+ Replies are plain text; newlines are preserved. Markdown and HTML are not rendered.
19
+
20
+ For `share`, `history`, `comments`, `reply`, and `delete`, add `--json` when scripting or when exact structured fields are needed.
18
21
 
19
22
  ## Docs Topics
20
23
 
@@ -32,4 +35,4 @@ p11 example all
32
35
  p11 example all --output ./all.tsx
33
36
  ```
34
37
 
35
- Use `p11 share --help`, `p11 comments --help`, `p11 delete --help`, and `p11 history --help` for command-specific flags.
38
+ Use `p11 share --help`, `p11 comments --help`, `p11 reply --help`, `p11 delete --help`, and `p11 history --help` for command-specific flags.
package/docs/sharing.md CHANGED
@@ -26,6 +26,16 @@ p11 comments <readUrl|editUrl|readId|editId> --version 1
26
26
  p11 comments <readUrl|editUrl|readId|editId> --output comments.json
27
27
  ```
28
28
 
29
+ Reply to a top-level comment:
30
+
31
+ ```bash
32
+ p11 reply <readUrl|readId> <commentId> --body "Reply text"
33
+ p11 reply <readUrl|readId> <commentId> --body-file reply.txt
34
+ p11 reply <readUrl|readId> <commentId> --body-file - --json
35
+ ```
36
+
37
+ Replies are plain text; newlines are preserved. Markdown and HTML are not rendered.
38
+
29
39
  Delete a shared document and all of its versions, comments, and replies:
30
40
 
31
41
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p11-core/cli",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "bin": {