@hraness/wordcell 0.22.5 → 0.24.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.
Files changed (78) hide show
  1. package/README.md +67 -18
  2. package/dist/authoring.js +2 -2
  3. package/dist/{authority-3ga47syb.js → authority-35cbvp1x.js} +2 -2
  4. package/dist/benchmark.js +3 -3
  5. package/dist/capture.js +6 -6
  6. package/dist/cli.js +26 -24
  7. package/dist/clip/acquire.js +3 -3
  8. package/dist/clip/args.js +1 -1
  9. package/dist/clip/cli.js +6 -6
  10. package/dist/clip/doctor.js +4 -4
  11. package/dist/evaluation-builder.js +10 -10
  12. package/dist/evaluation-kb.js +10 -10
  13. package/dist/graph-authority.js +4 -4
  14. package/dist/graph-percolation.js +6 -6
  15. package/dist/graph.js +3 -1
  16. package/dist/{index-bcknqxrq.js → index-0eacgvpv.js} +9 -1
  17. package/dist/{index-f75zmmdr.js → index-1er88ckw.js} +4 -4
  18. package/dist/{index-8v6k9h4r.js → index-1s8mc4hz.js} +1 -1
  19. package/dist/{index-n9kwenpt.js → index-1w0sp1q7.js} +1 -1
  20. package/dist/{index-de2w8crk.js → index-2fmnttca.js} +82 -15
  21. package/dist/{index-8bgnhjgq.js → index-44kw9tr8.js} +3867 -162
  22. package/dist/{index-42tb8qqp.js → index-5d1rd5v9.js} +444 -9
  23. package/dist/{index-aer0jdrq.js → index-ahyhryb8.js} +1 -1
  24. package/dist/{index-f7fnww7a.js → index-b0khj0h5.js} +164 -9
  25. package/dist/{index-qdb8f8va.js → index-bdwcjvr4.js} +1 -1
  26. package/dist/{index-dfag79p7.js → index-byz4kzww.js} +3 -0
  27. package/dist/{index-hgve9rh2.js → index-cd75vky9.js} +96 -1
  28. package/dist/{index-h2qn3y3f.js → index-f184qyh1.js} +4 -4
  29. package/dist/{index-n57tewfr.js → index-fp732bgg.js} +2 -2
  30. package/dist/{index-zy7an84p.js → index-jvb7w0gg.js} +5 -2
  31. package/dist/{index-f984hw45.js → index-k86wepd8.js} +1 -1
  32. package/dist/{index-1r5rnkfr.js → index-mvs6f7ne.js} +2 -2
  33. package/dist/{index-p96kb6t2.js → index-nn67kzwa.js} +1 -1
  34. package/dist/{index-fc4dr114.js → index-nwrehke7.js} +1 -1
  35. package/dist/{index-pz2b2x0y.js → index-pgtm2nhf.js} +1 -1
  36. package/dist/{index-054mb7d3.js → index-qfqjfxaa.js} +10 -9
  37. package/dist/{index-xzvcw9ga.js → index-r0m5taz2.js} +2 -2
  38. package/dist/{index-gtwqye5a.js → index-tcaq1c7f.js} +4 -4
  39. package/dist/{index-g5vsqmdy.js → index-xwxy71ew.js} +190 -4
  40. package/dist/index.js +15 -13
  41. package/dist/pdf.js +3 -3
  42. package/dist/percolate.js +2 -2
  43. package/dist/portfolio.js +10 -10
  44. package/dist/publish.js +3 -3
  45. package/dist/sdk.js +9 -9
  46. package/dist/search.js +2 -2
  47. package/dist/semantic.js +4 -4
  48. package/dist/{support-jf9v10j7.js → support-1yhv6vm6.js} +25 -23
  49. package/dist/url-intelligence.js +1 -1
  50. package/dist/workflows/decision-context.js +10 -10
  51. package/dist/workflows/index.js +10 -10
  52. package/package.json +12 -4
  53. package/skills/wordcell/AGENTS.md +1 -0
  54. package/skills/wordcell/SKILL.md +5 -3
  55. package/skills/wordcell/references/query.md +52 -4
  56. package/skills/wordcell/references/session-memory.md +207 -0
  57. package/src/authoring-import.ts +32 -0
  58. package/src/authoring-model.ts +185 -2
  59. package/src/authoring-program.ts +19 -7
  60. package/src/cli-help.ts +894 -0
  61. package/src/cli-intro.ts +1 -1
  62. package/src/cli-program.ts +612 -83
  63. package/src/cli-style.ts +132 -0
  64. package/src/clip/acquire.ts +20 -0
  65. package/src/clip/args.ts +3 -0
  66. package/src/clip/cli.ts +82 -5
  67. package/src/clip/cookie-permission.ts +241 -0
  68. package/src/clip/url-metadata-cli.ts +6 -5
  69. package/src/clip/url-metadata.ts +14 -0
  70. package/src/graph.ts +9 -4
  71. package/src/import-supermemory.ts +1629 -0
  72. package/src/mcp-server.ts +415 -0
  73. package/src/mcp-tools.ts +1138 -0
  74. package/src/pdf/cli.ts +5 -4
  75. package/src/sdk.ts +149 -1
  76. package/src/selected-passage.ts +409 -0
  77. package/src/support-profile.ts +1 -1
  78. package/src/support.ts +3 -0
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { posix, relative, resolve, sep } from "node:path";
3
3
  import { Document, isMap, isScalar, isSeq, parseDocument, type YAMLMap, type YAMLSeq } from "yaml";
4
4
  import { type NoteLockOptions } from "./note-lock.js";
5
- import { isCanonicalNoteId, isCanonicalRelationPredicate } from "./graph.js";
5
+ import { isCanonicalNoteId, isCanonicalRelationPredicate, isMetadataNumber } from "./graph.js";
6
6
  import { parseDocumentId, parseQualifiedDocumentUri } from "./portfolio-identity.js";
7
7
 
8
8
  export const MAX_NOTE_BYTES = 16 * 1024 * 1024;
@@ -721,12 +721,188 @@ export function renderUpdatedNoteBody(
721
721
  return `${header}${parts.newline}${parts.newline}${normalized}`;
722
722
  }
723
723
 
724
- export function renderCreatedNote(input: CreateNoteInput, documentId: string): string {
724
+ /**
725
+ * Extra top-level frontmatter values that internal writers, such as the
726
+ * supermemory importer, own. Public authoring never passes them.
727
+ */
728
+ export type FrontmatterScalar = string | number | boolean;
729
+ export type FrontmatterFieldValue =
730
+ | FrontmatterScalar
731
+ | readonly string[]
732
+ | Readonly<Record<string, FrontmatterScalar>>;
733
+ export type FrontmatterFields = Readonly<Record<string, FrontmatterFieldValue>>;
734
+ /** Field updates; `null` deletes the key. `title` is allowed here. */
735
+ export type FrontmatterFieldUpdates = Readonly<Record<string, FrontmatterFieldValue | null>>;
736
+
737
+ export const MAX_FRONTMATTER_FIELDS = 64;
738
+ export const MAX_FRONTMATTER_LIST_ITEMS = 256;
739
+ const FRONTMATTER_FIELD_KEY = /^[a-z][a-z0-9_]{0,63}$/u;
740
+ const FRONTMATTER_MAP_KEY = /^[A-Za-z0-9_.:-]{1,64}$/u;
741
+ const RESERVED_FRONTMATTER_FIELDS: ReadonlySet<string> = new Set([
742
+ "document_id", "type", "title", "tags", "relations",
743
+ ]);
744
+
745
+ function validateFieldString(value: unknown, label: string): string {
746
+ if (typeof value !== "string") throw new TypeError(`frontmatter field ${label} must be a string`);
747
+ if (
748
+ Buffer.from(value, "utf8").toString("utf8") !== value
749
+ || /[\u0000-\u001f\u007f]/u.test(value)
750
+ ) {
751
+ throw new TypeError(`frontmatter field ${label} must be a single line of well-formed text`);
752
+ }
753
+ return value;
754
+ }
755
+
756
+ function validateFieldScalar(value: unknown, label: string): FrontmatterScalar {
757
+ if (typeof value === "boolean") return value;
758
+ if (typeof value === "number") {
759
+ if (!isMetadataNumber(value)) {
760
+ throw new TypeError(`frontmatter field ${label} must be a finite number and a safe integer when integral`);
761
+ }
762
+ return value;
763
+ }
764
+ return validateFieldString(value, label);
765
+ }
766
+
767
+ function validateFieldValue(value: unknown, key: string): FrontmatterFieldValue {
768
+ if (Array.isArray(value)) {
769
+ if (value.length > MAX_FRONTMATTER_LIST_ITEMS) {
770
+ throw new TypeError(`frontmatter field ${key} has too many items`);
771
+ }
772
+ return value.map((item, index) => validateFieldString(item, `${key}[${index}]`));
773
+ }
774
+ if (typeof value === "object" && value !== null) {
775
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
776
+ throw new TypeError(`frontmatter field ${key} must be a plain map`);
777
+ }
778
+ const entries = Object.entries(value);
779
+ if (entries.length > MAX_FRONTMATTER_FIELDS) {
780
+ throw new TypeError(`frontmatter field ${key} has too many keys`);
781
+ }
782
+ const result: Record<string, FrontmatterScalar> = {};
783
+ for (const [nested, item] of entries) {
784
+ if (!FRONTMATTER_MAP_KEY.test(nested) || nested === "__proto__") {
785
+ throw new TypeError(`frontmatter field ${key} has an invalid key: ${JSON.stringify(nested)}`);
786
+ }
787
+ result[nested] = validateFieldScalar(item, `${key}.${nested}`);
788
+ }
789
+ return result;
790
+ }
791
+ return validateFieldScalar(value, key);
792
+ }
793
+
794
+ function validateFieldKey(key: string, allowTitle: boolean): void {
795
+ if (!FRONTMATTER_FIELD_KEY.test(key)) {
796
+ throw new TypeError(`not a valid frontmatter field name: ${JSON.stringify(key)}`);
797
+ }
798
+ if (RESERVED_FRONTMATTER_FIELDS.has(key) && !(allowTitle && key === "title")) {
799
+ throw new TypeError(`frontmatter field ${key} is reserved`);
800
+ }
801
+ }
802
+
803
+ /** Validate importer-owned fields in their given key order. */
804
+ export function validateFrontmatterFields(fields: FrontmatterFields | undefined): FrontmatterFields {
805
+ if (fields === undefined) return {};
806
+ const entries = Object.entries(fields);
807
+ if (entries.length > MAX_FRONTMATTER_FIELDS) throw new TypeError("too many frontmatter fields");
808
+ const result: Record<string, FrontmatterFieldValue> = {};
809
+ for (const [key, value] of entries) {
810
+ validateFieldKey(key, false);
811
+ result[key] = validateFieldValue(value, key);
812
+ }
813
+ return result;
814
+ }
815
+
816
+ /** Validate field updates; `title` must pass `validateTitle` and `null` deletes. */
817
+ export function validateFrontmatterFieldUpdates(
818
+ fields: FrontmatterFieldUpdates | undefined,
819
+ ): FrontmatterFieldUpdates {
820
+ if (fields === undefined) return {};
821
+ const entries = Object.entries(fields);
822
+ if (entries.length > MAX_FRONTMATTER_FIELDS) throw new TypeError("too many frontmatter fields");
823
+ const result: Record<string, FrontmatterFieldValue | null> = {};
824
+ for (const [key, value] of entries) {
825
+ validateFieldKey(key, true);
826
+ if (key === "title") {
827
+ if (typeof value !== "string") throw new TypeError("frontmatter field title must be a string");
828
+ result[key] = validateTitle(value);
829
+ } else {
830
+ result[key] = value === null ? null : validateFieldValue(value, key);
831
+ }
832
+ }
833
+ return result;
834
+ }
835
+
836
+ function canonicalJson(value: unknown): string {
837
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
838
+ if (typeof value === "object" && value !== null) {
839
+ const keys = Object.keys(value).sort();
840
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson((value as Record<string, unknown>)[key])}`).join(",")}}`;
841
+ }
842
+ return JSON.stringify(value) ?? "undefined";
843
+ }
844
+
845
+ /** The plain value of one top-level key, or `undefined` when absent. */
846
+ export function topLevelValue(parts: FrontmatterParts, key: string): unknown {
847
+ const contents = parts.document.contents;
848
+ if (!isMap(contents) || !contents.has(key)) return undefined;
849
+ const node: unknown = contents.get(key, true);
850
+ if (typeof node === "object" && node !== null && "toJSON" in node && typeof node.toJSON === "function") {
851
+ return (node as { toJSON: () => unknown }).toJSON();
852
+ }
853
+ return node;
854
+ }
855
+
856
+ export function sameFrontmatterValue(present: unknown, requested: unknown): boolean {
857
+ return canonicalJson(present) === canonicalJson(requested);
858
+ }
859
+
860
+ /**
861
+ * Replace the body and set or delete top-level fields in one rendered write.
862
+ * With no fields this is exactly `renderUpdatedNoteBody`.
863
+ */
864
+ export function renderUpdatedNoteBodyAndFields(
865
+ snapshot: Pick<NoteSnapshot, "content" | "relativePath">,
866
+ parts: FrontmatterParts,
867
+ body: string,
868
+ fields: FrontmatterFieldUpdates | undefined,
869
+ ): string {
870
+ const updates = validateFrontmatterFieldUpdates(fields);
871
+ const bodyOnly = renderUpdatedNoteBody(snapshot, parts, body);
872
+ if (Object.keys(updates).length === 0) return bodyOnly;
873
+ if (!parts.hadFrontmatter || !isMap(parts.document.contents)) {
874
+ throw new TypeError("frontmatter fields require a note with a frontmatter map");
875
+ }
876
+ const contents = parts.document.contents;
877
+ for (const [key, value] of Object.entries(updates)) {
878
+ if (value === null) {
879
+ contents.delete(key);
880
+ } else if (!sameFrontmatterValue(topLevelValue(parts, key), value)) {
881
+ contents.set(key, parts.document.createNode(value));
882
+ }
883
+ }
884
+ const content = renderFrontmatter({
885
+ ...parts,
886
+ bodySuffix: `${parts.newline}${parts.newline}${normalizedRequestedBody(body)}`,
887
+ });
888
+ if (Buffer.byteLength(content, "utf8") > MAX_NOTE_BYTES) {
889
+ throw new RangeError("the note is too large for bounded authoring");
890
+ }
891
+ return content;
892
+ }
893
+
894
+ export function renderCreatedNote(
895
+ input: CreateNoteInput,
896
+ documentId: string,
897
+ fields?: FrontmatterFields,
898
+ ): string {
725
899
  const title = validateTitle(input.title);
726
900
  const type = validateType(input.type);
727
901
  const tags = validateTags(input.tags);
902
+ const extra = validateFrontmatterFields(fields);
728
903
  const metadata: Record<string, unknown> = { document_id: documentId, type, title };
729
904
  if (tags.length > 0) metadata["tags"] = tags;
905
+ for (const [key, value] of Object.entries(extra)) metadata[key] = value;
730
906
  const document = new Document(metadata, { schema: "core" });
731
907
  const body = normalizedRequestedBody(input.body ?? `# ${title}\n`);
732
908
  return `---\n${document.toString({ lineWidth: 0 })}---\n\n${body}`;
@@ -786,8 +962,10 @@ export function assertCompatibleCreate(
786
962
  snapshot: NoteSnapshot,
787
963
  input: CreateNoteInput,
788
964
  requestedDocumentId: string | undefined,
965
+ fields?: FrontmatterFields,
789
966
  ): CompatibleCreate {
790
967
  const parts = frontmatter(snapshot.content, snapshot.relativePath);
968
+ const requestedFields = validateFrontmatterFields(fields);
791
969
  const requestedType = validateType(input.type);
792
970
  const requestedTitle = validateTitle(input.title);
793
971
  if (topLevelScalar(parts, "type") !== requestedType) {
@@ -810,6 +988,11 @@ export function assertCompatibleCreate(
810
988
  ) {
811
989
  throw new NoteAlreadyExistsError(snapshot.relativePath, "body differs");
812
990
  }
991
+ for (const [key, value] of Object.entries(requestedFields)) {
992
+ if (!sameFrontmatterValue(topLevelValue(parts, key), value)) {
993
+ throw new NoteAlreadyExistsError(snapshot.relativePath, `${key} differs`);
994
+ }
995
+ }
813
996
  const existingId = existingDocumentId(parts);
814
997
  if (
815
998
  requestedDocumentId !== undefined
@@ -4,12 +4,12 @@ import {
4
4
  MAX_NOTE_BYTES, NoteRevisionConflictError, addRelationToParts, assertCompatibleCreate,
5
5
  assertExpected, canonicalNoteId, canonicalRelationTarget, checkedExpectedRevision,
6
6
  existingDocumentId, frontmatter, isErrno, normalizeRelationPredicate, noteResult, pathFor, relationsFromParts,
7
- removeRelationFromParts, renderCreatedNote, renderFrontmatter, renderUpdatedNoteBody, requireRevision, revisionFor,
8
- sameQuarantinedSnapshot, sameSnapshot, withRecoveryPath,
7
+ removeRelationFromParts, renderCreatedNote, renderFrontmatter, renderUpdatedNoteBodyAndFields, requireRevision, revisionFor,
8
+ sameQuarantinedSnapshot, sameSnapshot, validateFrontmatterFieldUpdates, validateFrontmatterFields, withRecoveryPath,
9
9
  } from "./authoring-model.js";
10
10
  import type {
11
11
  AuthoringDependencies, AuthoringInstallContext, AuthoringOptions, CreateNoteInput,
12
- DirectoryIdentity, NoteAuthoringResult, NoteRevision, NoteSnapshot, RecoveryLocation, UpdateNoteBodyOptions, Vault,
12
+ DirectoryIdentity, FrontmatterFieldUpdates, FrontmatterFields, NoteAuthoringResult, NoteRevision, NoteSnapshot, RecoveryLocation, UpdateNoteBodyOptions, Vault,
13
13
  } from "./authoring-model.js";
14
14
  import type { NoteLock } from "./note-lock.js";
15
15
  import { parseDocumentId } from "./portfolio-identity.js";
@@ -211,12 +211,18 @@ function installNote(
211
211
  });
212
212
  }
213
213
 
214
+ /**
215
+ * `fields` is internal: extra top-level frontmatter keys owned by an importer.
216
+ * The public `createNote` never passes it.
217
+ */
214
218
  export function createNoteProgram(
215
219
  platform: AuthoringPlatform, root: string, input: CreateNoteInput, options: AuthoringOptions,
220
+ fields?: FrontmatterFields,
216
221
  ): Effect.Effect<NoteAuthoringResult, AuthoringFailure> {
217
222
  return Effect.gen(function*() {
218
223
  const vault = yield* authoringNative(() => platform.resolveVault(root));
219
224
  const id = yield* authoringSync(() => canonicalNoteId(input.id));
225
+ const extra = yield* authoringSync(() => validateFrontmatterFields(fields));
220
226
  const requestedDocumentId = yield* authoringSync(() => input.documentId === undefined ? undefined : parseDocumentId(input.documentId));
221
227
  const expected = yield* authoringSync(() => checkedExpectedRevision(options));
222
228
  const dependencies = yield* authoringSync(() => platform.dependenciesFor(options.dependencies));
@@ -227,12 +233,12 @@ export function createNoteProgram(
227
233
  const existing = yield* authoringNative(() => platform.readOptionalSnapshot(vault, id));
228
234
  if (existing !== null) {
229
235
  yield* authoringSync(() => assertExpected(existing, expected));
230
- const compatible = yield* authoringSync(() => assertCompatibleCreate(existing, input, requestedDocumentId));
236
+ const compatible = yield* authoringSync(() => assertCompatibleCreate(existing, input, requestedDocumentId, extra));
231
237
  return yield* authoringSync(() => noteResult(existing, compatible.relations, false, compatible.documentId));
232
238
  }
233
239
  if (expected !== undefined) return yield* fail(new NoteRevisionConflictError(`${id}.md`, expected, null));
234
240
  const documentId = yield* authoringSync(() => requestedDocumentId ?? parseDocumentId(dependencies.documentId()));
235
- const content = yield* authoringSync(() => renderCreatedNote(input, documentId));
241
+ const content = yield* authoringSync(() => renderCreatedNote(input, documentId, extra));
236
242
  const revision = yield* installNote(platform, vault, id, content, null, lock, dependencies);
237
243
  return { changed: true, path: `${id}.md`, revision, relations: [], documentId };
238
244
  }),
@@ -277,12 +283,18 @@ export function editNoteRelationProgram(
277
283
  });
278
284
  }
279
285
 
286
+ /**
287
+ * `fields` is internal: top-level frontmatter keys (and `title`) set in the
288
+ * same atomic write, where `null` deletes a key. The public `updateNoteBody`
289
+ * never passes it, so the authored frontmatter bytes stay untouched there.
290
+ */
280
291
  export function updateNoteBodyProgram(
281
292
  platform: AuthoringPlatform, root: string, idInput: string, body: string,
282
- options: UpdateNoteBodyOptions,
293
+ options: UpdateNoteBodyOptions, fields?: FrontmatterFieldUpdates,
283
294
  ): Effect.Effect<NoteAuthoringResult, AuthoringFailure> {
284
295
  return Effect.gen(function*() {
285
296
  const id = yield* authoringSync(() => canonicalNoteId(idInput));
297
+ const updates = yield* authoringSync(() => validateFrontmatterFieldUpdates(fields));
286
298
  const expected = yield* authoringSync(() => {
287
299
  if (typeof options?.expectedRevision !== "string") {
288
300
  throw new TypeError("expectedRevision is required for a note body update");
@@ -302,7 +314,7 @@ export function updateNoteBodyProgram(
302
314
  if (identity.kind === "invalid") return yield* fail(new TypeError("the note has an invalid document_id"));
303
315
  const documentId = identity.kind === "valid" ? identity.documentId : undefined;
304
316
  const relations = yield* authoringSync(() => relationsFromParts(parts, source.relativePath));
305
- const content = yield* authoringSync(() => renderUpdatedNoteBody(source, parts, body));
317
+ const content = yield* authoringSync(() => renderUpdatedNoteBodyAndFields(source, parts, body, updates));
306
318
  if (content === source.content) return yield* authoringSync(() => noteResult(source, relations, false, documentId));
307
319
  const revision = yield* installNote(platform, vault, id, content, source, lock, dependencies);
308
320
  return yield* authoringSync(() => noteResult({ relativePath: source.relativePath, revision }, relations, true, documentId));