@saasontools/strauss-kb 0.1.2 → 0.1.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/dist/index.d.cts CHANGED
@@ -334,6 +334,12 @@ type KbWriteInput = {
334
334
  /** Replace an existing record rather than failing on the collision. */
335
335
  overwrite?: boolean;
336
336
  };
337
+ type KbWriteResult = KbRecord & {
338
+ /** Whether this write also marked prior records superseded. */
339
+ action: "created" | "superseded-prior";
340
+ /** `frontmatter.strauss_supersedes` ids that were actually marked. */
341
+ supersededIds: string[];
342
+ };
337
343
  /**
338
344
  * Reads and writes a knowledge bundle.
339
345
  *
@@ -357,7 +363,7 @@ declare class KbStore {
357
363
  * concept id, so a caller cannot produce a file whose identity disagrees with
358
364
  * its contents.
359
365
  */
360
- write(bundlePath: string, input: KbWriteInput, actor?: string): Promise<KbRecord>;
366
+ write(bundlePath: string, input: KbWriteInput, actor?: string): Promise<KbWriteResult>;
361
367
  /** One record by concept id, or null when it does not exist. */
362
368
  read(bundlePath: string, conceptId: string): Promise<KbRecord | null>;
363
369
  /**
@@ -447,6 +453,19 @@ declare class KbStore {
447
453
  * knows which agent touched what. So a bad line is surfaced and left alone.
448
454
  */
449
455
  readLog(bundlePath: string): Promise<ReturnType<typeof parseLog>>;
456
+ /**
457
+ * `markSuperseded`, tolerant of the two ways it legitimately doesn't land:
458
+ * a missing target (a broken link, legal per compose.ts) or a CAS conflict
459
+ * from a concurrent writer touching the same target. A conflict is retried
460
+ * a bounded number of times — each attempt re-reads the target fresh — and
461
+ * on the last, `false` reports "not marked" rather than throwing: the
462
+ * caller's own record is already published, so failing here would leave
463
+ * that publish unreported instead of undone. kb_validate's existing
464
+ * "not marked superseded" check is what surfaces the residue.
465
+ */
466
+ private markSupersededRetrying;
467
+ /** The one-directional half of `supersede`: marks `conceptId` superseded. */
468
+ private markSuperseded;
450
469
  private mutate;
451
470
  /**
452
471
  * Two guarantees, both about writers running in parallel.
@@ -583,6 +602,7 @@ declare const composeInputSchema: z.ZodObject<{
583
602
  last_modified: z.ZodOptional<z.ZodString>;
584
603
  }, z.core.$loose>>>;
585
604
  assumption: z.ZodOptional<z.ZodBoolean>;
605
+ stale_after: z.ZodOptional<z.ZodString>;
586
606
  verify: z.ZodOptional<z.ZodArray<z.ZodString>>;
587
607
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
588
608
  relatedConceptIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -631,9 +651,260 @@ declare const INDEX_FILE = "INDEX.md";
631
651
  * decide what is worth opening, and a list of titles does not answer that.
632
652
  */
633
653
  declare function renderIndex(records: KbRecord[]): string;
654
+ /**
655
+ * One record's index line. The single writer of this shape — `context` emits
656
+ * the same line rather than growing a second index renderer that would drift
657
+ * from this one.
658
+ */
659
+ declare function renderIndexLine(record: KbRecord): string;
634
660
  /** Whether the stored projection still matches the records it claims to index. */
635
661
  declare function indexIsStale(stored: string | null, expected: string): boolean;
636
662
 
663
+ /**
664
+ * Pin manifests, in three layers.
665
+ *
666
+ * Pins are workspace state, not base state: they record which bases a session
667
+ * should be shown at every context birth. The pinned base is never touched —
668
+ * not even its log — because a base must remain copyable without knowing who
669
+ * pins it.
670
+ *
671
+ * The layers, nearest wins when the same base appears in more than one:
672
+ *
673
+ * | layer | file | for |
674
+ * | ------- | --------------------------------------- | -------------------------- |
675
+ * | project | <workspace>/.strauss/kb-pins.json | committed, the team's pins |
676
+ * | local | <workspace>/.strauss/kb-pins.local.json | personal, gitignored |
677
+ * | user | ~/.strauss/kb-pins.json | personal, every workspace |
678
+ *
679
+ * Every manifest's paths resolve against its own root — the workspace for
680
+ * project and local, the home directory for user — so each file is portable
681
+ * with the tree it belongs to. `STRAUSS_KB_USER_ROOT` overrides the user root
682
+ * (tests, unusual homes).
683
+ */
684
+ declare const PINS_FILE: string;
685
+ declare const PINS_LOCAL_FILE: string;
686
+ declare const PIN_LAYERS: readonly ["project", "local", "user"];
687
+ type KbPinLayer = (typeof PIN_LAYERS)[number];
688
+ /**
689
+ * Primary state — it records an intent nothing else holds — but trivially
690
+ * rewritable, so a full rewrite on change is fine and no append log is needed.
691
+ * Unknown keys are preserved on rewrite, the same tolerance the record reader
692
+ * extends to frontmatter it did not write.
693
+ */
694
+ declare const pinSchema: z.ZodObject<{
695
+ path: z.ZodString;
696
+ pinnedAt: z.ZodOptional<z.ZodString>;
697
+ mode: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
698
+ full: "full";
699
+ index: "index";
700
+ }>>>;
701
+ profiles: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodString>>>;
702
+ frozen: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
703
+ }, z.core.$loose>;
704
+ declare const pinsManifestSchema: z.ZodObject<{
705
+ pins: z.ZodDefault<z.ZodArray<z.ZodObject<{
706
+ path: z.ZodString;
707
+ pinnedAt: z.ZodOptional<z.ZodString>;
708
+ mode: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
709
+ full: "full";
710
+ index: "index";
711
+ }>>>;
712
+ profiles: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodString>>>;
713
+ frozen: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
714
+ }, z.core.$loose>>>;
715
+ context: z.ZodOptional<z.ZodUnknown>;
716
+ }, z.core.$loose>;
717
+ type KbPin = z.infer<typeof pinSchema>;
718
+ type KbPinsManifest = z.infer<typeof pinsManifestSchema>;
719
+ type KbContextBudgets = {
720
+ budgetTokens?: number;
721
+ fullUnderTokens?: number;
722
+ };
723
+ /** A pin as the merged view hands it back: entry + where it came from. */
724
+ type KbMergedPin = KbPin & {
725
+ layer: KbPinLayer;
726
+ absolutePath: string;
727
+ };
728
+ type KbMergedPins = {
729
+ /** Effective pins after dedup — nearest layer wins per resolved path. */
730
+ pins: KbMergedPin[];
731
+ /** Per-layer manifests that parsed, for budget merging. */
732
+ manifests: Partial<Record<KbPinLayer, KbPinsManifest>>;
733
+ };
734
+ /** One pinned base, with whether it currently resolves to anything readable. */
735
+ type KbPinStatus = {
736
+ /** As stored — relative to its layer's root. */
737
+ path: string;
738
+ layer: KbPinLayer;
739
+ pinnedAt: string | null;
740
+ absolutePath: string;
741
+ /** The directory exists and yielded at least one parseable record. */
742
+ valid: boolean;
743
+ recordCount: number;
744
+ mode: "full" | "index" | null;
745
+ profiles: string[] | null;
746
+ frozen: boolean;
747
+ };
748
+ type KbPinResult = {
749
+ path: string;
750
+ layer: KbPinLayer;
751
+ pinnedAt: string;
752
+ alreadyPinned: boolean;
753
+ mode?: "full" | "index";
754
+ profiles?: string[];
755
+ frozen?: boolean;
756
+ /** Set when the path holds no readable records — pinned anyway. */
757
+ warning?: string;
758
+ };
759
+ type KbPinOptions = {
760
+ mode?: "full" | "index";
761
+ profiles?: string[];
762
+ frozen?: boolean;
763
+ /** Which manifest to write. Defaults to the committed project layer. */
764
+ layer?: KbPinLayer;
765
+ };
766
+
767
+ /**
768
+ * One manifest's budgets for one profile: the named profile's values over the
769
+ * manifest's `"default"` entry. What is absent here falls through to the
770
+ * caller's built-ins — a manifest narrows, it never has to be complete.
771
+ */
772
+ declare function contextProfileBudgets(manifest: KbPinsManifest, profile?: string): KbContextBudgets;
773
+ /**
774
+ * Budgets across the layers: user underneath, local over it, project on top —
775
+ * the committed file is the workspace's word — and explicit flags above all
776
+ * of this, applied by the caller.
777
+ */
778
+ declare function mergedContextBudgets(merged: KbMergedPins, profile?: string): KbContextBudgets;
779
+
780
+ declare class KbPinsMalformedError extends Error {
781
+ constructor(file: string, cause: string);
782
+ }
783
+ declare class KbBaseFrozenError extends Error {
784
+ constructor(bundlePath: string, layer: KbPinLayer);
785
+ }
786
+
787
+ /**
788
+ * Refuses when a write would land in a base this workspace froze. Called by
789
+ * every mutating command; a workspace that pinned a base `--frozen` said the
790
+ * base is concluded, and a quiet write past that would be exactly the silent
791
+ * drift the pin was meant to stop.
792
+ */
793
+ declare function assertBaseNotFrozen(workspaceDir: string, bundlePath: string): Promise<void>;
794
+
795
+ /**
796
+ * One layer's manifest, or an empty one when the file is missing.
797
+ *
798
+ * A malformed file throws rather than being treated as empty: every write path
799
+ * does a full rewrite, and rewriting over content we could not read would
800
+ * destroy the one copy of it. Read-only consumers that must stay silent
801
+ * (`context` from a session hook, the merged reader) skip malformed layers
802
+ * themselves.
803
+ */
804
+ declare function readPinsLayer(workspaceDir: string, layer: KbPinLayer): Promise<KbPinsManifest>;
805
+ /** Where a stored pin points, resolved against its layer's root. */
806
+ declare function resolvePinPath(rootDir: string, path: string): string;
807
+ /**
808
+ * All three layers, merged. A malformed layer is skipped rather than thrown:
809
+ * this feeds hooks at every session start, and one broken personal file must
810
+ * not silence the team's pins — `pin`/`unpin` against the broken layer still
811
+ * refuse loudly.
812
+ */
813
+ declare function readMergedPins(workspaceDir: string): Promise<KbMergedPins>;
814
+
815
+ /** Every effective pin across the layers, with whether it points at records. */
816
+ declare function listPins(store: KbStore, workspaceDir: string): Promise<KbPinStatus[]>;
817
+
818
+ /**
819
+ * Adds a base to one layer's manifest. Idempotent — re-pinning a pinned path
820
+ * with no options returns the existing entry untouched, and re-pinning with
821
+ * `mode`, `profiles`, or `frozen` updates just those fields, which is how a
822
+ * pin's rendering or writability is changed. A path that is not (yet) a valid
823
+ * base succeeds with a warning: bases are routinely pinned before they are
824
+ * populated, the same way records link to records that do not exist yet.
825
+ */
826
+ declare function pinBase(store: KbStore, workspaceDir: string, bundlePath: string, at: string, options?: KbPinOptions): Promise<KbPinResult>;
827
+
828
+ /**
829
+ * Removes a base from every layer that holds it — unpinned means gone, not
830
+ * "gone from one file and still injected from another". A malformed layer is
831
+ * skipped (it cannot be rewritten safely); the layers actually touched are
832
+ * reported.
833
+ */
834
+ declare function unpinBase(workspaceDir: string, bundlePath: string): Promise<{
835
+ path: string;
836
+ removed: boolean;
837
+ layers: KbPinLayer[];
838
+ }>;
839
+
840
+ /**
841
+ * What the hooks ask for by name, so the numbers live in one place and a repo
842
+ * can override them in its pin manifest rather than editing hook commands.
843
+ * `session-start` is a fresh window — room for tiny bases to arrive whole.
844
+ * `compact` competes with a summary for a smaller window — index only.
845
+ * `turn` is per-turn injection (Antigravity) — same tight stance as compact.
846
+ */
847
+ declare const CONTEXT_PROFILES: Record<string, KbContextBudgets>;
848
+ type KbContextOptions = {
849
+ /** Refuse past this. Defaults to 4000 tokens. */
850
+ budgetTokens?: number;
851
+ /** Emit bases whose full `load` fits under this as records, not index. 0 = off. */
852
+ fullUnderTokens?: number;
853
+ /**
854
+ * A named budget set. Resolution, most specific wins: explicit options,
855
+ * then the manifest's `context[profile]` over its `context.default`, then
856
+ * the built-in profile, then the package defaults. An unknown profile is
857
+ * not an error — it simply falls through; hooks must never break over a
858
+ * name.
859
+ */
860
+ profile?: string;
861
+ /**
862
+ * Where budget pressure is reported outside the block itself: a full pin
863
+ * that had to degrade to an index, a block that refused. The block already
864
+ * says both to the agent; this says them to the operator's log.
865
+ */
866
+ warn?: (entry: Record<string, unknown>) => void;
867
+ };
868
+ type KbContextResult = {
869
+ /** The markdown block. Empty when there are no pins — silence, not a stub. */
870
+ block: string;
871
+ /** Refused: over budget. The block then lists the bases instead of the index. */
872
+ refused: boolean;
873
+ approxTokens: number;
874
+ budgetTokens: number;
875
+ bases: {
876
+ path: string;
877
+ absolutePath: string;
878
+ approxTokens: number;
879
+ }[];
880
+ };
881
+ /**
882
+ * Builds the block, or a refusal that lists the bases — never a truncation. A
883
+ * truncated index is indistinguishable from a complete one, so a reader would
884
+ * take a slice for the whole, which is `load`'s argument one layer up.
885
+ */
886
+ declare function buildContext(store: KbStore, workspaceDir: string, options?: KbContextOptions): Promise<KbContextResult>;
887
+ /**
888
+ * The same block in the envelope hook protocols that demand strict JSON on
889
+ * stdout require:
890
+ * those protocols treat non-JSON stdout as a violation, where Claude Code and
891
+ * Codex take plain text. One canonical writer for the block; this is wrapping.
892
+ */
893
+ declare function toHookJson(block: string, event: string): string;
894
+ declare const CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
895
+ declare const CONTEXT_END = "<!-- strauss-kb:end -->";
896
+ type KbSyncResult = {
897
+ file: string;
898
+ action: "created" | "replaced" | "appended" | "removed" | "unchanged";
899
+ };
900
+ /**
901
+ * Idempotently plants the block between sentinels in an instruction file
902
+ * (AGENTS.md, CLAUDE.md). This is how a runtime without a reliable
903
+ * post-compact hook keeps a refreshable index: the file is re-read where the
904
+ * conversation is not. Everything outside the sentinels is left alone.
905
+ */
906
+ declare function syncInstructions(file: string, block: string): Promise<KbSyncResult>;
907
+
637
908
  /**
638
909
  * The frontmatter contract, emitted rather than restated.
639
910
  *
@@ -762,6 +1033,7 @@ declare const decisionInputSchema: z.ZodObject<{
762
1033
  author: z.ZodOptional<z.ZodString>;
763
1034
  last_modified: z.ZodOptional<z.ZodString>;
764
1035
  }, z.core.$loose>>>;
1036
+ stale_after: z.ZodOptional<z.ZodString>;
765
1037
  slug: z.ZodString;
766
1038
  why: z.ZodString;
767
1039
  anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -802,15 +1074,17 @@ declare function selectDecisions(records: KbRecord[]): KbRecord[];
802
1074
  /**
803
1075
  * Every operation a knowledge base exposes, defined once.
804
1076
  *
805
- * The CLI and the MCP server are both projections of this list. Kept apart they
806
- * drift within a day — fourteen commands against six tools — which is the same
807
- * failure as a schema restated in prose beside the code that enforces it, one
808
- * level up. A command added here appears in both surfaces or in neither, and a
809
- * test asserts exactly that.
1077
+ * The CLI and the MCP server are both projections of this table. Kept apart
1078
+ * they drift within a day — fourteen commands against six tools — which is the
1079
+ * same failure as a schema restated in prose beside the code that enforces it,
1080
+ * one level up. A command added to the table appears in both surfaces or in
1081
+ * neither, and a test asserts exactly that.
810
1082
  *
811
1083
  * The two differ only in how arguments arrive: MCP passes an object matching
812
1084
  * `input`, while the CLI has to turn positional argv into the same object.
813
1085
  * `fromArgv` is that adapter and is the only per-surface code a command needs.
1086
+ *
1087
+ * One file per command in this folder; `index.ts` assembles the table.
814
1088
  */
815
1089
  type KbCommandContext = {
816
1090
  store: KbStore;
@@ -820,8 +1094,13 @@ type KbCommandContext = {
820
1094
  type KbCommand<Shape extends z.ZodRawShape = z.ZodRawShape> = {
821
1095
  /** CLI verb. */
822
1096
  name: string;
823
- /** MCP tool name. */
824
- tool: string;
1097
+ /**
1098
+ * MCP tool name. Absent only for CLI-only plumbing (`sync-instructions`),
1099
+ * which exists to edit files for hooks and instruction blocks rather than to
1100
+ * give an agent a capability — the capability, "get the pinned context
1101
+ * block", is `kb_context`.
1102
+ */
1103
+ tool?: string;
825
1104
  /** Argument spelling for CLI usage output. */
826
1105
  usage: string;
827
1106
  /** Shown to an agent choosing a tool, so it carries the judgment too. */
@@ -837,10 +1116,16 @@ type KbCommand<Shape extends z.ZodRawShape = z.ZodRawShape> = {
837
1116
  */
838
1117
  failsWhen?(result: unknown): boolean;
839
1118
  };
840
- declare const KB_COMMANDS: KbCommand<z.ZodRawShape>[];
841
- declare const KB_COMMANDS_BY_NAME: Map<string, KbCommand<Readonly<{
842
- [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
843
- }>>>;
1119
+
1120
+ /**
1121
+ * The command table, assembled from one file per command.
1122
+ *
1123
+ * Order is the CLI usage listing's order: the write path, the read path,
1124
+ * base housekeeping, the format, and the workspace pin verbs.
1125
+ */
1126
+
1127
+ declare const KB_COMMANDS: KbCommand[];
1128
+ declare const KB_COMMANDS_BY_NAME: Map<string, KbCommand>;
844
1129
 
845
1130
  /**
846
1131
  * A knowledge base's own MCP server, over stdio.
@@ -974,4 +1259,4 @@ declare function parseMarkdownWithFrontmatter<S extends z.ZodType>(text: string,
974
1259
  frontmatter: ReturnType<S["safeParse"]>;
975
1260
  };
976
1261
 
977
- export { BaseError, type ComposeInput, type ComposedRecord, DECISION_TYPE, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, type KbCommand, type KbCommandContext, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, createKbMcpServer, decisionInputSchema, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, loadQmd, matchToDiff, parseLog, parseMarkdownWithFrontmatter, renderIndex, renderLogEntry, resolveHeads, resolveHits, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, trace, validateBundle };
1262
+ export { BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposedRecord, DECISION_TYPE, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, KbBaseFrozenError, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, assertBaseNotFrozen, buildContext, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, renderIndex, renderIndexLine, renderLogEntry, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };