html-collab 0.1.0 → 0.1.1

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.
Files changed (3) hide show
  1. package/README.md +19 -7
  2. package/dist/cli.js +113 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,7 +46,8 @@ revision can be done by a human or an agent.
46
46
  - Suggested edits can be replacements, inserts, or deletions. Toggle between
47
47
  the tracked-change markup and a clean preview.
48
48
  - Autosave back to the same local file when a Chromium browser asks for
49
- permission. Until you choose a local file, changes live only in the tab.
49
+ permission. Safari and Firefox keep changes in the tab until you export or
50
+ save another way.
50
51
 
51
52
  Send your edited file to anyone else with `html-collab merge` to combine
52
53
  copies, and `html-collab extract` to pull out a brief.
@@ -61,6 +62,10 @@ Requires Node.js 18+ and npm. No clone or install step is needed:
61
62
  npx html-collab wrap report.html --out report.review.html
62
63
  ```
63
64
 
65
+ The command writes a self-contained review file and prints a short summary.
66
+ Small source files grow by roughly 90 KB because the review UI is embedded
67
+ inside the HTML.
68
+
64
69
  Open `report.review.html` in your browser, mark it up, then export a brief:
65
70
 
66
71
  ```sh
@@ -78,7 +83,7 @@ you want another round.
78
83
 
79
84
  ```sh
80
85
  npx html-collab wrap report.html --out report.review.html
81
- # Open report.review.html in any browser. Mark it up. Click Brief.
86
+ # Open report.review.html in your browser. Mark it up. Click Brief.
82
87
  npx html-collab extract report.review.html --format markdown --out brief.md
83
88
  ```
84
89
 
@@ -108,6 +113,8 @@ review file when Autosave asks where to save.
108
113
  ## CLI reference
109
114
 
110
115
  ```
116
+ html-collab --help
117
+ html-collab --version
111
118
  html-collab wrap <input.html> --out <output.review.html>
112
119
  html-collab unwrap <input.review.html> [--apply-edits] --out <output.html>
113
120
  html-collab merge <a.review.html> <b.review.html> [more...] --out <merged.review.html>
@@ -116,12 +123,15 @@ html-collab skill --out html-collab.SKILL.md
116
123
  ```
117
124
 
118
125
  Use `npx html-collab ...` if you have not installed the CLI globally.
119
- The `skill` command writes an optional agent workflow file for tools that
120
- support local skills.
126
+ The browser Brief button exports markdown. For JSON, plain text, or an
127
+ agent-oriented plan, use `html-collab extract --format <json|text|agent>`.
128
+ The `skill` command writes an optional agent workflow file you can drop into
129
+ a local skills directory or paste into an agent prompt.
121
130
 
122
- `unwrap --apply-edits` writes the final document with accepted edits applied.
123
- Ambiguous edits fail instead of guessing, so the output is never silently
124
- wrong.
131
+ `unwrap --apply-edits` only applies suggested edits that have been accepted
132
+ in the browser. Open, rejected, and deleted edits are skipped and reported in
133
+ the command summary. Ambiguous edits fail instead of guessing, so the output
134
+ is never silently wrong.
125
135
 
126
136
  ## How it works
127
137
 
@@ -134,6 +144,8 @@ panel shows.
134
144
 
135
145
  For mission, principles, and workflows, see the
136
146
  [project notes](https://github.com/glendigity/html-collab/blob/main/docs/README.md).
147
+ For a fresh external sanity check, use the
148
+ [independent user test prompt](https://github.com/glendigity/html-collab/blob/main/USER_TEST_PROMPT.md).
137
149
 
138
150
  ## Status
139
151
 
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { realpathSync } from "node:fs";
5
- import { basename as basename2 } from "node:path";
4
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
5
+ import { basename as basename2, dirname as dirname3, join as join2 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7
7
 
8
8
  // src/commands/extract.ts
@@ -2987,6 +2987,7 @@ function renderReviewShell(source, state) {
2987
2987
  <meta charset="utf-8">
2988
2988
  <meta name="viewport" content="width=device-width, initial-scale=1">
2989
2989
  <meta name="generator" content="html-collab">
2990
+ <link rel="icon" href="data:,">
2990
2991
  <link rel="author" href="${PROJECT_URL}">
2991
2992
  <title>${escapeHtml(state.title ?? "Reviewable HTML")}</title>
2992
2993
  <style>
@@ -3845,6 +3846,12 @@ async function mergeFiles(inputPaths, outputPath) {
3845
3846
  const result = mergeReviewStates(states);
3846
3847
  const mergedHtml = createReviewHtmlFromParts(source, result.state);
3847
3848
  await writeFile2(outputPath, mergedHtml, "utf8");
3849
+ return {
3850
+ addedOps: result.addedOps,
3851
+ addedActors: result.addedActors,
3852
+ totalOps: result.state.ops.length,
3853
+ reviewBytes: Buffer.byteLength(mergedHtml, "utf8")
3854
+ };
3848
3855
  }
3849
3856
 
3850
3857
  // src/commands/skill.ts
@@ -3890,7 +3897,8 @@ import { readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises
3890
3897
 
3891
3898
  // src/format/apply-edits.ts
3892
3899
  function applyAcceptedEdits(sourceHtml, state) {
3893
- const accepted = reduceReviewState(state).edits.filter((edit) => edit.status === "accepted");
3900
+ const reduced = reduceReviewState(state);
3901
+ const accepted = reduced.edits.filter((edit) => edit.status === "accepted");
3894
3902
  const planned = accepted.map((edit) => planEdit(sourceHtml, edit));
3895
3903
  planned.sort((left, right) => right.start - left.start);
3896
3904
  let html = sourceHtml;
@@ -3899,7 +3907,13 @@ function applyAcceptedEdits(sourceHtml, state) {
3899
3907
  }
3900
3908
  return {
3901
3909
  html,
3902
- appliedEdits: planned.length
3910
+ appliedEdits: planned.length,
3911
+ totalSuggestedEdits: reduced.edits.length,
3912
+ acceptedEdits: accepted.length,
3913
+ openEdits: reduced.edits.filter((edit) => edit.status === "open").length,
3914
+ rejectedEdits: reduced.edits.filter((edit) => edit.status === "rejected").length,
3915
+ deletedEdits: reduced.edits.filter((edit) => edit.status === "deleted").length,
3916
+ skippedEdits: reduced.edits.length - accepted.length
3903
3917
  };
3904
3918
  }
3905
3919
  function planEdit(sourceHtml, edit) {
@@ -3947,12 +3961,21 @@ async function unwrapFile(inputPath, outputPath, options = {}) {
3947
3961
  const sourceBytes = unwrapReviewHtml(reviewHtml);
3948
3962
  if (!options.applyAcceptedEdits) {
3949
3963
  await writeFile4(outputPath, sourceBytes);
3950
- return {};
3964
+ return { sourceBytes: sourceBytes.byteLength };
3951
3965
  }
3952
3966
  const state = extractReviewState(reviewHtml);
3953
3967
  const result = applyAcceptedEdits(sourceBytes.toString("utf8"), state);
3954
3968
  await writeFile4(outputPath, result.html, "utf8");
3955
- return { appliedEdits: result.appliedEdits };
3969
+ return {
3970
+ sourceBytes: Buffer.byteLength(result.html, "utf8"),
3971
+ appliedEdits: result.appliedEdits,
3972
+ totalSuggestedEdits: result.totalSuggestedEdits,
3973
+ acceptedEdits: result.acceptedEdits,
3974
+ openEdits: result.openEdits,
3975
+ rejectedEdits: result.rejectedEdits,
3976
+ deletedEdits: result.deletedEdits,
3977
+ skippedEdits: result.skippedEdits
3978
+ };
3956
3979
  }
3957
3980
 
3958
3981
  // src/commands/wrap.ts
@@ -3961,18 +3984,36 @@ async function wrapFile(inputPath, outputPath) {
3961
3984
  const sourceBytes = await readFile5(inputPath);
3962
3985
  const reviewHtml = createReviewHtml(sourceBytes, { sourcePath: inputPath });
3963
3986
  await writeFile5(outputPath, reviewHtml, "utf8");
3987
+ return {
3988
+ sourceBytes: sourceBytes.byteLength,
3989
+ reviewBytes: Buffer.byteLength(reviewHtml, "utf8")
3990
+ };
3964
3991
  }
3965
3992
 
3966
3993
  // src/cli.ts
3967
3994
  async function main(argv = process.argv) {
3968
3995
  try {
3969
- const command = parseArgs(argv.slice(2));
3996
+ const args = argv.slice(2);
3997
+ const commandName = basename2(argv[1] ?? "html-collab");
3998
+ if (args[0] === "--help" || args[0] === "-h") {
3999
+ process.stdout.write(usage(commandName) + `
4000
+ `);
4001
+ return;
4002
+ }
4003
+ if (args[0] === "--version" || args[0] === "-v") {
4004
+ process.stdout.write(packageVersion() + `
4005
+ `);
4006
+ return;
4007
+ }
4008
+ const command = parseArgs(args);
3970
4009
  if (command.command === "wrap") {
3971
- await wrapFile(command.inputPath, command.outputPath);
4010
+ const result2 = await wrapFile(command.inputPath, command.outputPath);
4011
+ writeStatus(`Wrote ${command.outputPath} (${formatBytes(result2.reviewBytes)} review file from ${formatBytes(result2.sourceBytes)} source).`);
3972
4012
  return;
3973
4013
  }
3974
4014
  if (command.command === "merge") {
3975
- await mergeFiles(command.inputPaths, command.outputPath);
4015
+ const result2 = await mergeFiles(command.inputPaths, command.outputPath);
4016
+ writeStatus(`Merged ${command.inputPaths.length} files into ${command.outputPath} (${formatBytes(result2.reviewBytes)}, ${result2.totalOps} ${plural(result2.totalOps, "operation")}, ${result2.addedOps} new).`);
3976
4017
  return;
3977
4018
  }
3978
4019
  if (command.command === "extract") {
@@ -3982,6 +4023,8 @@ async function main(argv = process.argv) {
3982
4023
  });
3983
4024
  if (!command.outputPath) {
3984
4025
  process.stdout.write(output);
4026
+ } else {
4027
+ writeStatus(`Wrote ${command.outputPath} (${command.format}, ${formatBytes(Buffer.byteLength(output, "utf8"))}).`);
3985
4028
  }
3986
4029
  return;
3987
4030
  }
@@ -3989,14 +4032,18 @@ async function main(argv = process.argv) {
3989
4032
  const output = await skillFile(command.outputPath);
3990
4033
  if (!command.outputPath) {
3991
4034
  process.stdout.write(output);
4035
+ } else {
4036
+ writeStatus(`Wrote ${command.outputPath} (${formatBytes(Buffer.byteLength(output, "utf8"))}).`);
3992
4037
  }
3993
4038
  return;
3994
4039
  }
3995
4040
  const result = await unwrapFile(command.inputPath, command.outputPath, {
3996
4041
  applyAcceptedEdits: command.applyAcceptedEdits
3997
4042
  });
3998
- if (command.applyAcceptedEdits && result.appliedEdits === 0) {
3999
- console.warn("No accepted edits to apply; wrote the original source HTML.");
4043
+ if (command.applyAcceptedEdits) {
4044
+ writeStatus(applyEditsStatus(command.outputPath, result));
4045
+ } else {
4046
+ writeStatus(`Wrote ${command.outputPath} (${formatBytes(result.sourceBytes)}, original source HTML).`);
4000
4047
  }
4001
4048
  } catch (error) {
4002
4049
  const message = error instanceof Error ? error.message : String(error);
@@ -4132,6 +4179,8 @@ function isExtractFormat(value) {
4132
4179
  }
4133
4180
  function usage(commandName) {
4134
4181
  return `Usage:
4182
+ ${commandName} --help
4183
+ ${commandName} --version
4135
4184
  ${commandName} wrap report.html --out report.review.html
4136
4185
  ${commandName} merge glen.review.html maya.review.html --out merged.review.html
4137
4186
  ${commandName} extract merged.review.html --format markdown --out review-brief.md
@@ -4141,6 +4190,59 @@ function usage(commandName) {
4141
4190
  ${commandName} unwrap report.review.html --out report.final.html
4142
4191
  ${commandName} unwrap report.review.html --apply-edits --out report.final.html`;
4143
4192
  }
4193
+ function writeStatus(message) {
4194
+ console.error(message);
4195
+ }
4196
+ function applyEditsStatus(outputPath, result) {
4197
+ const applied = result.appliedEdits ?? 0;
4198
+ const skipped = result.skippedEdits ?? 0;
4199
+ const skippedStatus = skipped > 0 ? `; skipped ${formatSkippedEdits(result)}` : "";
4200
+ const output = `Wrote ${outputPath} (${formatBytes(result.sourceBytes)}).`;
4201
+ if (applied === 0) {
4202
+ return `Applied 0 accepted edits${skippedStatus}. ${output}`;
4203
+ }
4204
+ return `Applied ${applied} accepted ${plural(applied, "edit")}${skippedStatus}. ${output}`;
4205
+ }
4206
+ function formatSkippedEdits(result) {
4207
+ const parts = [
4208
+ countLabel(result.openEdits ?? 0, "open edit"),
4209
+ countLabel(result.rejectedEdits ?? 0, "rejected edit"),
4210
+ countLabel(result.deletedEdits ?? 0, "deleted edit")
4211
+ ].filter(Boolean);
4212
+ return parts.length > 0 ? parts.join(", ") : "0 edits";
4213
+ }
4214
+ function countLabel(count, label) {
4215
+ return count > 0 ? `${count} ${plural(count, label)}` : undefined;
4216
+ }
4217
+ function plural(count, singular) {
4218
+ return count === 1 ? singular : `${singular}s`;
4219
+ }
4220
+ function formatBytes(bytes) {
4221
+ if (bytes < 1024) {
4222
+ return `${bytes} B`;
4223
+ }
4224
+ const kib = bytes / 1024;
4225
+ if (kib < 1024) {
4226
+ return `${kib.toFixed(kib >= 10 ? 0 : 1)} KB`;
4227
+ }
4228
+ const mib = kib / 1024;
4229
+ return `${mib.toFixed(mib >= 10 ? 0 : 1)} MB`;
4230
+ }
4231
+ function packageVersion() {
4232
+ let current = dirname3(fileURLToPath2(import.meta.url));
4233
+ while (true) {
4234
+ const candidate = join2(current, "package.json");
4235
+ if (existsSync(candidate)) {
4236
+ const packageJson = JSON.parse(readFileSync(candidate, "utf8"));
4237
+ return packageJson.version ?? "0.0.0";
4238
+ }
4239
+ const parent = dirname3(current);
4240
+ if (parent === current) {
4241
+ return "0.0.0";
4242
+ }
4243
+ current = parent;
4244
+ }
4245
+ }
4144
4246
  if (isDirectRun()) {
4145
4247
  await main(process.argv);
4146
4248
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "html-collab",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Turn any single-file HTML report into a portable, reviewable document. Comments, suggested edits, and deterministic merge — all stored inside the HTML file itself.",
5
5
  "type": "module",
6
6
  "engines": {