@saasontools/strauss-kb 0.1.2 → 0.1.3

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
@@ -583,6 +583,7 @@ declare const composeInputSchema: z.ZodObject<{
583
583
  last_modified: z.ZodOptional<z.ZodString>;
584
584
  }, z.core.$loose>>>;
585
585
  assumption: z.ZodOptional<z.ZodBoolean>;
586
+ stale_after: z.ZodOptional<z.ZodString>;
586
587
  verify: z.ZodOptional<z.ZodArray<z.ZodString>>;
587
588
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
588
589
  relatedConceptIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -631,9 +632,260 @@ declare const INDEX_FILE = "INDEX.md";
631
632
  * decide what is worth opening, and a list of titles does not answer that.
632
633
  */
633
634
  declare function renderIndex(records: KbRecord[]): string;
635
+ /**
636
+ * One record's index line. The single writer of this shape — `context` emits
637
+ * the same line rather than growing a second index renderer that would drift
638
+ * from this one.
639
+ */
640
+ declare function renderIndexLine(record: KbRecord): string;
634
641
  /** Whether the stored projection still matches the records it claims to index. */
635
642
  declare function indexIsStale(stored: string | null, expected: string): boolean;
636
643
 
644
+ /**
645
+ * Pin manifests, in three layers.
646
+ *
647
+ * Pins are workspace state, not base state: they record which bases a session
648
+ * should be shown at every context birth. The pinned base is never touched —
649
+ * not even its log — because a base must remain copyable without knowing who
650
+ * pins it.
651
+ *
652
+ * The layers, nearest wins when the same base appears in more than one:
653
+ *
654
+ * | layer | file | for |
655
+ * | ------- | --------------------------------------- | -------------------------- |
656
+ * | project | <workspace>/.strauss/kb-pins.json | committed, the team's pins |
657
+ * | local | <workspace>/.strauss/kb-pins.local.json | personal, gitignored |
658
+ * | user | ~/.strauss/kb-pins.json | personal, every workspace |
659
+ *
660
+ * Every manifest's paths resolve against its own root — the workspace for
661
+ * project and local, the home directory for user — so each file is portable
662
+ * with the tree it belongs to. `STRAUSS_KB_USER_ROOT` overrides the user root
663
+ * (tests, unusual homes).
664
+ */
665
+ declare const PINS_FILE: string;
666
+ declare const PINS_LOCAL_FILE: string;
667
+ declare const PIN_LAYERS: readonly ["project", "local", "user"];
668
+ type KbPinLayer = (typeof PIN_LAYERS)[number];
669
+ /**
670
+ * Primary state — it records an intent nothing else holds — but trivially
671
+ * rewritable, so a full rewrite on change is fine and no append log is needed.
672
+ * Unknown keys are preserved on rewrite, the same tolerance the record reader
673
+ * extends to frontmatter it did not write.
674
+ */
675
+ declare const pinSchema: z.ZodObject<{
676
+ path: z.ZodString;
677
+ pinnedAt: z.ZodOptional<z.ZodString>;
678
+ mode: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
679
+ full: "full";
680
+ index: "index";
681
+ }>>>;
682
+ profiles: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodString>>>;
683
+ frozen: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
684
+ }, z.core.$loose>;
685
+ declare const pinsManifestSchema: z.ZodObject<{
686
+ pins: z.ZodDefault<z.ZodArray<z.ZodObject<{
687
+ path: z.ZodString;
688
+ pinnedAt: z.ZodOptional<z.ZodString>;
689
+ mode: z.ZodCatch<z.ZodOptional<z.ZodEnum<{
690
+ full: "full";
691
+ index: "index";
692
+ }>>>;
693
+ profiles: z.ZodCatch<z.ZodOptional<z.ZodArray<z.ZodString>>>;
694
+ frozen: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
695
+ }, z.core.$loose>>>;
696
+ context: z.ZodOptional<z.ZodUnknown>;
697
+ }, z.core.$loose>;
698
+ type KbPin = z.infer<typeof pinSchema>;
699
+ type KbPinsManifest = z.infer<typeof pinsManifestSchema>;
700
+ type KbContextBudgets = {
701
+ budgetTokens?: number;
702
+ fullUnderTokens?: number;
703
+ };
704
+ /** A pin as the merged view hands it back: entry + where it came from. */
705
+ type KbMergedPin = KbPin & {
706
+ layer: KbPinLayer;
707
+ absolutePath: string;
708
+ };
709
+ type KbMergedPins = {
710
+ /** Effective pins after dedup — nearest layer wins per resolved path. */
711
+ pins: KbMergedPin[];
712
+ /** Per-layer manifests that parsed, for budget merging. */
713
+ manifests: Partial<Record<KbPinLayer, KbPinsManifest>>;
714
+ };
715
+ /** One pinned base, with whether it currently resolves to anything readable. */
716
+ type KbPinStatus = {
717
+ /** As stored — relative to its layer's root. */
718
+ path: string;
719
+ layer: KbPinLayer;
720
+ pinnedAt: string | null;
721
+ absolutePath: string;
722
+ /** The directory exists and yielded at least one parseable record. */
723
+ valid: boolean;
724
+ recordCount: number;
725
+ mode: "full" | "index" | null;
726
+ profiles: string[] | null;
727
+ frozen: boolean;
728
+ };
729
+ type KbPinResult = {
730
+ path: string;
731
+ layer: KbPinLayer;
732
+ pinnedAt: string;
733
+ alreadyPinned: boolean;
734
+ mode?: "full" | "index";
735
+ profiles?: string[];
736
+ frozen?: boolean;
737
+ /** Set when the path holds no readable records — pinned anyway. */
738
+ warning?: string;
739
+ };
740
+ type KbPinOptions = {
741
+ mode?: "full" | "index";
742
+ profiles?: string[];
743
+ frozen?: boolean;
744
+ /** Which manifest to write. Defaults to the committed project layer. */
745
+ layer?: KbPinLayer;
746
+ };
747
+
748
+ /**
749
+ * One manifest's budgets for one profile: the named profile's values over the
750
+ * manifest's `"default"` entry. What is absent here falls through to the
751
+ * caller's built-ins — a manifest narrows, it never has to be complete.
752
+ */
753
+ declare function contextProfileBudgets(manifest: KbPinsManifest, profile?: string): KbContextBudgets;
754
+ /**
755
+ * Budgets across the layers: user underneath, local over it, project on top —
756
+ * the committed file is the workspace's word — and explicit flags above all
757
+ * of this, applied by the caller.
758
+ */
759
+ declare function mergedContextBudgets(merged: KbMergedPins, profile?: string): KbContextBudgets;
760
+
761
+ declare class KbPinsMalformedError extends Error {
762
+ constructor(file: string, cause: string);
763
+ }
764
+ declare class KbBaseFrozenError extends Error {
765
+ constructor(bundlePath: string, layer: KbPinLayer);
766
+ }
767
+
768
+ /**
769
+ * Refuses when a write would land in a base this workspace froze. Called by
770
+ * every mutating command; a workspace that pinned a base `--frozen` said the
771
+ * base is concluded, and a quiet write past that would be exactly the silent
772
+ * drift the pin was meant to stop.
773
+ */
774
+ declare function assertBaseNotFrozen(workspaceDir: string, bundlePath: string): Promise<void>;
775
+
776
+ /**
777
+ * One layer's manifest, or an empty one when the file is missing.
778
+ *
779
+ * A malformed file throws rather than being treated as empty: every write path
780
+ * does a full rewrite, and rewriting over content we could not read would
781
+ * destroy the one copy of it. Read-only consumers that must stay silent
782
+ * (`context` from a session hook, the merged reader) skip malformed layers
783
+ * themselves.
784
+ */
785
+ declare function readPinsLayer(workspaceDir: string, layer: KbPinLayer): Promise<KbPinsManifest>;
786
+ /** Where a stored pin points, resolved against its layer's root. */
787
+ declare function resolvePinPath(rootDir: string, path: string): string;
788
+ /**
789
+ * All three layers, merged. A malformed layer is skipped rather than thrown:
790
+ * this feeds hooks at every session start, and one broken personal file must
791
+ * not silence the team's pins — `pin`/`unpin` against the broken layer still
792
+ * refuse loudly.
793
+ */
794
+ declare function readMergedPins(workspaceDir: string): Promise<KbMergedPins>;
795
+
796
+ /** Every effective pin across the layers, with whether it points at records. */
797
+ declare function listPins(store: KbStore, workspaceDir: string): Promise<KbPinStatus[]>;
798
+
799
+ /**
800
+ * Adds a base to one layer's manifest. Idempotent — re-pinning a pinned path
801
+ * with no options returns the existing entry untouched, and re-pinning with
802
+ * `mode`, `profiles`, or `frozen` updates just those fields, which is how a
803
+ * pin's rendering or writability is changed. A path that is not (yet) a valid
804
+ * base succeeds with a warning: bases are routinely pinned before they are
805
+ * populated, the same way records link to records that do not exist yet.
806
+ */
807
+ declare function pinBase(store: KbStore, workspaceDir: string, bundlePath: string, at: string, options?: KbPinOptions): Promise<KbPinResult>;
808
+
809
+ /**
810
+ * Removes a base from every layer that holds it — unpinned means gone, not
811
+ * "gone from one file and still injected from another". A malformed layer is
812
+ * skipped (it cannot be rewritten safely); the layers actually touched are
813
+ * reported.
814
+ */
815
+ declare function unpinBase(workspaceDir: string, bundlePath: string): Promise<{
816
+ path: string;
817
+ removed: boolean;
818
+ layers: KbPinLayer[];
819
+ }>;
820
+
821
+ /**
822
+ * What the hooks ask for by name, so the numbers live in one place and a repo
823
+ * can override them in its pin manifest rather than editing hook commands.
824
+ * `session-start` is a fresh window — room for tiny bases to arrive whole.
825
+ * `compact` competes with a summary for a smaller window — index only.
826
+ * `turn` is per-turn injection (Antigravity) — same tight stance as compact.
827
+ */
828
+ declare const CONTEXT_PROFILES: Record<string, KbContextBudgets>;
829
+ type KbContextOptions = {
830
+ /** Refuse past this. Defaults to 4000 tokens. */
831
+ budgetTokens?: number;
832
+ /** Emit bases whose full `load` fits under this as records, not index. 0 = off. */
833
+ fullUnderTokens?: number;
834
+ /**
835
+ * A named budget set. Resolution, most specific wins: explicit options,
836
+ * then the manifest's `context[profile]` over its `context.default`, then
837
+ * the built-in profile, then the package defaults. An unknown profile is
838
+ * not an error — it simply falls through; hooks must never break over a
839
+ * name.
840
+ */
841
+ profile?: string;
842
+ /**
843
+ * Where budget pressure is reported outside the block itself: a full pin
844
+ * that had to degrade to an index, a block that refused. The block already
845
+ * says both to the agent; this says them to the operator's log.
846
+ */
847
+ warn?: (entry: Record<string, unknown>) => void;
848
+ };
849
+ type KbContextResult = {
850
+ /** The markdown block. Empty when there are no pins — silence, not a stub. */
851
+ block: string;
852
+ /** Refused: over budget. The block then lists the bases instead of the index. */
853
+ refused: boolean;
854
+ approxTokens: number;
855
+ budgetTokens: number;
856
+ bases: {
857
+ path: string;
858
+ absolutePath: string;
859
+ approxTokens: number;
860
+ }[];
861
+ };
862
+ /**
863
+ * Builds the block, or a refusal that lists the bases — never a truncation. A
864
+ * truncated index is indistinguishable from a complete one, so a reader would
865
+ * take a slice for the whole, which is `load`'s argument one layer up.
866
+ */
867
+ declare function buildContext(store: KbStore, workspaceDir: string, options?: KbContextOptions): Promise<KbContextResult>;
868
+ /**
869
+ * The same block in the envelope hook protocols that demand strict JSON on
870
+ * stdout require:
871
+ * those protocols treat non-JSON stdout as a violation, where Claude Code and
872
+ * Codex take plain text. One canonical writer for the block; this is wrapping.
873
+ */
874
+ declare function toHookJson(block: string, event: string): string;
875
+ declare const CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
876
+ declare const CONTEXT_END = "<!-- strauss-kb:end -->";
877
+ type KbSyncResult = {
878
+ file: string;
879
+ action: "created" | "replaced" | "appended" | "removed" | "unchanged";
880
+ };
881
+ /**
882
+ * Idempotently plants the block between sentinels in an instruction file
883
+ * (AGENTS.md, CLAUDE.md). This is how a runtime without a reliable
884
+ * post-compact hook keeps a refreshable index: the file is re-read where the
885
+ * conversation is not. Everything outside the sentinels is left alone.
886
+ */
887
+ declare function syncInstructions(file: string, block: string): Promise<KbSyncResult>;
888
+
637
889
  /**
638
890
  * The frontmatter contract, emitted rather than restated.
639
891
  *
@@ -762,6 +1014,7 @@ declare const decisionInputSchema: z.ZodObject<{
762
1014
  author: z.ZodOptional<z.ZodString>;
763
1015
  last_modified: z.ZodOptional<z.ZodString>;
764
1016
  }, z.core.$loose>>>;
1017
+ stale_after: z.ZodOptional<z.ZodString>;
765
1018
  slug: z.ZodString;
766
1019
  why: z.ZodString;
767
1020
  anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -802,15 +1055,17 @@ declare function selectDecisions(records: KbRecord[]): KbRecord[];
802
1055
  /**
803
1056
  * Every operation a knowledge base exposes, defined once.
804
1057
  *
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.
1058
+ * The CLI and the MCP server are both projections of this table. Kept apart
1059
+ * they drift within a day — fourteen commands against six tools — which is the
1060
+ * same failure as a schema restated in prose beside the code that enforces it,
1061
+ * one level up. A command added to the table appears in both surfaces or in
1062
+ * neither, and a test asserts exactly that.
810
1063
  *
811
1064
  * The two differ only in how arguments arrive: MCP passes an object matching
812
1065
  * `input`, while the CLI has to turn positional argv into the same object.
813
1066
  * `fromArgv` is that adapter and is the only per-surface code a command needs.
1067
+ *
1068
+ * One file per command in this folder; `index.ts` assembles the table.
814
1069
  */
815
1070
  type KbCommandContext = {
816
1071
  store: KbStore;
@@ -820,8 +1075,13 @@ type KbCommandContext = {
820
1075
  type KbCommand<Shape extends z.ZodRawShape = z.ZodRawShape> = {
821
1076
  /** CLI verb. */
822
1077
  name: string;
823
- /** MCP tool name. */
824
- tool: string;
1078
+ /**
1079
+ * MCP tool name. Absent only for CLI-only plumbing (`sync-instructions`),
1080
+ * which exists to edit files for hooks and instruction blocks rather than to
1081
+ * give an agent a capability — the capability, "get the pinned context
1082
+ * block", is `kb_context`.
1083
+ */
1084
+ tool?: string;
825
1085
  /** Argument spelling for CLI usage output. */
826
1086
  usage: string;
827
1087
  /** Shown to an agent choosing a tool, so it carries the judgment too. */
@@ -837,10 +1097,16 @@ type KbCommand<Shape extends z.ZodRawShape = z.ZodRawShape> = {
837
1097
  */
838
1098
  failsWhen?(result: unknown): boolean;
839
1099
  };
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
- }>>>;
1100
+
1101
+ /**
1102
+ * The command table, assembled from one file per command.
1103
+ *
1104
+ * Order is the CLI usage listing's order: the write path, the read path,
1105
+ * base housekeeping, the format, and the workspace pin verbs.
1106
+ */
1107
+
1108
+ declare const KB_COMMANDS: KbCommand[];
1109
+ declare const KB_COMMANDS_BY_NAME: Map<string, KbCommand>;
844
1110
 
845
1111
  /**
846
1112
  * A knowledge base's own MCP server, over stdio.
@@ -974,4 +1240,4 @@ declare function parseMarkdownWithFrontmatter<S extends z.ZodType>(text: string,
974
1240
  frontmatter: ReturnType<S["safeParse"]>;
975
1241
  };
976
1242
 
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 };
1243
+ 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 };